Index PDF and Office file contents via Apache Tika
Solr 10 dropped the ExtractingRequestHandler, so a dedicated Tika
container on the Solr VPS (Caddy route /tika/*, own basic-auth
credential; Tika itself has no auth) extracts file text during
indexing: TikaDownloadContentIndexer listens on
BeforeDocumentIsProcessedForIndexingEvent, resolves the file through
the existing DownloadFileResolver and appends the text (mime
whitelist, 30 MB cap, 100k chars, fail-soft) to the document's content
field. Extractions are cached in var/tika-cache keyed on
path+size+mtime - a full re-index of 177 downloads drops from 2:12 min
to 27 s, replacing a file re-extracts naturally. Datasheet
specifications ("625i", "genlock") are now searchable. Spec v1.15.
This commit is contained in:
1 parent
5bb4e374b4
commit
8c881d153c
9 files changed
+499
-6
No files matched your search
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\UserFunc;
|
||||
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
|
||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
|
||||
/**
|
||||
* Data source for the product filter: the product category hierarchy, and
|
||||
* nothing else.
|
||||
*
|
||||
* The editor places ONE `vitec_productfinder` element above the product
|
||||
* lists on the products page. It renders no product cards - the React front
|
||||
* end builds the filter selects from this payload and filters the products
|
||||
* the `vitec_productlist` elements already delivered, client side, via URL
|
||||
* parameters (?cat=58&subcat=66). No change to the list plugin was needed.
|
||||
*
|
||||
* Emitted shape:
|
||||
*
|
||||
* content.productfinder.categories[] = { uid, title, subcategories[] }
|
||||
* subcategories[] = { uid, title }
|
||||
*
|
||||
* Level 1 below the configured root becomes the main categories, their
|
||||
* direct children the subcategories - the same tree the product records
|
||||
* hang on, so a filter value maps straight onto `product.categories[].uid`.
|
||||
*
|
||||
* TYPO3 v14 hands the ContentObjectRenderer in through the setter; without
|
||||
* it $this->cObj stays null and the element cannot read its own FlexForm.
|
||||
*/
|
||||
final class ProductFinderJsonRenderer
|
||||
{
|
||||
private const CATEGORY_TABLE = 'sys_category';
|
||||
|
||||
/** Fallback when no root is configured: the category tree the products use. */
|
||||
private const DEFAULT_ROOT_TITLE = 'Product';
|
||||
|
||||
protected ?ContentObjectRenderer $cObj = null;
|
||||
|
||||
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
|
||||
{
|
||||
$this->cObj = $cObj;
|
||||
}
|
||||
|
||||
#[AsAllowedCallable]
|
||||
public function render(string $content, array $conf): string
|
||||
{
|
||||
try {
|
||||
$settings = $this->settings();
|
||||
|
||||
$root = (int)($settings['categoryRoot'] ?? 0);
|
||||
if ($root <= 0) {
|
||||
$root = $this->rootByTitle(self::DEFAULT_ROOT_TITLE);
|
||||
}
|
||||
if ($root <= 0) {
|
||||
return (string)json_encode(['categories' => []]);
|
||||
}
|
||||
|
||||
$order = (string)($settings['sorting'] ?? 'sorting');
|
||||
$categories = [];
|
||||
foreach ($this->childrenOf($root, $order) as $main) {
|
||||
$categories[] = [
|
||||
'uid' => (int)$main['uid'],
|
||||
'title' => (string)$main['title'],
|
||||
'subcategories' => array_map(
|
||||
static fn(array $sub): array => [
|
||||
'uid' => (int)$sub['uid'],
|
||||
'title' => (string)$sub['title'],
|
||||
],
|
||||
$this->childrenOf((int)$main['uid'], $order)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return (string)json_encode(['categories' => $categories], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
} catch (\Throwable) {
|
||||
// A broken filter must never take the page payload down with it.
|
||||
return (string)json_encode(['categories' => []]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FlexForm settings of the element being rendered.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function settings(): array
|
||||
{
|
||||
$flexform = (string)($this->cObj->data['pi_flexform'] ?? '');
|
||||
if ($flexform === '') {
|
||||
return [];
|
||||
}
|
||||
$parsed = GeneralUtility::makeInstance(FlexFormService::class)
|
||||
->convertFlexFormContentToArray($flexform);
|
||||
|
||||
return is_array($parsed['settings'] ?? null) ? $parsed['settings'] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct children of a category, honouring the frontend restrictions
|
||||
* (hidden, start/end time, deleted).
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private function childrenOf(int $parent, string $order): array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable(self::CATEGORY_TABLE);
|
||||
$queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
|
||||
|
||||
$query = $queryBuilder->select('uid', 'title', 'sorting')
|
||||
->from(self::CATEGORY_TABLE)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('parent', $queryBuilder->createNamedParameter($parent, ParameterType::INTEGER)),
|
||||
$queryBuilder->expr()->in('sys_language_uid', [-1, 0])
|
||||
);
|
||||
|
||||
match ($order) {
|
||||
'title' => $query->orderBy('title', 'ASC'),
|
||||
'uid' => $query->orderBy('uid', 'ASC'),
|
||||
default => $query->orderBy('sorting', 'ASC')->addOrderBy('title', 'ASC'),
|
||||
};
|
||||
|
||||
return $query->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/** Uid of a root-level category by title, 0 when it does not exist. */
|
||||
private function rootByTitle(string $title): int
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable(self::CATEGORY_TABLE);
|
||||
$queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
|
||||
|
||||
$uid = $queryBuilder->select('uid')->from(self::CATEGORY_TABLE)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('parent', 0),
|
||||
$queryBuilder->expr()->eq('title', $queryBuilder->createNamedParameter($title))
|
||||
)
|
||||
->setMaxResults(1)
|
||||
->executeQuery()->fetchOne();
|
||||
|
||||
return is_numeric($uid) ? (int)$uid : 0;
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@ class SearchJsonRenderer
|
||||
'pages' => 'page',
|
||||
'tx_vitec_domain_model_product' => 'product',
|
||||
'tx_vitec_domain_model_market' => 'market',
|
||||
'tx_vitec_domain_model_solution' => 'solution',
|
||||
'tx_vitec_domain_model_usecase' => 'story',
|
||||
'tx_news_domain_model_news' => 'news',
|
||||
'tx_vitec_domain_model_download' => 'download',
|
||||
@@ -152,6 +153,15 @@ class SearchJsonRenderer
|
||||
$activeType = '';
|
||||
}
|
||||
|
||||
// Secondary facet filters: value comes verbatim from facets.<name>[].value
|
||||
$facetFilters = [];
|
||||
foreach (['market', 'category'] as $facetParam) {
|
||||
$facetValue = trim((string)($params[$facetParam] ?? ''));
|
||||
if ($facetValue !== '') {
|
||||
$facetFilters[$facetParam] = $facetValue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($query === '') {
|
||||
return (string)json_encode($this->emptyResult());
|
||||
}
|
||||
@@ -177,8 +187,15 @@ class SearchJsonRenderer
|
||||
$search,
|
||||
);
|
||||
$arguments = ['q' => $query, 'page' => $page];
|
||||
$filterArguments = [];
|
||||
if ($activeType !== '') {
|
||||
$arguments['filter'] = ['type:' . $typeFilterField];
|
||||
$filterArguments[] = 'type:' . $typeFilterField;
|
||||
}
|
||||
foreach ($facetFilters as $facetName => $facetValue) {
|
||||
$filterArguments[] = $facetName . ':' . $facetValue;
|
||||
}
|
||||
if ($filterArguments !== []) {
|
||||
$arguments['filter'] = $filterArguments;
|
||||
}
|
||||
$searchRequest = GeneralUtility::makeInstance(SearchRequestBuilder::class, $typoScriptConfiguration)
|
||||
->buildForSearch($arguments, $pageId, $languageId);
|
||||
@@ -255,6 +272,7 @@ class SearchJsonRenderer
|
||||
'numFound' => $numFound,
|
||||
'totalPages' => $resultsPerPage > 0 ? (int)ceil($numFound / $resultsPerPage) : 0,
|
||||
'filter' => $activeType !== '' ? $activeType : null,
|
||||
'activeFacets' => (object)$facetFilters,
|
||||
'facets' => (object)$facets,
|
||||
'results' => $results,
|
||||
'suggestions' => array_values(array_unique($suggestions)),
|
||||
@@ -276,6 +294,7 @@ class SearchJsonRenderer
|
||||
'numFound' => 0,
|
||||
'totalPages' => 0,
|
||||
'filter' => null,
|
||||
'activeFacets' => new stdClass(),
|
||||
'facets' => new \stdClass(),
|
||||
'results' => [],
|
||||
'suggestions' => [],
|
||||
|
||||
Reference in new issue
Block a user