Add type filter and facet counts to the search endpoint
The search JSON gains tab support: the new `filter` GET parameter restricts results to one document type (product, news, download, story, page, market); the response carries `filter` (active value or null) and `facets.type` with per-type counts and active flags. Counts stay complete while a filter is active (keepAllFacetsOnSelection), so tabs never collapse - except on an empty filtered result, documented in spec v1.12 clause 7.16. The parameter is named `filter` because `type` is TYPO3's reserved page-type parameter and crashes page resolution; like q and page it is excluded from cHash validation. Facets come from EXT:solr's native faceting, serialized generically by SearchJsonRenderer - a future category facet only needs TypoScript. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,9 @@ use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
* "numFound": total hits,
|
||||
* "totalPages": ceil(numFound / resultsPerPage),
|
||||
* "results": [ { "title", "url", "type", "teaser" } ],
|
||||
* "filter": the active type filter or null,
|
||||
* "facets": { "type": [ { "value", "count", "active" } ] } -
|
||||
* counts stay complete while a filter is active,
|
||||
* "suggestions": spellcheck alternatives ("did you mean"), [] if none
|
||||
* }
|
||||
*
|
||||
@@ -35,6 +38,8 @@ use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
* q = search terms; without it the empty shape above is returned
|
||||
* (numFound 0) so the frontend always sees the same structure
|
||||
* page = 1-based page number, optional
|
||||
* filter = restrict results to one type from the `type` vocabulary
|
||||
* (page|product|market|story|news|download); unknown values ignored
|
||||
* The EXT:solr namespace (tx_solr[q], tx_solr[page]) is accepted as a
|
||||
* fallback so classic solr URLs keep working.
|
||||
*
|
||||
@@ -132,13 +137,21 @@ class SearchJsonRenderer
|
||||
// excluded from cHash - cached variants would collide (config.no_cache
|
||||
// is gone in TYPO3 v14, so the cache is disabled per request here).
|
||||
$request->getAttribute('frontend.cache.instruction')
|
||||
?->disableCache('vitec search: response varies by q/page query parameters');
|
||||
?->disableCache('vitec search: response varies by q/page/filter query parameters');
|
||||
|
||||
$params = $request->getQueryParams();
|
||||
$solrNamespace = (array)($params['tx_solr'] ?? []);
|
||||
$query = trim((string)($params['q'] ?? $solrNamespace['q'] ?? ''));
|
||||
$page = max(1, (int)($params['page'] ?? $solrNamespace['page'] ?? 1));
|
||||
|
||||
// Optional type filter, friendly vocabulary (see TYPE_LABELS).
|
||||
// Unknown values are ignored, never an error.
|
||||
$activeType = trim((string)($params['filter'] ?? ''));
|
||||
$typeFilterField = array_search($activeType, self::TYPE_LABELS, true);
|
||||
if ($typeFilterField === false) {
|
||||
$activeType = '';
|
||||
}
|
||||
|
||||
if ($query === '') {
|
||||
return (string)json_encode($this->emptyResult());
|
||||
}
|
||||
@@ -163,8 +176,12 @@ class SearchJsonRenderer
|
||||
$typoScriptConfiguration,
|
||||
$search,
|
||||
);
|
||||
$arguments = ['q' => $query, 'page' => $page];
|
||||
if ($activeType !== '') {
|
||||
$arguments['filter'] = ['type:' . $typeFilterField];
|
||||
}
|
||||
$searchRequest = GeneralUtility::makeInstance(SearchRequestBuilder::class, $typoScriptConfiguration)
|
||||
->buildForSearch(['q' => $query, 'page' => $page], $pageId, $languageId);
|
||||
->buildForSearch($arguments, $pageId, $languageId);
|
||||
|
||||
$resultSet = $searchService->search($searchRequest);
|
||||
|
||||
@@ -205,6 +222,29 @@ class SearchJsonRenderer
|
||||
// spellchecking disabled or unavailable - not essential
|
||||
}
|
||||
|
||||
$facets = [];
|
||||
try {
|
||||
foreach ($resultSet->getFacets() as $facet) {
|
||||
$options = [];
|
||||
foreach ($facet->getOptions() as $option) {
|
||||
$value = (string)$option->getUriValue();
|
||||
if ($facet->getName() === 'type') {
|
||||
$value = self::TYPE_LABELS[$value] ?? $value;
|
||||
}
|
||||
$options[] = [
|
||||
'value' => $value,
|
||||
'count' => $option->getDocumentCount(),
|
||||
'active' => $option->getSelected(),
|
||||
];
|
||||
}
|
||||
if ($options !== []) {
|
||||
$facets[$facet->getName()] = $options;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// faceting disabled or unavailable - facets stay empty
|
||||
}
|
||||
|
||||
$numFound = $resultSet->getAllResultCount();
|
||||
$resultsPerPage = $resultSet->getUsedResultsPerPage() ?: count($results);
|
||||
|
||||
@@ -214,6 +254,8 @@ class SearchJsonRenderer
|
||||
'resultsPerPage' => $resultsPerPage,
|
||||
'numFound' => $numFound,
|
||||
'totalPages' => $resultsPerPage > 0 ? (int)ceil($numFound / $resultsPerPage) : 0,
|
||||
'filter' => $activeType !== '' ? $activeType : null,
|
||||
'facets' => (object)$facets,
|
||||
'results' => $results,
|
||||
'suggestions' => array_values(array_unique($suggestions)),
|
||||
]);
|
||||
@@ -233,6 +275,8 @@ class SearchJsonRenderer
|
||||
'resultsPerPage' => 0,
|
||||
'numFound' => 0,
|
||||
'totalPages' => 0,
|
||||
'filter' => null,
|
||||
'facets' => new \stdClass(),
|
||||
'results' => [],
|
||||
'suggestions' => [],
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user