Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 167 additions & 1 deletion Classes/Common/Indexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,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
Expand Down Expand Up @@ -371,6 +378,10 @@ protected static function processLogical(Document $document, array $logicalUnit)
$solrDoc->setField('toplevel', $logicalUnit['id'] == $doc->getToplevelId());
$solrDoc->setField('title', $metadata['title'][0]);
$solrDoc->setField('volume', $metadata['volume'][0] ?? '');
// extract structure path
self::$extractedStructurePathNodes[$logicalUnit['id']] = self::extractStructurePathNodes($doc->tableOfContents, $logicalUnit['id']);
$processedStructurePath = self::buildStructurePathData(self::$extractedStructurePathNodes[$logicalUnit['id']], $document->getCurrentDocument()->getToplevelId());
$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]));
Expand Down Expand Up @@ -465,7 +476,21 @@ protected static function processPhysical(Document $document, int $page, array $
$solrDoc->setField('type', $physicalUnit['type']);
$solrDoc->setField('collection', $doc->metadataArray[$doc->getToplevelId()]['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()->getToplevelId());
$processedStructurePath[] = json_encode($segments, JSON_UNESCAPED_UNICODE);
}
$solrDoc->setField('structure_path', $processedStructurePath);
$solrDoc->setField('fulltext', $fullText);
if (is_array($doc->metadataArray[$doc->getToplevelId()])) {
self::addFaceting($doc, $solrDoc, $physicalUnit);
Expand Down Expand Up @@ -728,6 +753,147 @@ private static function removeAppendsFromAuthor(array|string $authors): array|st
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.
*
Expand Down
19 changes: 19 additions & 0 deletions Classes/Common/Solr/SearchResult/ResultDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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']];
Expand Down Expand Up @@ -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.
*
Expand Down
1 change: 1 addition & 0 deletions Classes/Common/Solr/Solr.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Searches now fail with an Oops unless the complete Solr index is rebuilt. I fixed it locally with self::$fields['structure_path'] = $solrFields['structurePath'] ?? '';.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the report. I opened a new issue: #1851

@michaelkubina michaelkubina Feb 24, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dear Stefan,
do you mind checking if the field is actually registered in your LocalConfiguration.php or settings.php respectivly? I believe it isnt. Alternativly you can inspect the "Extension-Configuration" - it should be visible in the "solr"-Tab. Or you manually add 'structurePath' => 'structure_path', to dlf.solr.fields.

grafik

When there is no such field registered, it should appear after "Maintenence" -> "Flush Cache", as i tried to describe in the migration path to 7.x . Florian did it this way and his configuration was updated.

Searches fail, because in this case it tries to access a field, that has not been applied to your configuration yet. This would have happened to any other field as well, when its not in the configuration.

self::$fields['date'] = $solrFields['date'] ?? null;
self::$fields['thumbnail'] = $solrFields['thumbnail'];
self::$fields['default'] = $solrFields['default'];
Expand Down
28 changes: 27 additions & 1 deletion Classes/Common/Solr/SolrSearch.php
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,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) {
Expand Down Expand Up @@ -560,6 +560,31 @@ public function submit(int $start, int $rows, bool $processResults = true): void
$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])) {
Expand Down Expand Up @@ -901,6 +926,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(),
];
Expand Down
2 changes: 2 additions & 0 deletions Configuration/ApacheSolr/configsets/dlf/conf/schema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ limitations under the License.
<!-- Next two fields are mandatory for identifying documents. -->
<field name="title" type="standard" indexed="true" stored="true" multiValued="false" default="" />
<field name="volume" type="standard" indexed="true" stored="true" multiValued="false" default="" />
<!-- Convenience field to provide context about the path within the logical structure map -->
<field name="structure_path" type="string" indexed="false" stored="true" multiValued="true" default="" />
<!-- The keydate of a resource e.g a newspaper was issued or a letter was written -->
<field name="date" type="daterange" indexed="true" stored="true" multiValued="false" />
<!-- URL of thumbnail image for the document. -->
Expand Down
8 changes: 8 additions & 0 deletions Configuration/FlexForms/ListView.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@
<default>0</default>
</config>
</settings.getTitle>
<settings.getStructurePath>
<exclude>1</exclude>
<label>LLL:EXT:dlf/Resources/Private/Language/locallang_be.xlf:flexform.getStructurePath</label>
<config>
<type>check</type>
<default>0</default>
</config>
</settings.getStructurePath>
<settings.basketButton>
<onChange>reload</onChange>
<exclude>1</exclude>
Expand Down
4 changes: 4 additions & 0 deletions Resources/Private/Language/de.locallang_be.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
<source><![CDATA[Show only documents from the selected collection(s)]]></source>
<target><![CDATA[Nur Dokumente der ausgewählten Kollektion(en) berücksichtigen]]></target>
</trans-unit>
<trans-unit id="flexform.getStructurePath" approved="yes">
<source><![CDATA[Show breadcrumb/path to result location within the structure map]]></source>
<target><![CDATA[Breadcrumb/Pfad des Treffers innerhalb des Strukturbaums anzeigen]]></target>
</trans-unit>
<trans-unit id="flexform.getTitle" approved="yes">
<source><![CDATA[Show title of parent document if document has no title itself]]></source>
<target><![CDATA[Bei Bedarf Titel des übergeordneten Dokuments anzeigen]]></target>
Expand Down
4 changes: 4 additions & 0 deletions Resources/Private/Language/de.locallang_labels.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,10 @@
<target>Solr-Schema-Feld "volume" : Volume field is mandatory for identifying documents (Standard ist "volume")</target>
<source>Solr Schema Field "volume" : Volume field is mandatory for identifying documents (default is "volume")</source>
</trans-unit>
<trans-unit id="config.solr.fields.structurePath">
<target>Solr-Schema-Feld "structure_path" : Field providing context about the location of a resource in the structure map (Standard ist "structure_path")</target>
<source>Solr Schema Field "structure_path" : Field providing context about the location of a resource in the structure map (default is "structure_path")</source>
</trans-unit>
<trans-unit id="config.solr.fields.date">
<target>Solr Schema Field "date" : The date a resource was issued or created. Used for datesearch (Standard ist "date")</target>
<source>Solr Schema Field "date" : The date a resource was issued or created. Used for datesearch (default is "date")</source>
Expand Down
4 changes: 4 additions & 0 deletions Resources/Private/Language/de.locallang_metadata.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@
<source><![CDATA[Shelfmark]]></source>
<target><![CDATA[Signatur]]></target>
</trans-unit>
<trans-unit id="metadata.structure_path" approved="yes">
<source><![CDATA[Structure Path]]></source>
<target><![CDATA[Strukturpfad]]></target>
</trans-unit>
<trans-unit id="metadata.terms" approved="yes">
<source><![CDATA[Terms of Use]]></source>
<target><![CDATA[Nutzungsbedingungen]]></target>
Expand Down
9 changes: 6 additions & 3 deletions Resources/Private/Language/locallang_be.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
<trans-unit id="flexform.excludeOtherCollections">
<source><![CDATA[Show only documents from the selected collection(s)]]></source>
</trans-unit>
<trans-unit id="flexform.getStructurePath">
<source><![CDATA[Show breadcrumb/path to result location within the structure map]]></source>
</trans-unit>
<trans-unit id="flexform.getTitle">
<source><![CDATA[Show title of parent document if document has no title itself]]></source>
</trans-unit>
<trans-unit id="flexform.library">
<source><![CDATA[Providing library]]></source>
</trans-unit>
Expand Down Expand Up @@ -356,9 +362,6 @@
<trans-unit id="plugins.listview.flexform.limit">
<source><![CDATA[Documents per page]]></source>
</trans-unit>
<trans-unit id="flexform.getTitle">
<source><![CDATA[Show title of parent document if document has no title itself]]></source>
</trans-unit>
<trans-unit id="plugins.collection.title">
<source><![CDATA[Kitodo: Collection]]></source>
</trans-unit>
Expand Down
3 changes: 3 additions & 0 deletions Resources/Private/Language/locallang_labels.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,9 @@
<trans-unit id="config.solr.fields.volume">
<source>Solr Schema Field "volume" : Volume field is mandatory for identifying documents (default is "volume")</source>
</trans-unit>
<trans-unit id="config.solr.fields.structurePath">
<source>Solr Schema Field "structure_path" : Field providing context about the location of a resource in the structure map (default is "structure_path")</source>
</trans-unit>
<trans-unit id="config.solr.fields.date">
<source>Solr Schema Field "date" : The date a resource was issued or created. Used for datesearch (default is "date")</source>
</trans-unit>
Expand Down
Loading
Loading