Extend Solr search: content coverage, 14.0.2, news recency, result images

- Index the text columns of the market/solution Content Blocks and the
  vitec_items/vitec_faq_items collection children; story documents now
  include their body (inline tt_content children via
  tx_vitec_usecase_content, same per-element extraction as pages) -
  stories were indexed with subtitle and teaser only until now
- Upgrade EXT:solr 14.0.0-RC1 -> 14.0.2 final (the Solr image moved to a
  fresh volume because the configset changed; synonyms restored via REST)
- News decay with age by publication date: crdate is the import date, so
  news map datetime to created; boost function gated to news only
- Results carry `image`: processed 640px tile image per type,
  root-relative, null for pages and downloads
- Spec v1.21: 7.16 states the /_json prefix and lists `solution` in the
  type vocabulary; footer had been stuck at v1.19
- suggest-demo.html fetches through /_json after the URL swap
This commit is contained in:
orasche committed 2026-10-07 16:22:50 +02:00
1 parent 8e67f5db23
commit 9b1ad202a9
7 files changed
+242 -23

No files matched your search

@@ -27,7 +27,7 @@ use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
* or the FlexForm override on the plugin),
* "numFound": total hits,
* "totalPages": ceil(numFound / resultsPerPage),
* "results": [ { "title", "url", "type", "teaser" } ],
* "results": [ { "title", "url", "type", "teaser", "image" } ],
* "filter": the active type filter or null,
* "facets": { "type": [ { "value", "count", "active" } ] } -
* counts stay complete while a filter is active,
@@ -217,11 +217,20 @@ class SearchJsonRenderer
} else {
$teaser = mb_substr(trim((string)$document->getContent()), 0, self::TEASER_FALLBACK_LENGTH);
}
$image = (string)($document['image'] ?? '');
// IMG_RESOURCE yields fileadmin/... without a leading slash -
// made root-relative like every other URL in the payload
if ($image !== '' && !str_starts_with($image, '/') && !preg_match('#^https?://#', $image)) {
$image = '/' . $image;
}
$results[] = [
'title' => (string)$document->getTitle(),
'url' => (string)$document->getUrl(),
'type' => self::TYPE_LABELS[$document->getType()] ?? (string)$document->getType(),
'teaser' => $teaser,
// processed tile image (640px wide) from the index; null when the
// document type has none (pages, downloads) or no image is set
'image' => $image !== '' ? $image : null,
];
}
@@ -394,6 +394,67 @@ plugin.tx_solr.index.queue.pages.fields {
40 = TEXT
40.field = vitec_quote
40.noTrimWrap = || |
# Content Blocks with their own text columns (market/solution
# templates, flip cards) - the workbook imports fill these
50 = TEXT
50.field = vitec_subline
50.noTrimWrap = || |
60 = TEXT
60.field = vitec_intro_headline
60.noTrimWrap = || |
70 = TEXT
70.field = vitec_intro_text
70.noTrimWrap = || |
80 = TEXT
80.field = vitec_text1
80.noTrimWrap = || |
90 = TEXT
90.field = vitec_text2
90.noTrimWrap = || |
100 = TEXT
100.field = vitec_back_text
100.noTrimWrap = || |
110 = TEXT
110.field = vitec_quote_position
110.noTrimWrap = || |
# Content Blocks collection children (cards, icon lists,
# FAQ) live in their own tables, keyed on the parent uid
120 = CONTENT
120 {
table = vitec_items
select {
pidInList.field = pid
where.dataWrap = {#foreign_table_parent_uid} = {field:uid}
orderBy = sorting
}
renderObj = COA
renderObj {
10 = TEXT
10.field = headline
10.noTrimWrap = || |
20 = TEXT
20.field = text
20.noTrimWrap = || |
}
}
130 = CONTENT
130 {
table = vitec_faq_items
select {
pidInList.field = pid
where.dataWrap = {#foreign_table_parent_uid} = {field:uid}
orderBy = sorting
}
renderObj = COA
renderObj {
10 = TEXT
10.field = question
10.noTrimWrap = || |
20 = TEXT
20.field = answer
20.noTrimWrap = || |
}
}
}
}
}
@@ -419,6 +480,37 @@ plugin.tx_solr.index.queue {
products = 1
products {
# result tile image: first reference, processed to 640px width
fields.image = FILES
fields.image {
references {
table = tx_vitec_domain_model_product
uid.data = field:uid
fieldName = image
}
maxItems = 1
renderObj = IMG_RESOURCE
renderObj {
file.import.data = file:current:uid
file.treatIdAsReference = 1
file.width = 640
}
stdWrap.ifEmpty.cObject = FILES
stdWrap.ifEmpty.cObject {
references {
table = tx_vitec_domain_model_product
uid.data = field:uid
fieldName = productimage
}
maxItems = 1
renderObj = IMG_RESOURCE
renderObj {
file.import.data = file:current:uid
file.treatIdAsReference = 1
file.width = 640
}
}
}
# sys_category titles - feeds the category facet
fields.category_stringM = SOLR_RELATION
fields.category_stringM {
@@ -470,6 +562,22 @@ plugin.tx_solr.index.queue {
markets = 1
markets {
# result tile image: first reference, processed to 640px width
fields.image = FILES
fields.image {
references {
table = tx_vitec_domain_model_market
uid.data = field:uid
fieldName = image
}
maxItems = 1
renderObj = IMG_RESOURCE
renderObj {
file.import.data = file:current:uid
file.treatIdAsReference = 1
file.width = 640
}
}
type = tx_vitec_domain_model_market
# only markets with an editorially assigned detail page are linkable;
# grows automatically as editors fill the field (4 of 34 right now)
@@ -503,6 +611,37 @@ plugin.tx_solr.index.queue {
usecases = 1
usecases {
# result tile image: first reference, processed to 640px width
fields.image = FILES
fields.image {
references {
table = tx_vitec_domain_model_usecase
uid.data = field:uid
fieldName = card_image
}
maxItems = 1
renderObj = IMG_RESOURCE
renderObj {
file.import.data = file:current:uid
file.treatIdAsReference = 1
file.width = 640
}
stdWrap.ifEmpty.cObject = FILES
stdWrap.ifEmpty.cObject {
references {
table = tx_vitec_domain_model_usecase
uid.data = field:uid
fieldName = hero_bgimage
}
maxItems = 1
renderObj = IMG_RESOURCE
renderObj {
file.import.data = file:current:uid
file.treatIdAsReference = 1
file.width = 640
}
}
}
# market titles from the MM relation - feeds the market facet
fields.market_stringM = SOLR_RELATION
fields.market_stringM {
@@ -524,6 +663,18 @@ plugin.tx_solr.index.queue {
20 = TEXT
20.field = teaser
20.noTrimWrap = || |
# the story body: inline tt_content children (text, columns,
# quotations) - same per-element extraction as page content
30 = CONTENT
30 {
table = tt_content
select {
pidInList.field = pid
where.dataWrap = {#tx_vitec_usecase_content} = {field:uid}
orderBy = sorting
}
renderObj < plugin.tx_solr.index.queue.pages.fields.content.cObject.10.renderObj
}
}
}
url = TEXT
@@ -544,6 +695,22 @@ plugin.tx_solr.index.queue {
# 39 records a detail_page (and slug). Same pattern as markets.
solutions = 1
solutions {
# result tile image: first reference, processed to 640px width
fields.image = FILES
fields.image {
references {
table = tx_vitec_domain_model_solution
uid.data = field:uid
fieldName = image
}
maxItems = 1
renderObj = IMG_RESOURCE
renderObj {
file.import.data = file:current:uid
file.treatIdAsReference = 1
file.width = 640
}
}
type = tx_vitec_domain_model_solution
additionalWhereClause = detail_page > 0
fields {
@@ -575,6 +742,25 @@ plugin.tx_solr.index.queue {
news = 1
news {
# crdate is the IMPORT date (Jul/Aug 2026) - the publication date
# lives in datetime. `created` feeds the news recency boost.
fields.created = datetime
# result tile image: first reference, processed to 640px width
fields.image = FILES
fields.image {
references {
table = tx_news_domain_model_news
uid.data = field:uid
fieldName = fal_media
}
maxItems = 1
renderObj = IMG_RESOURCE
renderObj {
file.import.data = file:current:uid
file.treatIdAsReference = 1
file.width = 640
}
}
type = tx_news_domain_model_news
# type 1 ("page as news") points at pages that are already indexed as
# pages - indexing them again would duplicate every one of them
@@ -707,3 +893,9 @@ plugin.tx_solr.suggest {
# type and the full indexed content - too fat and off-contract for type-ahead
showTopResults = 0
}
# News recency: old news decays against everything else. Additive edismax
# boost (bf): non-news documents get the constant 10, news get
# recip(age) = 10 when new, ~5 after one year, ~0.7 after 13 years.
# The gate keeps products/pages/stories out of the decay entirely.
plugin.tx_solr.search.query.boostFunction = if(termfreq(type,'tx_news_domain_model_news'),recip(ms(NOW,created),3.16e-11,10,1),10)
@@ -3,9 +3,9 @@
| | |
|---|---|
| **Document identifier** | EVO‑VITEC‑HL‑001 |
| **Version** | 1.20 |
| **Version** | 1.21 |
| **Status** | Released |
| **Date** | 2026‑10‑01 |
| **Date** | 2026‑10‑07 |
| **Applies to** | `evomedien/vitec` on TYPO3 v14.3 (headless) |
| **Owner** | evomedien — VITEC relaunch |
@@ -33,6 +33,7 @@
| 1.18 | 2026‑09‑24 | **Interface change (additive).** Products gained `solutions` — an MM relation to solutions, emitted as solution cards in both product payloads — and a **Resources tab**: child table `tx_vitec_product_resource` with documentation / support cards (editable title, text and icon around one link, resolved per 9.12; cards without a resolvable link are not emitted) plus a `datasheets` MM selection of download records rendered into the same envelope as `type: datasheet` cards, delivered detail‑only as the `resources` list envelope whose `layout` switches to `carousel` above three cards in total (7.15). The `solutions` relation accepts group‑level records: the eight groups exist as `is_group = 1` solutions (created by `vitec:create-solution-groups`, excluded from lists and from `vitec:align-solutions`) because the agency workbooks reference the group level. Editorial: the footer had been stuck at v1.15 since 1.16/1.17 (2026‑09‑11). |
| 1.19 | 2026‑09‑25 | **Interface change (additive).** The `vitec_card` Content Block gained the `layout_variant` value `flipcard` and a *Card Back* palette: `back_headline` (string, max 100) and `back_text` (richtext → HTML). The two fields are `displayCond`‑gated on the flipcard layout in the backend but serialised unconditionally (7.7) — the front end **shall** key the back face on `layout_variant`, never on field presence, because stored back values persist when an editor switches the layout away. Backend repair: `vitec_layout_variant` / `vitec_background_variant` are shared with `vitec_ctabanner`, and the index‑wise Content‑Blocks merge had dropped four card‑only select values (“Missing label”) — per‑type item lists now come from `columnsOverrides` (7.7); the Card‑Back palette lives in the Layout tab below the variant select. Editorial: the head‑matter version had been stuck at v1.17 since 1.18. |
| 1.20 | 2026‑10‑01 | **Interface change (additive).** Sub-market page template: frontend layout 21 with one colPos group per design section (`colPos110`..`colPos160`, clause 7.17); new Content Blocks `vitec_markethero`, `vitec_marketintro`, `vitec_marketinsights`; new plugin `vitec_productfeature` (key `productfeature`, one product in the 7.15.2 item shape); story cards gained `quote`/`quotePerson`/`quoteRole`; `vitec_usecaselist` gained the layout value `testimonials`; `vitec_productlist` gained hand-picked product selection (editor order, category filter ignored). Field semantics: Template_Sub-Market_Spezifikation_2026-10-01.md. |
| 1.21 | 2026‑10‑07 | **Interface change (additive) and corpus change.** Search results carry `image` (processed 640 px tile image, root-relative, `null` for types without one). Corpus: story documents now include their body (the inline `tt_content` children), page and story content include the text columns of the market/solution Content Blocks and the `vitec_items`/`vitec_faq_items` collection children; solutions are indexed. News rank by publication date (`datetime`; `crdate` is the import date) with an age-decay boost. EXT:solr upgraded from 14.0.0-RC1 to 14.0.2 final (TYPO3 and the Solr image). Editorial: 7.16 now states the `/_json` prefix the URL swap introduced, the `type` vocabulary lists `solution` (delivered since 1.14), and the footer had been stuck at v1.19. |
This document is drafted in the style of, and adopts the terminology conventions of,
ISO/IEC/IEEE 42010 (architecture description), ISO/IEC/IEEE 26514 (information for
@@ -1033,6 +1034,10 @@ headless by `SearchJsonRenderer` under the payload key `search`. The renderer ru
EXT:solr’s own query pipeline (`SearchRequestBuilder` → `SearchResultSetService`) -
only the rendering differs from the stock Fluid plugin.
Since the URL swap of 2026‑09‑28 every JSON URL carries the prefix `/_json`:
the search answers at `/_json/search?q=…`, while the bare `/search` serves the
single-page app. Paths in this clause are given without the prefix.
**Request.** GET parameters on the search page:
| Parameter | Meaning |
@@ -1071,13 +1076,15 @@ identifier).
},
"results": [
{ "title": "MGW Diamond-H", "url": "/product/mgw-diamond-h-hdmi-encoder",
"type": "product", "teaser": "… 4K HDMI <mark>Encoder</mark> …" }
"type": "product", "teaser": "… 4K HDMI <mark>Encoder</mark> …",
"image": "/fileadmin/_processed_/…/csm_mgw-diamond-h_….jpg" }
],
"suggestions": []
}
```
- `type` **shall** be one of `page | product | market | story | news | download`
- `type` **shall** be one of `page | product | market | solution | story | news |
download`
(mapping table in the renderer; unknown index types pass through verbatim).
- `teaser` carries the highlighted fragment (`<mark>…</mark>`, fragments joined
with " ... ") when highlighting applies, otherwise a plain 250-character excerpt
@@ -1085,6 +1092,10 @@ identifier).
- `url` follows the shared link contract: root-relative, resolved server-side.
`download` results point at the forced-download endpoint `/download/<slug>`
(5.3) - front ends should present them as file downloads.
- `image` is a processed tile image (640 px wide, root-relative) or `null`.
Sources: products `image` (fallback `productimage`), stories `card_image`
(fallback `hero_bgimage`), markets/solutions `image`, news `fal_media`;
pages and downloads carry none.
- `facets.type` carries the per-type counts for filter tabs, ordered by count.
While a `filter` is active the counts remain those of the unfiltered query, so
tabs do not collapse - except when the filtered result is empty, where only
@@ -1102,11 +1113,16 @@ resolvable public URL - products, markets (only those with `detail_page`),
use cases, news (`type = 0`; "page as news" records are excluded because their
target pages are already indexed) and downloads (`private_download = 0 AND
hideonwebsite = 0`; their `content` additionally carries the file text extracted
through Apache Tika, so datasheet specifications are searchable). Solutions are not indexed until they carry slugs or detail
pages. Ranking boosts products (^10) and stories (^2). Every executed search is
through Apache Tika, so datasheet specifications are searchable). Solutions are indexed like markets (`detail_page > 0`). Stories include their
body - the inline `tt_content` children - and both page and story content
include the text columns of the market/solution Content Blocks and the
collection children (`vitec_items`, `vitec_faq_items`). Ranking boosts products
(^10) and stories (^2); news decay with age by their publication date
(`datetime` mapped to `created` - `crdate` is the import date), all other
types are exempt. Every executed search is
logged to `tx_solr_statistics` with the last two IP octets masked.
**Autocomplete.** `GET /search?type=7384&tx_solr[queryString]=<input>` answers as
**Autocomplete.** `GET /_json/search?type=7384&tx_solr[queryString]=<input>` answers as
a lean JSON page type without the page envelope:
```json
@@ -1120,14 +1136,17 @@ wired as **USER_INT** - the example shipped with EXT:solr predates TYPO3 v14
(`config.no_cache` no longer exists), and a cached USER would pin the first
query’s suggestions for every later request. Front ends should debounce
(~200 ms), start at 2-3 characters, and turn a picked suggestion into a
regular `/search?q=` request. Top-result documents are deliberately disabled
regular `/_json/search?q=` request. Top-result documents are deliberately disabled
(`showTopResults = 0`): their shape is hardcoded upstream and carries the raw
document type and the full indexed content.
**Operations.** Indexing runs automatically: record and page saves enter the
index queue, a scheduler task (Index Queue Worker, every 5 minutes) pushes them
to Solr; the CLI command `vitec:solr-index` (with `--initialize` and `--debug`)
covers manual runs - EXT:solr 14 ships no console commands of its own. Page
covers manual runs - EXT:solr 14 ships no console commands of its own. Versions:
EXT:solr 14.0.2 and the matching image `typo3solr/ext-solr:14.0.2` (Solr 10);
an image upgrade needs a fresh volume, because an existing volume keeps its
old configset - managed synonyms are restored through the REST API. Page
content is extracted from the page’s `tt_content` rows via
`index.queue.pages.fields.content` because the headless JSON output carries no
`TYPO3SEARCH` markers.
@@ -1538,4 +1557,4 @@ remediation.
- Header convention — the uniform header section across CEs, plugins and containers.
- `Configuration/Sets/Vitecset/setup.typoscript` — the single TypoScript entry point.
*End of document EVO‑VITEC‑HL‑001 v1.19.*
*End of document EVO‑VITEC‑HL‑001 v1.21.*