diff --git a/Classes/Common/Indexer.php b/Classes/Common/Indexer.php index 582bb9f310..3f859fbf71 100644 --- a/Classes/Common/Indexer.php +++ b/Classes/Common/Indexer.php @@ -74,6 +74,13 @@ class Indexer */ protected static array $processedDocs = []; + /** + * @access protected + * @static + * @var array List of already extracted structure nodes for structure path + */ + protected static array $extractedStructurePathNodes = []; + /** * @access protected * @static @@ -319,6 +326,10 @@ protected static function processLogical(Document $document, array $logicalUnit) $solrDoc->setField('toplevel', $logicalUnit['id'] == $doc->toplevelId ? true : false); $solrDoc->setField('title', $metadata['title'][0], self::$fields['fieldboost']['title']); $solrDoc->setField('volume', $metadata['volume'][0], self::$fields['fieldboost']['volume']); + // extract structure path + self::$extractedStructurePathNodes[$logicalUnit['id']] = self::extractStructurePathNodes($doc->tableOfContents, $logicalUnit['id']); + $processedStructurePath = self::buildStructurePathData(self::$extractedStructurePathNodes[$logicalUnit['id']], $document->getCurrentDocument()->toplevelId); + $solrDoc->setField('structure_path', json_encode($processedStructurePath, JSON_UNESCAPED_UNICODE)); // verify date formatting if(strtotime($metadata['date'][0])) { $solrDoc->setField('date', self::getFormattedDate($metadata['date'][0])); @@ -404,6 +415,21 @@ protected static function processPhysical(Document $document, int $page, array $ $solrDoc->setField('type', $physicalUnit['type'], self::$fields['fieldboost']['type']); $solrDoc->setField('collection', $doc->metadataArray[$doc->toplevelId]['collection']); $solrDoc->setField('location', $document->getLocation()); + // pick only the deepest structure paths + $associatedPaths = []; + foreach ($doc->smLinks['p2l'][$physicalUnit['id']] as $logicalId) { + $path = self::$extractedStructurePathNodes[$logicalId] ?? []; + if (!empty($path)) { + $associatedPaths[$logicalId] = $path; + } + } + $deepestPaths = self::filterDeepestStructurePaths($associatedPaths); + $processedStructurePath = []; + foreach ($deepestPaths as $path) { + $segments = self::buildStructurePathData($path, $document->getCurrentDocument()->toplevelId); + $processedStructurePath[] = json_encode($segments, JSON_UNESCAPED_UNICODE); + } + $solrDoc->setField('structure_path', $processedStructurePath); $solrDoc->setField('fulltext', $fullText); if (is_array($doc->metadataArray[$doc->toplevelId])) { @@ -639,6 +665,147 @@ private static function removeAppendsFromAuthor($authors) return $authors; } + /** + * Extract nodes alongside the structure map in direct line to the target id and return them as flattened array. + * + * @access private + * + * @static + * + * @param array $nodes Tree or Sub-Tree, where the target id should be extracted from if present + * @param string $targetId The ID of the logical structure element to be found + * @param array $path An intermediate array that keeps track of the current branch that is being looked up + * + * @return array + */ + private static function extractStructurePathNodes(array $nodes, string $targetId, array $path = []): array + { + foreach ($nodes as $node) { + // remember where we came from + $currentPath = array_merge($path, [$node]); + if ($node['id'] == $targetId) { + return $currentPath; + } + if (!empty($node['children'])) { + $result = self::extractStructurePathNodes($node['children'], $targetId, $currentPath); + if ($result) { + return $result; + } + } + } + return []; + } + + /** + * Filters those structure path nodes that are the descending into the structure tree the most and removes any that resemble a "prefix" of another. + * + * @access private + * + * @static + * + * @param array $paths The array containing all structure path nodes associated with a physical page + * + * @return array + */ + private static function filterDeepestStructurePaths(array $paths): array + { + if (count($paths) <= 1) { + return $paths; + } + + $deepestPath = []; + foreach ($paths as $currentLogicalId => $currentPath) { + $currentIds = array_column($currentPath, 'id'); + $isPrefix = false; + + foreach ($paths as $comparisonLogicalId => $comparisonPath) { + if ($currentLogicalId === $comparisonLogicalId) { + continue; + } + $comparisonIds = array_column($comparisonPath, 'id'); + // check if structure path is part/prefix of another structure path + if ( + count($currentIds) < count($comparisonIds) + && array_slice($comparisonIds, 0, count($currentIds)) === $currentIds + ) { + $isPrefix = true; + break; + } + } + + if (!$isPrefix) { + $deepestPath[$currentLogicalId] = $currentPath; + } + } + return $deepestPath; + } + + /** + * Create the actual array with the required data for the structure path that will be JSON encoded and indexed. + * + * @access private + * + * @static + * + * @param array $path The structure path nodes that shall be processed + * @param string $cutoffId The logical id at which ancestors and itself will not be part of the structure path data + * + * @return array + */ + private static function buildStructurePathData(array $path, string $cutoffId): array + { + $cutoffIndex = array_search($cutoffId, array_column($path, 'id')); + if ($cutoffIndex !== false) { + $path = array_slice($path, $cutoffIndex + 1); + } + + $segments = []; + foreach ($path as $node) { + $segments[] = self::buildStructurePathSegments($node); + } + return $segments; + } + + /** + * Gets the label or type of a structure path node with corresponding tag + * + * @access private + * + * @static + * + * @param array $node The current node that should be processed + * + * @return array + */ + private static function buildStructurePathSegments(array $node): array + { + if (!empty($node['label'])) { + return [ + 'label' => $node['label'], + ]; + } + if (!empty($node['orderlabel'])) { + return [ + 'label' => $node['orderlabel'], + ]; + } + if (!empty($node['volume'])) { + $value = !empty($node['year']) + ? $node['volume'] . ' ' . $node['year'] + : $node['volume']; + + return [ + 'label' => $value, + ]; + } + if (!empty($node['type'])) { + return [ + 'type' => $node['type'], + ]; + } + return ['label' => '']; + } + /** * Handle exception. * diff --git a/Classes/Common/Solr/SearchResult/ResultDocument.php b/Classes/Common/Solr/SearchResult/ResultDocument.php index 764070aef1..ddbc8e6c47 100644 --- a/Classes/Common/Solr/SearchResult/ResultDocument.php +++ b/Classes/Common/Solr/SearchResult/ResultDocument.php @@ -73,6 +73,12 @@ class ResultDocument */ private ?string $type; + /** + * @access private + * @var array The JSON encoded structure path(s) + */ + private array $structurePath = []; + /** * @access private * @var Page[] All pages in which search phrase was found @@ -117,6 +123,7 @@ public function __construct(Document $record, array $highlighting, array $fields $this->title = $record[$fields['title']]; $this->toplevel = $record[$fields['toplevel']] ?? false; $this->type = $record[$fields['type']]; + $this->structurePath = $record[$fields['structure_path']] ?? []; if (!empty($highlighting[$this->id])) { $highlightingForRecord = $highlighting[$this->id][$fields['fulltext']]; @@ -225,6 +232,18 @@ public function getType(): ?string return $this->type; } + /** + * Get the structure path(s) + * + * @access public + * + * @return array + */ + public function getStructurePath(): array + { + return $this->structurePath; + } + /** * Get all result's pages which contain search phrase. * diff --git a/Classes/Common/Solr/Solr.php b/Classes/Common/Solr/Solr.php index c6eea82e45..9786b7b58f 100644 --- a/Classes/Common/Solr/Solr.php +++ b/Classes/Common/Solr/Solr.php @@ -256,6 +256,7 @@ public static function getFields(): array self::$fields['type'] = $solrFields['type']; self::$fields['title'] = $solrFields['title']; self::$fields['volume'] = $solrFields['volume']; + self::$fields['structure_path'] = $solrFields['structurePath']; self::$fields['date'] = $solrFields['date']; self::$fields['thumbnail'] = $solrFields['thumbnail']; self::$fields['default'] = $solrFields['default']; diff --git a/Classes/Common/Solr/SolrSearch.php b/Classes/Common/Solr/SolrSearch.php index a21cc3a8b1..829b3cecd0 100644 --- a/Classes/Common/Solr/SolrSearch.php +++ b/Classes/Common/Solr/SolrSearch.php @@ -443,7 +443,7 @@ public function prepare() $params['listMetadataRecords'] = []; // Restrict the fields to the required ones. - $params['fields'] = 'uid,id,page,title,thumbnail,partof,toplevel,type'; + $params['fields'] = 'uid,id,page,title,thumbnail,partof,toplevel,type,structure_path'; if ($this->listedMetadata) { foreach ($this->listedMetadata as $metadata) { @@ -525,6 +525,31 @@ public function submit($start, $rows, $processResults = true) $searchResult['page'] = $doc['page']; $searchResult['thumbnail'] = $doc['thumbnail']; $searchResult['structure'] = $doc['type']; + // create string(s) from structure path(s) + $encodedStructurePaths = $doc['structure_path'] ?? []; + if (!is_array($encodedStructurePaths)) { + $encodedStructurePaths = [$encodedStructurePaths]; + } + $structurePathStrings = []; + foreach ($encodedStructurePaths as $jsonString) { + if (!is_string($jsonString) || $jsonString === '') { + continue; + } + $segments = json_decode($jsonString, true); + if ($segments === null && json_last_error() !== JSON_ERROR_NONE) { + continue; + } + $structurePathLabels = []; + foreach ($segments as $currentSegment) { + if (isset($currentSegment['type'])) { + $structurePathLabels[] = Helper::translate($currentSegment['type'], 'tx_dlf_structures', $this->settings['storagePid']); + } elseif (!empty($currentSegment['label'])) { + $structurePathLabels[] = $currentSegment['label']; + } + } + $structurePathStrings[] = implode(' → ', $structurePathLabels); + } + $searchResult['structure_path'] = $structurePathStrings; $searchResult['title'] = $doc['title']; foreach ($params['listMetadataRecords'] as $indexName => $solrField) { if (isset($doc['metadata'][$indexName])) { @@ -826,6 +851,7 @@ private function getDocument(Document $record, array $highlighting, array $field 'title' => $resultDocument->getTitle(), 'toplevel' => $resultDocument->getToplevel(), 'type' => $resultDocument->getType(), + 'structure_path' => $resultDocument->getStructurePath(), 'uid' => !empty($resultDocument->getUid()) ? $resultDocument->getUid() : $parameters['uid'], 'highlight' => $resultDocument->getHighlightsIds(), ]; diff --git a/Configuration/FlexForms/ListView.xml b/Configuration/FlexForms/ListView.xml index 69439b9c5c..2c8783d41d 100644 --- a/Configuration/FlexForms/ListView.xml +++ b/Configuration/FlexForms/ListView.xml @@ -76,6 +76,14 @@ + + 1 + + + check + 0 + + reload diff --git a/Resources/Private/Language/de.locallang_be.xlf b/Resources/Private/Language/de.locallang_be.xlf index 3923d04225..82c91e6b7c 100644 --- a/Resources/Private/Language/de.locallang_be.xlf +++ b/Resources/Private/Language/de.locallang_be.xlf @@ -14,6 +14,10 @@ + + + + diff --git a/Resources/Private/Language/de.locallang_labels.xlf b/Resources/Private/Language/de.locallang_labels.xlf index b9101521ec..b3fc37796e 100644 --- a/Resources/Private/Language/de.locallang_labels.xlf +++ b/Resources/Private/Language/de.locallang_labels.xlf @@ -776,6 +776,10 @@ Solr-Schema-Feld "volume" : Volume field is mandatory for identifying documents (Standard ist "volume") Solr Schema Field "volume" : Volume field is mandatory for identifying documents (default is "volume") + + + Solr-Schema-Feld "structure_path" : Field providing context about the location of a resource in the structure map (Standard ist "structure_path") + Solr Schema Field "structure_path" : Field providing context about the location of a resource in the structure map (default is "structure_path") Solr Schema Field "date" : The date a resource was issued or created. Used for datesearch (Standard ist "date") diff --git a/Resources/Private/Language/de.locallang_metadata.xlf b/Resources/Private/Language/de.locallang_metadata.xlf index 84af587407..a49a040bf7 100644 --- a/Resources/Private/Language/de.locallang_metadata.xlf +++ b/Resources/Private/Language/de.locallang_metadata.xlf @@ -89,6 +89,10 @@ + + + + diff --git a/Resources/Private/Language/locallang_be.xlf b/Resources/Private/Language/locallang_be.xlf index ab3c0d3985..9a0d80ae45 100644 --- a/Resources/Private/Language/locallang_be.xlf +++ b/Resources/Private/Language/locallang_be.xlf @@ -14,6 +14,9 @@ + + + diff --git a/Resources/Private/Language/locallang_labels.xlf b/Resources/Private/Language/locallang_labels.xlf index b36e6b23bd..e4c4def65f 100644 --- a/Resources/Private/Language/locallang_labels.xlf +++ b/Resources/Private/Language/locallang_labels.xlf @@ -587,6 +587,9 @@ Solr Schema Field "volume" : Volume field is mandatory for identifying documents (default is "volume") + + Solr Schema Field "structure_path" : Field providing context about the location of a resource in the structure map (default is "structure_path") + Solr Schema Field "date" : The date a resource was issued or created. Used for datesearch (default is "date") diff --git a/Resources/Private/Language/locallang_metadata.xlf b/Resources/Private/Language/locallang_metadata.xlf index 70034a09df..02165d5fa6 100644 --- a/Resources/Private/Language/locallang_metadata.xlf +++ b/Resources/Private/Language/locallang_metadata.xlf @@ -68,6 +68,9 @@ + + + diff --git a/ext_conf_template.txt b/ext_conf_template.txt index b2e51147a8..559d6ee498 100644 --- a/ext_conf_template.txt +++ b/ext_conf_template.txt @@ -62,6 +62,8 @@ solr.fields.partof = partof solr.fields.root = root # cat=Solr; type=string; label=LLL:EXT:dlf/Resources/Private/Language/locallang_labels.xlf:config.solr.fields.sid solr.fields.sid = sid +# cat=Solr; type=string; label=LLL:EXT:dlf/Resources/Private/Language/locallang_labels.xlf:config.solr.fields.structurePath +solr.fields.structurePath = structure_path # cat=Solr; type=string; label=LLL:EXT:dlf/Resources/Private/Language/locallang_labels.xlf:config.solr.fields.toplevel solr.fields.toplevel = toplevel # cat=Solr; type=string; label=LLL:EXT:dlf/Resources/Private/Language/locallang_labels.xlf:config.solr.fields.type