diff --git a/bundle/DependencyInjection/Configuration.php b/bundle/DependencyInjection/Configuration.php index ce69b341..d369b9f3 100644 --- a/bundle/DependencyInjection/Configuration.php +++ b/bundle/DependencyInjection/Configuration.php @@ -25,6 +25,8 @@ public function getConfigTreeBuilder(): TreeBuilder $this->addIndexableFieldTypeSection($rootNode); $this->addSearchResultExtractorSection($rootNode); $this->addAsynchronousIndexingSection($rootNode); + $this->addParentChildIndexingSection($rootNode); + return $treeBuilder; } @@ -73,4 +75,15 @@ private function addAsynchronousIndexingSection(ArrayNodeDefinition $nodeDefinit ->end() ->end(); } + + private function addParentChildIndexingSection(ArrayNodeDefinition $nodeDefinition): void + { + $nodeDefinition + ->children() + ->booleanNode('use_parent_child_indexing') + ->info('Use parent child indexing') + ->defaultFalse() + ->end() + ->end(); + } } diff --git a/lib/Container/Compiler/AsynchronousIndexingPass.php b/lib/Container/Compiler/AsynchronousIndexingPass.php index 87852388..118d7c10 100644 --- a/lib/Container/Compiler/AsynchronousIndexingPass.php +++ b/lib/Container/Compiler/AsynchronousIndexingPass.php @@ -37,6 +37,7 @@ public function process(ContainerBuilder $container): void ->setDecoratedService(CoreContentEventSubscriber::class) ->setArguments([ new Reference('netgen.ibexa_search_extra.asynchronous_indexing.messenger.bus'), + new Reference('ibexa.api.service.location'), ]); $container diff --git a/lib/Core/Search/Common/EventSubscriber/ContentEventSubscriber.php b/lib/Core/Search/Common/EventSubscriber/ContentEventSubscriber.php index 418ad801..6676b24a 100644 --- a/lib/Core/Search/Common/EventSubscriber/ContentEventSubscriber.php +++ b/lib/Core/Search/Common/EventSubscriber/ContentEventSubscriber.php @@ -12,6 +12,7 @@ use Ibexa\Contracts\Core\Repository\Events\Content\PublishVersionEvent; use Ibexa\Contracts\Core\Repository\Events\Content\RevealContentEvent; use Ibexa\Contracts\Core\Repository\Events\Content\UpdateContentMetadataEvent; +use Ibexa\Contracts\Core\Repository\LocationService; use Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\Message\Search\Content\CopyContent; use Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\Message\Search\Content\DeleteContent; use Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\Message\Search\Content\DeleteTranslation; @@ -25,8 +26,11 @@ class ContentEventSubscriber implements EventSubscriberInterface { + private array $contentParentLocations = []; public function __construct( private readonly MessageBusInterface $messageBus, + private readonly LocationService $locationService, + ) {} public static function getSubscribedEvents(): array @@ -55,8 +59,11 @@ public function onCopyContent(CopyContentEvent $event): void public function onBeforeDeleteContent(BeforeDeleteContentEvent $event): void { + $contentLocations = $this->locationService->loadLocations($event->getContentInfo()); try { - $event->getContentInfo()->getMainLocation()?->parentLocationId; + foreach ($contentLocations as $contentLocation){ + $this->contentParentLocations[] = $contentLocation->parentLocationId; + } } catch (Throwable) { // does nothing } @@ -64,17 +71,12 @@ public function onBeforeDeleteContent(BeforeDeleteContentEvent $event): void public function onDeleteContent(DeleteContentEvent $event): void { - try { - $mainLocationParentLocationId = $event->getContentInfo()->getMainLocation()?->parentLocationId; - } catch (Throwable) { - $mainLocationParentLocationId = null; - } - + $parentLocationIds = $this->contentParentLocations ?? []; $this->messageBus->dispatch( new DeleteContent( $event->getContentInfo()->id, $event->getLocations(), - $mainLocationParentLocationId, + $parentLocationIds, ), ); } diff --git a/lib/Core/Search/Common/Messenger/Message/Search/Content/DeleteContent.php b/lib/Core/Search/Common/Messenger/Message/Search/Content/DeleteContent.php index f2d990c0..aff1ee3b 100644 --- a/lib/Core/Search/Common/Messenger/Message/Search/Content/DeleteContent.php +++ b/lib/Core/Search/Common/Messenger/Message/Search/Content/DeleteContent.php @@ -12,6 +12,6 @@ final class DeleteContent public function __construct( public readonly int $contentId, public readonly array $locationIds, - public readonly ?int $mainLocationParentLocationId, + public readonly ?array $parentLocationIds, ) {} } diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/AncestorIndexer.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/AncestorIndexer.php new file mode 100644 index 00000000..b921294c --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/AncestorIndexer.php @@ -0,0 +1,82 @@ +ancestorResolver->resolveAncestor($location); + + + if ($ancestor === null) { + return; + } + + try { + $content = $this->contentHandler->load($ancestor->contentId); + } catch (NotFoundException) { + return; + } + + $this->searchHandler->indexContent($content); + $this->searchHandler->indexLocation($ancestor); + } + + /** + * @param \Ibexa\Contracts\Core\Persistence\Content\Location $location + */ + public function indexSingleForDeleteContent(Location $location): void + { + $ancestor = $this->ancestorResolver->resolveAncestorForDeleteContent($location); + + if ($ancestor === null) { + return; + } + + try { + $content = $this->contentHandler->load($ancestor->contentId); + } catch (NotFoundException) { + return; + } + + $this->searchHandler->indexContent($content); + $this->searchHandler->indexLocation($ancestor); + } + + /** + * @param \Ibexa\Contracts\Core\Persistence\Content\Location[] $locations + */ + public function indexMultiple(array $locations): void + { + foreach ($locations as $location) { + $this->indexSingle($location); + } + } + + /*** + * @param \Ibexa\Contracts\Core\Persistence\Content\Location[] $locations + */ + public function indexMultipleForDeleteContent(array $locations): void + { + $this->indexMultiple($locations); + + foreach ($locations as $location) { + $this->indexSingleForDeleteContent($location); + } + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/CopyContentHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/CopyContentHandler.php new file mode 100644 index 00000000..6be41995 --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/CopyContentHandler.php @@ -0,0 +1,26 @@ +ancestorIndexer->indexMultiple( + $this->locationHandler->loadLocationsByContent( + $message->contentId, + ), + ); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/DeleteContentHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/DeleteContentHandler.php new file mode 100644 index 00000000..9b22c413 --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/DeleteContentHandler.php @@ -0,0 +1,53 @@ +parentLocationIds === []) { + $this->logger->info( + sprintf( + '%s: Could not find main Location parent Location ID for deleted Content #%d, aborting', + $this::class, + $message->contentId, + ), + ); + + return; + } + $locations = []; + foreach ($message->parentLocationIds as $locationId) { + try { + $locations[] = $this->locationHandler->load($locationId); + } catch (NotFoundException) { + $this->logger->info( + sprintf( + '%s: Location #%d is gone, aborting', + $this::class, + $locationId, + ), + ); + } + } + $this->ancestorIndexer->indexMultipleForDeleteContent($locations); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/DeleteTranslationHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/DeleteTranslationHandler.php new file mode 100644 index 00000000..e8cfd393 --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/DeleteTranslationHandler.php @@ -0,0 +1,56 @@ +contentHandler->loadContentInfo( + $message->contentId, + ); + } catch (NotFoundException) { + $this->logger->info( + sprintf( + '%s: Content #%d is gone, aborting', + $this::class, + $message->contentId, + ), + ); + + return; + } + + if ($contentInfo->status !== ContentInfo::STATUS_PUBLISHED) { + return; + } + + $this->ancestorIndexer->indexMultiple( + $this->locationHandler->loadLocationsByContent( + $message->contentId, + ), + ); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/HideContentHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/HideContentHandler.php new file mode 100644 index 00000000..8b0233c4 --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/HideContentHandler.php @@ -0,0 +1,26 @@ +ancestorIndexer->indexMultiple( + $this->locationHandler->loadLocationsByContent( + $message->contentId, + ), + ); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/PublishVersionHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/PublishVersionHandler.php new file mode 100644 index 00000000..5a27392e --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/PublishVersionHandler.php @@ -0,0 +1,27 @@ +ancestorIndexer->indexMultiple( + $this->locationHandler->loadLocationsByContent( + $message->contentId, + ), + ); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/RevealContentHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/RevealContentHandler.php new file mode 100644 index 00000000..5c936f52 --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/RevealContentHandler.php @@ -0,0 +1,26 @@ +ancestorIndexer->indexMultiple( + $this->locationHandler->loadLocationsByContent( + $message->contentId, + ), + ); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/UpdateContentMetadataHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/UpdateContentMetadataHandler.php new file mode 100644 index 00000000..c854297c --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Content/UpdateContentMetadataHandler.php @@ -0,0 +1,67 @@ +contentHandler->loadContentInfo( + $message->contentId, + ); + } catch (NotFoundException) { + $this->logger->info( + sprintf( + '%s: Content #%d is gone, aborting', + $this::class, + $message->contentId, + ), + ); + + return; + } + + if ($contentInfo->status !== ContentInfo::STATUS_PUBLISHED) { + return; + } + + try { + $location = $this->locationHandler->load( + $contentInfo->mainLocationId, + ); + } catch (NotFoundException) { + $this->logger->info( + sprintf( + '%s: Location #%d is gone, aborting', + $this::class, + $contentInfo->mainLocationId, + ), + ); + + return; + } + + $this->ancestorIndexer->indexSingle($location); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/AssignSectionToSubtreeHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/AssignSectionToSubtreeHandler.php new file mode 100644 index 00000000..e9913bf2 --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/AssignSectionToSubtreeHandler.php @@ -0,0 +1,44 @@ +locationHandler->load( + $message->locationId, + ); + } catch (NotFoundException) { + $this->logger->info( + sprintf( + '%s: Location #%d is gone, aborting', + $this::class, + $message->locationId, + ), + ); + + return; + } + + $this->ancestorIndexer->indexSingle($location); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/CopySubtreeHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/CopySubtreeHandler.php new file mode 100644 index 00000000..fd4694ac --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/CopySubtreeHandler.php @@ -0,0 +1,44 @@ +locationHandler->load( + $message->locationId, + ); + } catch (NotFoundException) { + $this->logger->info( + sprintf( + '%s: Location #%d is gone, aborting', + $this::class, + $message->locationId, + ), + ); + + return; + } + + $this->ancestorIndexer->indexSingle($location); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/CreateLocationHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/CreateLocationHandler.php new file mode 100644 index 00000000..00e4f3f3 --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/CreateLocationHandler.php @@ -0,0 +1,44 @@ +locationHandler->load( + $message->locationId, + ); + } catch (NotFoundException) { + $this->logger->info( + sprintf( + '%s: Location #%d is gone, aborting', + $this::class, + $message->locationId, + ), + ); + + return; + } + + $this->ancestorIndexer->indexSingle($location); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/DeleteLocationHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/DeleteLocationHandler.php new file mode 100644 index 00000000..300fd175 --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/DeleteLocationHandler.php @@ -0,0 +1,44 @@ +locationHandler->load( + $message->parentLocationId, + ); + } catch (NotFoundException) { + $this->logger->info( + sprintf( + '%s: Location #%d is gone, aborting', + $this::class, + $message->parentLocationId, + ), + ); + + return; + } + + $this->ancestorIndexer->indexSingle($location); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/HideLocationHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/HideLocationHandler.php new file mode 100644 index 00000000..e4f08810 --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/HideLocationHandler.php @@ -0,0 +1,44 @@ +locationHandler->load( + $message->locationId, + ); + } catch (NotFoundException) { + $this->logger->info( + sprintf( + '%s: Location #%d is gone, aborting', + $this::class, + $message->locationId, + ), + ); + + return; + } + + $this->ancestorIndexer->indexSingle($location); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/MoveSubtreeHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/MoveSubtreeHandler.php new file mode 100644 index 00000000..d0081923 --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/MoveSubtreeHandler.php @@ -0,0 +1,58 @@ +locationHandler->load( + $message->locationId, + ); + + $this->ancestorIndexer->indexSingle($location); + } catch (NotFoundException) { + $this->logger->info( + sprintf( + '%s: Location #%d is gone, aborting', + $this::class, + $message->locationId, + ), + ); + } + + try { + $location = $this->locationHandler->load( + $message->oldParentLocationId, + ); + + $this->ancestorIndexer->indexSingle($location); + } catch (NotFoundException) { + $this->logger->info( + sprintf( + '%s: Old parent Location #%d is gone, aborting', + $this::class, + $message->locationId, + ), + ); + } + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/SwapLocationHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/SwapLocationHandler.php new file mode 100644 index 00000000..ae039193 --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/SwapLocationHandler.php @@ -0,0 +1,48 @@ +reindexForLocation($message->location1Id); + $this->reindexForLocation($message->location2Id); + } + + private function reindexForLocation(int $locationId): void + { + try { + $location = $this->locationHandler->load($locationId); + } catch (NotFoundException) { + $this->logger->info( + sprintf( + '%s: Location #%d is gone, aborting', + $this::class, + $locationId, + ), + ); + + return; + } + + $this->ancestorIndexer->indexSingle($location); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/UnhideLocationHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/UnhideLocationHandler.php new file mode 100644 index 00000000..69dc00c9 --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/UnhideLocationHandler.php @@ -0,0 +1,44 @@ +locationHandler->load( + $message->locationId, + ); + } catch (NotFoundException) { + $this->logger->info( + sprintf( + '%s: Location #%d is gone, aborting', + $this::class, + $message->locationId, + ), + ); + + return; + } + + $this->ancestorIndexer->indexSingle($location); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/UpdateLocationHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/UpdateLocationHandler.php new file mode 100644 index 00000000..92a76f98 --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Location/UpdateLocationHandler.php @@ -0,0 +1,42 @@ +locationHandler->load($message->locationId); + } catch (NotFoundException) { + $this->logger->info( + sprintf( + '%s: Location #%d is gone, aborting', + $this::class, + $message->locationId, + ), + ); + + return; + } + + $this->ancestorIndexer->indexSingle($location); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/ObjectState/SetContentStateHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/ObjectState/SetContentStateHandler.php new file mode 100644 index 00000000..bee8711b --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/ObjectState/SetContentStateHandler.php @@ -0,0 +1,26 @@ +ancestorIndexer->indexMultiple( + $this->locationHandler->loadLocationsByContent( + $message->contentId, + ), + ); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Section/AssignSectionHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Section/AssignSectionHandler.php new file mode 100644 index 00000000..df1ad48b --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Section/AssignSectionHandler.php @@ -0,0 +1,26 @@ +ancestorIndexer->indexMultiple( + $this->locationHandler->loadLocationsByContent( + $message->contentId, + ), + ); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Trash/RecoverHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Trash/RecoverHandler.php new file mode 100644 index 00000000..93f49e73 --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Trash/RecoverHandler.php @@ -0,0 +1,42 @@ +locationHandler->load($message->locationId); + } catch (NotFoundException) { + $this->logger->info( + sprintf( + '%s: Location #%d is gone, aborting', + $this::class, + $message->locationId, + ), + ); + + return; + } + + $this->ancestorIndexer->indexSingle($location); + } +} diff --git a/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Trash/TrashHandler.php b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Trash/TrashHandler.php new file mode 100644 index 00000000..c9ad8f92 --- /dev/null +++ b/lib/Core/Search/Common/Messenger/MessageHandler/Search/ParentChildIndexing/Trash/TrashHandler.php @@ -0,0 +1,44 @@ +locationHandler->load( + $message->parentLocationId, + ); + } catch (NotFoundException) { + $this->logger->info( + sprintf( + '%s: Location #%d is gone, aborting', + $this::class, + $message->parentLocationId, + ), + ); + + return; + } + + $this->ancestorIndexer->indexSingle($location); + } +} diff --git a/lib/Core/Search/Solr/FieldMapper/FulltextFieldResolver.php b/lib/Core/Search/Solr/FieldMapper/FulltextFieldResolver.php new file mode 100644 index 00000000..21ddfec3 --- /dev/null +++ b/lib/Core/Search/Solr/FieldMapper/FulltextFieldResolver.php @@ -0,0 +1,72 @@ +versionInfo->contentInfo->contentTypeId; + $contentType = $this->contentTypeHandler->load($contentTypeId); + } catch (NotFoundException) { + return $fields; + } + + foreach ($content->fields as $field) { + if ($field->languageCode !== $languageCode) { + continue; + } + + foreach ($contentType->fieldDefinitions as $fieldDefinition) { + if (!$fieldDefinition->isSearchable) { + continue; + } + + if ($fieldDefinition->id !== $field->fieldDefinitionId) { + continue; + } + + $fieldType = $this->fieldRegistry->getType($field->type); + $indexFields = $fieldType->getIndexData($field, $fieldDefinition); + + foreach ($indexFields as $indexField) { + if ($indexField->value === null) { + continue; + } + + if (!$indexField->getType() instanceof FullTextField) { + continue; + } + + $fields[] = new Field( + 'meta_content__text', + (string) $indexField->value, + new TextField(), + ); + } + } + } + return $fields; + } +} diff --git a/lib/Core/Search/Solr/FieldMapper/ParentChildFieldMapper.php b/lib/Core/Search/Solr/FieldMapper/ParentChildFieldMapper.php new file mode 100644 index 00000000..eac82bf3 --- /dev/null +++ b/lib/Core/Search/Solr/FieldMapper/ParentChildFieldMapper.php @@ -0,0 +1,185 @@ + + */ + private array $contentTypeIdIdentifierCache; + + /** + * @param array $configuration + */ + public function __construct( + private readonly array $configuration, + private readonly FulltextFieldResolver $fulltextFieldResolver, + private readonly ContentTypeHandler $contentTypeHandler, + private readonly ContentHandler $contentHandler, + private readonly LocationHandler $locationHandler, + private readonly Handler $searchHandler, + private readonly int $childrenLimit = 99, + ) {} + + public function accept(SPIContent $content, $languageCode): bool + { + $contentTypeId = $content->versionInfo->contentInfo->contentTypeId; + $contentType = $this->contentTypeHandler->load($contentTypeId); + $contentTypeIdentifier = $contentType->identifier; + + return array_key_exists($contentTypeIdentifier, $this->configuration); + } + + /** + * @param string $languageCode + * + * @return \Ibexa\Contracts\Core\Search\Field[] + */ + public function mapFields(SPIContent $content, $languageCode): array + { + $contentTypeId = $content->versionInfo->contentInfo->contentTypeId; + $contentType = $this->contentTypeHandler->load($contentTypeId); + $contentTypeIdentifier = $contentType->identifier; + + return $this->recursiveMapFields( + $content->versionInfo->contentInfo, + $languageCode, + $this->configuration[$contentTypeIdentifier], + false, + ); + } + + /** + * @param array $configuration + * + * @return \Ibexa\Contracts\Core\Search\Field[] + */ + private function recursiveMapFields( + ContentInfo $contentInfo, + string $languageCode, + ?array $configuration, + bool $doIndex = true, + ): array { + $fieldsGrouped = [[]]; + $isIndexed = !isset($configuration['indexed']) || (bool) $configuration['indexed']; + $childrenConfiguration = $configuration['children'] ?? []; + + if ($isIndexed && $doIndex) { + $content = $this->contentHandler->load($contentInfo->id); + $fieldsGrouped[] = $this->fulltextFieldResolver->resolveFields($content, $languageCode); + } + + $childrenContentInfoList = $this->loadChildrenContentInfoList( + $contentInfo, + $languageCode, + $childrenConfiguration, + ); + + foreach ($childrenContentInfoList as $childContentInfo) { + $contentTypeId = $childContentInfo->contentTypeId; + $contentTypeIdentifier = $this->getContentTypeIdentifier($contentTypeId); + $childConfiguration = $childrenConfiguration[$contentTypeIdentifier] ?? null; + + if ($childConfiguration === null) { + continue; + } + + $fieldsGrouped[] = $this->recursiveMapFields( + $childContentInfo, + $languageCode, + $childConfiguration, + ); + } + return array_merge(...$fieldsGrouped); + } + + private function getContentTypeIdentifier(int $contentTypeId): ?string + { + if (isset($this->contentTypeIdIdentifierCache[$contentTypeId])) { + return $this->contentTypeIdIdentifierCache[$contentTypeId]; + } + + try { + $contentType = $this->contentTypeHandler->load($contentTypeId); + $identifier = $contentType->identifier; + } catch (NotFoundException) { + $identifier = null; + } + + $this->contentTypeIdIdentifierCache[$contentTypeId] = $identifier; + + return $identifier; + } + + /** + * @param array $configuration + * + * @return \Ibexa\Contracts\Core\Persistence\Content\ContentInfo[] + */ + private function loadChildrenContentInfoList( + ContentInfo $contentInfo, + string $languageCode, + array $configuration, + ): array { + $contentTypeIdentifiers = array_keys($configuration); + + if (count($contentTypeIdentifiers) === 0) { + return []; + } + + $searchResult = $this->searchHandler->findContent( + new Query([ + 'filter' => new LocationQueryCriterion( + new LogicalAnd([ + new ContentTypeIdentifier($contentTypeIdentifiers), + new ParentLocationId($contentInfo->mainLocationId), + new Visible(true), + ]), + ), + 'limit' => $this->childrenLimit, + ]), + [ + 'languages' => [ + $languageCode, + ], + ], + ); + + /** @var \Ibexa\Contracts\Core\Persistence\Content\ContentInfo[] $result */ + $result = array_map( + static fn (SearchHit $searchHit) => $searchHit->valueObject, + $searchResult->searchHits, + ); + + return $result; + } +} diff --git a/lib/Core/Search/Solr/ParentChildReindexAncestorResolver.php b/lib/Core/Search/Solr/ParentChildReindexAncestorResolver.php new file mode 100644 index 00000000..ffd47d0f --- /dev/null +++ b/lib/Core/Search/Solr/ParentChildReindexAncestorResolver.php @@ -0,0 +1,276 @@ + + */ + private array $contentIdContentTypeIdentifierCache = []; + + /** + * @param array $configuration + */ + public function __construct( + private readonly ContentHandler $contentHandler, + private readonly ContentTypeHandler $contentTypeHandler, + private readonly LocationHandler $locationHandler, + private readonly array $configuration, + ) {} + + public function resolveAncestor(Location $location): ?Location + { + $ancestry = [$location]; + + do { + $match = $this->matchPath($ancestry); + + if ($match === 0) { + return end($ancestry); + } + } while (is_int($match) && $this->addToAncestry($ancestry)); + + return null; + } + + /** + * Return the location if its content type matches the path parent + * + * @param Location $location + */ + public function resolveAncestorForDeleteContent(Location $location): ?Location + { + $contentTypeIdentifier = $this->getContentTypeIdentifier($location); + + foreach ($this->getPaths() as $path) { + if (str_ends_with($path, $contentTypeIdentifier)) { + return $location; + } + } + + return null; + } + + /** + * Return remaining string length if the path matches (if zero, the match is complete), false otherwise. + * + * @param \Ibexa\Contracts\Core\Persistence\Content\Location[] $ancestry + */ + private function matchPath(array $ancestry): false|int + { + $ancestryPath = $this->getAncestryPath($ancestry); + if ($ancestryPath === null) { + return false; + } + foreach ($this->getPaths() as $path) { + if (str_starts_with($path, $ancestryPath)) { + return mb_strlen($path) - mb_strlen($ancestryPath); + } + } + + return false; + } + + /** + * @param \Ibexa\Contracts\Core\Persistence\Content\Location[] $ancestry + */ + private function getAncestryPath(array $ancestry): ?string + { + $pathElements = []; + + foreach ($ancestry as $location) { + try { + $pathElements[] = $this->getContentTypeIdentifier($location); + } catch (NotFoundException) { + return null; + } + } + + return implode('/', $pathElements); + } + + /** + * @param \Ibexa\Contracts\Core\Persistence\Content\Location[] $ancestry + */ + private function addToAncestry(array &$ancestry): bool + { + /** @var \Ibexa\Contracts\Core\Persistence\Content\Location $last */ + $last = end($ancestry); + + if ($last->depth <= 1) { + return false; + } + + try { + $ancestry[] = $this->getParentLocation($last); + } catch (NotFoundException) { + return false; + } + + return true; + } + + private function getParentLocation(Location $location): Location + { + return $this->locationHandler->load($location->parentId); + } + + private function getContentTypeIdentifier(Location $location): string + { + /** @var int $contentId */ + $contentId = $location->contentId; + + if (!isset($this->contentIdContentTypeIdentifierCache[$contentId])) { + $contentInfo = $this->contentHandler->loadContentInfo($contentId); + $contentTypeId = $contentInfo->contentTypeId; + $contentType = $this->contentTypeHandler->load($contentTypeId); + + $this->contentIdContentTypeIdentifierCache[$contentId] = $contentType->identifier; + } + + return $this->contentIdContentTypeIdentifierCache[$contentId]; + } + + /** + * @return string[] + */ + private function getPaths(): array + { + if ($this->paths === null) { + $normalizedConfiguration = $this->normalizeConfiguration($this->configuration); + $paths = $this->recursiveFlattenPaths($normalizedConfiguration); + $this->paths = $this->expandPaths($paths); + } + + return $this->paths; + } + + /** + * @param string[] $paths + * + * @return string[] + */ + private function expandPaths(array $paths): array + { + $expandedPathsGrouped = [[]]; + + foreach ($paths as $path) { + $expandedPathsGrouped[] = $this->recursiveExpandPath(explode('/', $path)); + } + + return array_merge(...$expandedPathsGrouped); + } + + /** + * @param string[] $pathElements + * + * @return string[] + */ + private function recursiveExpandPath(array $pathElements): array + { + $expandedPaths = []; + + if (count($pathElements) > 1) { + $path = implode('/', $pathElements); + array_shift($pathElements); + + $expandedPaths = [ + $path, + ...$expandedPaths, + ...$this->recursiveExpandPath($pathElements), + ]; + } + + return $expandedPaths; + } + + /** + * @param array $config + * + * @return string[] + */ + private function recursiveFlattenPaths(array $config, string $path = ''): array + { + $paths = []; + + foreach ($config as $key => $value) { + if (is_array($value) && count($value) > 0) { + $paths = [ + ...$paths, + ...$this->recursiveFlattenPaths($value, '/' . $key . $path), + ]; + + continue; + } + + $paths[] = $key . $path; + } + + return $paths; + } + + /** + * @param array $config + * + * @return array + */ + private function normalizeConfiguration(array $config): array + { + $normalizedConfig = []; + + foreach ($config as $key => $value) { + $normalizedConfig[$key] = $this->recursiveNormalizeConfiguration($value); + } + + return $normalizedConfig; + } + + /** + * @param array $config + * + * @return array + */ + private function recursiveNormalizeConfiguration(array $config): array + { + $normalizedConfig = []; + + foreach ($config as $key => $value) { + if ($key === 'indexed') { + continue; + } + + if ($key === 'children') { + return $this->recursiveNormalizeConfiguration($value); + } + + $normalizedConfig[$key] = $this->recursiveNormalizeConfiguration($value); + } + + return $normalizedConfig; + } +} diff --git a/lib/Resources/config/search/common.yaml b/lib/Resources/config/search/common.yaml index f8c994e9..9f0355cf 100644 --- a/lib/Resources/config/search/common.yaml +++ b/lib/Resources/config/search/common.yaml @@ -1,3 +1,4 @@ imports: - { resource: common/asynchronous_indexing.yaml } - { resource: common/field_value_mappers.yaml } + - { resource: common/parent_child_indexing.yaml } diff --git a/lib/Resources/config/search/common/parent_child_indexing.yaml b/lib/Resources/config/search/common/parent_child_indexing.yaml new file mode 100644 index 00000000..57f0527f --- /dev/null +++ b/lib/Resources/config/search/common/parent_child_indexing.yaml @@ -0,0 +1,20 @@ +imports: + - { resource: parent_child_indexing/common.yaml } + - { resource: parent_child_indexing/content.yaml } + - { resource: parent_child_indexing/location.yaml } + - { resource: parent_child_indexing/object_state.yaml } + - { resource: parent_child_indexing/section.yaml } + - { resource: parent_child_indexing/trash.yaml } + +parameters: + netgen.ibexa_search_extra.parent_child_indexer: + parent_content: + children: + child_content: + indexed: true + children: + child_content: + indexed: true + children: + child_content: + indexed: true \ No newline at end of file diff --git a/lib/Resources/config/search/common/parent_child_indexing/common.yaml b/lib/Resources/config/search/common/parent_child_indexing/common.yaml new file mode 100644 index 00000000..c041d921 --- /dev/null +++ b/lib/Resources/config/search/common/parent_child_indexing/common.yaml @@ -0,0 +1,29 @@ +services: + Netgen\IbexaSearchExtra\Core\Search\Solr\FieldMapper\ParentChildFieldMapper: + arguments: + $configuration: '%netgen.ibexa_search_extra.parent_child_indexer%' + $fulltextFieldResolver: '@Netgen\IbexaSearchExtra\Core\Search\Solr\FieldMapper\FulltextFieldResolver' + $contentTypeHandler: '@Ibexa\Contracts\Core\Persistence\Content\Type\Handler' + $contentHandler: '@Ibexa\Contracts\Core\Persistence\Content\Handler' + $locationHandler: '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + $searchHandler: '@Ibexa\Core\Search\Legacy\Content\Handler' + tags: + - { name: ibexa.search.solr.field.mapper.content.translation } + + Netgen\IbexaSearchExtra\Core\Search\Solr\ParentChildReindexAncestorResolver: + arguments: + $contentHandler: '@Ibexa\Core\Persistence\Legacy\Content\Handler' + $contentTypeHandler: '@Ibexa\Contracts\Core\Persistence\Content\Type\Handler' + $locationHandler: '@Ibexa\Core\Persistence\Cache\LocationHandler' + $configuration: '%netgen.ibexa_search_extra.parent_child_indexer%' + + Netgen\IbexaSearchExtra\Core\Search\Solr\FieldMapper\FulltextFieldResolver: + arguments: + $contentTypeHandler: '@Ibexa\Contracts\Core\Persistence\Content\Type\Handler' + $fieldRegistry: '@Ibexa\Core\Search\Common\FieldRegistry' + + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer: + arguments: + $searchHandler: '@Ibexa\Contracts\Core\Search\VersatileHandler' + $contentHandler: '@Ibexa\Core\Persistence\Legacy\Content\Handler' + $ancestorResolver: '@Netgen\IbexaSearchExtra\Core\Search\Solr\ParentChildReindexAncestorResolver' \ No newline at end of file diff --git a/lib/Resources/config/search/common/parent_child_indexing/content.yaml b/lib/Resources/config/search/common/parent_child_indexing/content.yaml new file mode 100644 index 00000000..5ca774a5 --- /dev/null +++ b/lib/Resources/config/search/common/parent_child_indexing/content.yaml @@ -0,0 +1,57 @@ +services: + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Content\CopyContentHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + tags: + - { name: messenger.message_handler } + + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Content\DeleteContentHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + - '@?logger' + tags: + - { name: messenger.message_handler } + - { name: monolog.logger, channel: ngsearchextra } + + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Content\DeleteTranslationHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Handler' + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + - '@?logger' + tags: + - { name: messenger.message_handler } + - { name: monolog.logger, channel: ngsearchextra } + + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Content\HideContentHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + tags: + - { name: messenger.message_handler } + + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Content\PublishVersionHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + tags: + - { name: messenger.message_handler } + + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Content\RevealContentHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + tags: + - { name: messenger.message_handler } + + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Content\UpdateContentMetadataHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Handler' + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + - '@?logger' + tags: + - { name: messenger.message_handler } + - { name: monolog.logger, channel: ngsearchextra } \ No newline at end of file diff --git a/lib/Resources/config/search/common/parent_child_indexing/location.yaml b/lib/Resources/config/search/common/parent_child_indexing/location.yaml new file mode 100644 index 00000000..b151f3fb --- /dev/null +++ b/lib/Resources/config/search/common/parent_child_indexing/location.yaml @@ -0,0 +1,81 @@ +services: + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Location\AssignSectionToSubtreeHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + - '@?logger' + tags: + - { name: messenger.message_handler } + - { name: monolog.logger, channel: ngsearchextra } + + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Location\CopySubtreeHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + - '@?logger' + tags: + - { name: messenger.message_handler } + - { name: monolog.logger, channel: ngsearchextra } + + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Location\CreateLocationHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + - '@?logger' + tags: + - { name: messenger.message_handler } + - { name: monolog.logger, channel: ngsearchextra } + + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Location\DeleteLocationHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + - '@?logger' + tags: + - { name: messenger.message_handler } + - { name: monolog.logger, channel: ngsearchextra } + + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Location\HideLocationHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + - '@?logger' + tags: + - { name: messenger.message_handler } + - { name: monolog.logger, channel: ngsearchextra } + + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Location\MoveSubtreeHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + - '@?logger' + tags: + - { name: messenger.message_handler } + - { name: monolog.logger, channel: ngsearchextra } + + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Location\SwapLocationHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + - '@?logger' + tags: + - { name: messenger.message_handler } + - { name: monolog.logger, channel: ngsearchextra } + + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Location\UnhideLocationHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + - '@?logger' + tags: + - { name: messenger.message_handler } + - { name: monolog.logger, channel: ngsearchextra } + + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Location\UpdateLocationHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + - '@?logger' + tags: + - { name: messenger.message_handler } + - { name: monolog.logger, channel: ngsearchextra } \ No newline at end of file diff --git a/lib/Resources/config/search/common/parent_child_indexing/object_state.yaml b/lib/Resources/config/search/common/parent_child_indexing/object_state.yaml new file mode 100644 index 00000000..8d403ad3 --- /dev/null +++ b/lib/Resources/config/search/common/parent_child_indexing/object_state.yaml @@ -0,0 +1,7 @@ +services: + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\ObjectState\SetContentStateHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + tags: + - { name: messenger.message_handler } diff --git a/lib/Resources/config/search/common/parent_child_indexing/section.yaml b/lib/Resources/config/search/common/parent_child_indexing/section.yaml new file mode 100644 index 00000000..a88d77dd --- /dev/null +++ b/lib/Resources/config/search/common/parent_child_indexing/section.yaml @@ -0,0 +1,7 @@ +services: + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Section\AssignSectionHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + tags: + - { name: messenger.message_handler } diff --git a/lib/Resources/config/search/common/parent_child_indexing/trash.yaml b/lib/Resources/config/search/common/parent_child_indexing/trash.yaml new file mode 100644 index 00000000..d709f57a --- /dev/null +++ b/lib/Resources/config/search/common/parent_child_indexing/trash.yaml @@ -0,0 +1,18 @@ +services: + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Trash\RecoverHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + - '@?logger' + tags: + - { name: messenger.message_handler } + - { name: monolog.logger, channel: ngsearchextra } + + Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\Trash\TrashHandler: + arguments: + - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' + - '@Netgen\IbexaSearchExtra\Core\Search\Common\Messenger\MessageHandler\Search\ParentChildIndexing\AncestorIndexer' + - '@?logger' + tags: + - { name: messenger.message_handler } + - { name: monolog.logger, channel: ngsearchextra }