From 8be78f3acf58ceca4c0c44871e3339b1ce388fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20W=C3=B3js?= Date: Fri, 15 Mar 2024 08:17:57 +0100 Subject: [PATCH 1/5] Added Column search criterion --- composer.json | 4 +- .../IbexaFieldTypeMatrixExtension.php | 4 ++ .../Resources/config/services/solr.yaml | 14 ++++ src/contracts/Search/Criterion/Column.php | 51 +++++++++++++ src/lib/FieldType/Type.php | 2 + src/lib/Search/Solr/ContentFieldMapper.php | 72 +++++++++++++++++++ .../Solr/Criterion/ColumnCriterionVisitor.php | 41 +++++++++++ 7 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 src/bundle/Resources/config/services/solr.yaml create mode 100644 src/contracts/Search/Criterion/Column.php create mode 100644 src/lib/Search/Solr/ContentFieldMapper.php create mode 100644 src/lib/Search/Solr/Criterion/ColumnCriterionVisitor.php diff --git a/composer.json b/composer.json index ade1661..70dca3c 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ "EzSystems\\EzPlatformMatrixFieldtypeBundle\\": "src/bundle/", "EzSystems\\EzPlatformMatrixFieldtype\\": "src/lib/", "Ibexa\\FieldTypeMatrix\\": "src/lib/", - "Ibexa\\Bundle\\FieldTypeMatrix\\": "src/bundle/" + "Ibexa\\Bundle\\FieldTypeMatrix\\": "src/bundle/", + "Ibexa\\Contracts\\FieldTypeMatrix\\": "src/contracts/" } }, "autoload-dev": { @@ -45,6 +46,7 @@ "ibexa/http-cache": "~4.6.0@dev", "ibexa/design-engine": "~4.6.0@dev", "ibexa/code-style": "^1.0", + "ibexa/solr": "~4.6.0@dev", "friendsofphp/php-cs-fixer": "^3.0", "phpunit/phpunit": "^9.5" }, diff --git a/src/bundle/DependencyInjection/IbexaFieldTypeMatrixExtension.php b/src/bundle/DependencyInjection/IbexaFieldTypeMatrixExtension.php index da681aa..d9106cf 100644 --- a/src/bundle/DependencyInjection/IbexaFieldTypeMatrixExtension.php +++ b/src/bundle/DependencyInjection/IbexaFieldTypeMatrixExtension.php @@ -33,6 +33,10 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('default_parameters.yaml'); $loader->load('services.yaml'); + + if ($container->hasExtension('ibexa_solr')) { + $loader->load('services/solr.yaml'); + } } /** diff --git a/src/bundle/Resources/config/services/solr.yaml b/src/bundle/Resources/config/services/solr.yaml new file mode 100644 index 0000000..9c811ec --- /dev/null +++ b/src/bundle/Resources/config/services/solr.yaml @@ -0,0 +1,14 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + Ibexa\FieldTypeMatrix\Search\Solr\ContentFieldMapper: + tags: + - { name: ibexa.search.solr.field.mapper.content } + + Ibexa\FieldTypeMatrix\Search\Solr\ColumnCriterionVisitor: + tags: + - { name: ibexa.search.solr.query.content.criterion.visitor } + - { name: ibexa.search.solr.query.location.criterion.visitor } diff --git a/src/contracts/Search/Criterion/Column.php b/src/contracts/Search/Criterion/Column.php new file mode 100644 index 0000000..cacffd8 --- /dev/null +++ b/src/contracts/Search/Criterion/Column.php @@ -0,0 +1,51 @@ +fieldDefIdentifier = $fieldDefIdentifier; + $this->column = $column; + } + + public function getFieldDefIdentifier(): string + { + return $this->fieldDefIdentifier; + } + + public function getColumn(): string + { + return $this->column; + } + + public function getSpecifications(): array + { + return [ + new Specifications(Operator::IN, Specifications::FORMAT_ARRAY), + new Specifications(Operator::EQ, Specifications::FORMAT_SINGLE), + new Specifications(Operator::CONTAINS, Specifications::FORMAT_SINGLE), + ]; + } +} diff --git a/src/lib/FieldType/Type.php b/src/lib/FieldType/Type.php index f6be935..6fa00e1 100644 --- a/src/lib/FieldType/Type.php +++ b/src/lib/FieldType/Type.php @@ -17,6 +17,8 @@ class Type extends FieldType { + public const FIELD_TYPE_IDENTIFIER = 'matrix'; + /** * {@inheritdoc} */ diff --git a/src/lib/Search/Solr/ContentFieldMapper.php b/src/lib/Search/Solr/ContentFieldMapper.php new file mode 100644 index 0000000..9be5eff --- /dev/null +++ b/src/lib/Search/Solr/ContentFieldMapper.php @@ -0,0 +1,72 @@ +contentTypeHandler = $contentTypeHandler; + } + + public function accept(SPIContent $content): bool + { + return true; + } + + public function mapFields(SPIContent $content): array + { + $searchFields = []; + + $contentType = $this->contentTypeHandler->load( + $content->versionInfo->contentInfo->contentTypeId + ); + + foreach ($content->fields as $field) { + $definition = $this->findDefintion($contentType, $field); + if ($definition === null || $definition->fieldType !== Type::FIELD_TYPE_IDENTIFIER) { + continue; + } + + $columns = array_column($definition->fieldTypeConstraints->fieldSettings['columns'], 'identifier'); + + $data = $field->value->data; + foreach ($data['entries'] as $column => $value) { + $searchFields[] = new Search\Field( + $definition->identifier . '_col_' . $columns[$column] . '_value', + $value, + new Search\FieldType\MultipleStringField() + ); + } + } + + return $searchFields; + } + + private function findDefintion(SPIContent\Type $contentType, Field $field): ?FieldDefinition + { + foreach ($contentType->fieldDefinitions as $definition) { + if ($field->fieldDefinitionId === $definition->id) { + return $definition; + } + } + + return null; + } +} diff --git a/src/lib/Search/Solr/Criterion/ColumnCriterionVisitor.php b/src/lib/Search/Solr/Criterion/ColumnCriterionVisitor.php new file mode 100644 index 0000000..4a445f3 --- /dev/null +++ b/src/lib/Search/Solr/Criterion/ColumnCriterionVisitor.php @@ -0,0 +1,41 @@ +getFieldDefIdentifier() . '_col_' . $criterion->getColumn() . '_value_ms'; + + $queries = []; + foreach ((array)$criterion->value as $value) { + if ($criterion->operator === Operator::CONTAINS) { + $queries[] = $name . ':*' . $this->escapeExpressions($value) . '*'; + } else { + $queries[] = $name . ':"' . $this->escapeQuote($value, true) . '"'; + } + } + + return '(' . implode(' OR ', $queries) . ')'; + } +} From 0717cf75a6ae3319e98cd49d0f70cfaef8942a23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20W=C3=B3js?= Date: Sat, 16 Mar 2024 18:51:33 +0100 Subject: [PATCH 2/5] Added ES support --- composer.json | 12 ++-- .../IbexaFieldTypeMatrixExtension.php | 4 ++ .../config/services/elasticsearch.yaml | 14 ++++ src/lib/Search/Common/IndexDataProvider.php | 66 +++++++++++++++++++ .../Criterion/ColumnCriterionVisitor.php | 50 ++++++++++++++ .../Search/Elasticsearch/IndexSubscriber.php | 60 +++++++++++++++++ src/lib/Search/Solr/ContentFieldMapper.php | 49 ++------------ 7 files changed, 207 insertions(+), 48 deletions(-) create mode 100644 src/bundle/Resources/config/services/elasticsearch.yaml create mode 100644 src/lib/Search/Common/IndexDataProvider.php create mode 100644 src/lib/Search/Elasticsearch/Criterion/ColumnCriterionVisitor.php create mode 100644 src/lib/Search/Elasticsearch/IndexSubscriber.php diff --git a/composer.json b/composer.json index 70dca3c..a5d9960 100644 --- a/composer.json +++ b/composer.json @@ -8,11 +8,11 @@ }, "autoload": { "psr-4": { - "EzSystems\\EzPlatformMatrixFieldtypeBundle\\": "src/bundle/", - "EzSystems\\EzPlatformMatrixFieldtype\\": "src/lib/", "Ibexa\\FieldTypeMatrix\\": "src/lib/", "Ibexa\\Bundle\\FieldTypeMatrix\\": "src/bundle/", - "Ibexa\\Contracts\\FieldTypeMatrix\\": "src/contracts/" + "Ibexa\\Contracts\\FieldTypeMatrix\\": "src/contracts/", + "EzSystems\\EzPlatformMatrixFieldtypeBundle\\": "src/bundle/", + "EzSystems\\EzPlatformMatrixFieldtype\\": "src/lib/" } }, "autoload-dev": { @@ -47,6 +47,7 @@ "ibexa/design-engine": "~4.6.0@dev", "ibexa/code-style": "^1.0", "ibexa/solr": "~4.6.0@dev", + "ibexa/elasticsearch": "~4.6.0@dev", "friendsofphp/php-cs-fixer": "^3.0", "phpunit/phpunit": "^9.5" }, @@ -59,5 +60,8 @@ "branch-alias": { "dev-main": "4.6.x-dev" } - } + }, + "repositories": [ + { "type": "composer", "url": "https://updates.ibexa.co" } + ] } diff --git a/src/bundle/DependencyInjection/IbexaFieldTypeMatrixExtension.php b/src/bundle/DependencyInjection/IbexaFieldTypeMatrixExtension.php index d9106cf..b99e17e 100644 --- a/src/bundle/DependencyInjection/IbexaFieldTypeMatrixExtension.php +++ b/src/bundle/DependencyInjection/IbexaFieldTypeMatrixExtension.php @@ -37,6 +37,10 @@ public function load(array $configs, ContainerBuilder $container) if ($container->hasExtension('ibexa_solr')) { $loader->load('services/solr.yaml'); } + + if ($container->hasExtension('ibexa_elasticsearch')) { + $loader->load('services/elasticsearch.yaml'); + } } /** diff --git a/src/bundle/Resources/config/services/elasticsearch.yaml b/src/bundle/Resources/config/services/elasticsearch.yaml new file mode 100644 index 0000000..53cf93c --- /dev/null +++ b/src/bundle/Resources/config/services/elasticsearch.yaml @@ -0,0 +1,14 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + Ibexa\FieldTypeMatrix\Search\Elasticsearch\IndexSubscriber: + tags: + - { name: kernel.event_subscriber } + + Ibexa\FieldTypeMatrix\Search\Elasticsearch\Criterion\ColumnCriterionVisitor: + tags: + - { name: ibexa.search.elasticsearch.query.content.criterion.visitor } + - { name: ibexa.search.elasticsearch.query.location.criterion.visitor } diff --git a/src/lib/Search/Common/IndexDataProvider.php b/src/lib/Search/Common/IndexDataProvider.php new file mode 100644 index 0000000..d30553b --- /dev/null +++ b/src/lib/Search/Common/IndexDataProvider.php @@ -0,0 +1,66 @@ +contentTypeHandler = $contentTypeHandler; + } + + public function getSearchData(SPIContent $content): array + { + $searchFields = []; + + $contentType = $this->contentTypeHandler->load( + $content->versionInfo->contentInfo->contentTypeId + ); + + foreach ($content->fields as $field) { + $definition = $this->findDefintion($contentType, $field); + if ($definition === null || $definition->fieldType !== Type::FIELD_TYPE_IDENTIFIER) { + continue; + } + + $columns = array_column($definition->fieldTypeConstraints->fieldSettings['columns'], 'identifier'); + + $data = $field->value->data; + foreach ($data['entries'] as $column => $value) { + $searchFields[] = new Search\Field( + $definition->identifier . '_col_' . $columns[$column] . '_value', + $value, + new Search\FieldType\MultipleStringField() + ); + } + } + + return $searchFields; + } + + private function findDefintion(SPIContent\Type $contentType, Field $field): ?FieldDefinition + { + foreach ($contentType->fieldDefinitions as $definition) { + if ($field->fieldDefinitionId === $definition->id) { + return $definition; + } + } + + return null; + } +} diff --git a/src/lib/Search/Elasticsearch/Criterion/ColumnCriterionVisitor.php b/src/lib/Search/Elasticsearch/Criterion/ColumnCriterionVisitor.php new file mode 100644 index 0000000..122f02f --- /dev/null +++ b/src/lib/Search/Elasticsearch/Criterion/ColumnCriterionVisitor.php @@ -0,0 +1,50 @@ +getFieldDefIdentifier() . '_col_' . $criterion->getColumn() . '_value_ms'; + + if ($criterion->operator === Criterion\Operator::CONTAINS) { + $bool = new BoolQuery(); + foreach ((array) $criterion->value as $value) { + $wildcard = new WildcardQuery(); + $wildcard->withField($name); + $wildcard->withValue('*' . $value . '*'); + + $bool->addShould($wildcard); + } + } else { + $terms = new TermsQuery(); + $terms->withField($name); + $terms->withValue((array)$criterion->value); + + return $terms->toArray(); + } + } +} diff --git a/src/lib/Search/Elasticsearch/IndexSubscriber.php b/src/lib/Search/Elasticsearch/IndexSubscriber.php new file mode 100644 index 0000000..faa31f2 --- /dev/null +++ b/src/lib/Search/Elasticsearch/IndexSubscriber.php @@ -0,0 +1,60 @@ +contentHandler = $contentHandler; + $this->indexDataProvider = $indexDataProvider; + } + + public static function getSubscribedEvents(): array + { + return [ + ContentIndexCreateEvent::class => 'onContentIndexCreate', + LocationIndexCreateEvent::class => 'onLocationIndexCreate', + ]; + } + + public function onContentIndexCreate(ContentIndexCreateEvent $event): void + { + $this->appendSearchFields($event->getDocument(), $event->getContent()); + } + + public function onLocationIndexCreate(LocationIndexCreateEvent $event): void + { + $content = $this->contentHandler->load( + $event->getLocation()->contentId + ); + + $this->appendSearchFields($event->getDocument(), $content); + } + + private function appendSearchFields(Document $document, Content $content): void + { + $data = $this->indexDataProvider->getSearchData($content); + foreach ($data as $field) { + $document->fields[] = $field; + } + } +} diff --git a/src/lib/Search/Solr/ContentFieldMapper.php b/src/lib/Search/Solr/ContentFieldMapper.php index 9be5eff..ae2331b 100644 --- a/src/lib/Search/Solr/ContentFieldMapper.php +++ b/src/lib/Search/Solr/ContentFieldMapper.php @@ -8,21 +8,17 @@ namespace Ibexa\FieldTypeMatrix\Search\Solr; +use EzSystems\EzPlatformMatrixFieldtype\Search\Common\IndexDataProvider; use Ibexa\Contracts\Core\Persistence\Content as SPIContent; -use Ibexa\Contracts\Core\Persistence\Content\Field; -use Ibexa\Contracts\Core\Persistence\Content\Type\FieldDefinition; -use Ibexa\Contracts\Core\Persistence\Content\Type\Handler as ContentTypeHandler; -use Ibexa\Contracts\Core\Search; use Ibexa\Contracts\Solr\FieldMapper\ContentFieldMapper as BaseContentFieldMapper; -use Ibexa\FieldTypeMatrix\FieldType\Type; final class ContentFieldMapper extends BaseContentFieldMapper { - private ContentTypeHandler $contentTypeHandler; + private IndexDataProvider $indexDataProvider; - public function __construct(ContentTypeHandler $contentTypeHandler) + public function __construct(IndexDataProvider $indexDataProvider) { - $this->contentTypeHandler = $contentTypeHandler; + $this->indexDataProvider = $indexDataProvider; } public function accept(SPIContent $content): bool @@ -32,41 +28,6 @@ public function accept(SPIContent $content): bool public function mapFields(SPIContent $content): array { - $searchFields = []; - - $contentType = $this->contentTypeHandler->load( - $content->versionInfo->contentInfo->contentTypeId - ); - - foreach ($content->fields as $field) { - $definition = $this->findDefintion($contentType, $field); - if ($definition === null || $definition->fieldType !== Type::FIELD_TYPE_IDENTIFIER) { - continue; - } - - $columns = array_column($definition->fieldTypeConstraints->fieldSettings['columns'], 'identifier'); - - $data = $field->value->data; - foreach ($data['entries'] as $column => $value) { - $searchFields[] = new Search\Field( - $definition->identifier . '_col_' . $columns[$column] . '_value', - $value, - new Search\FieldType\MultipleStringField() - ); - } - } - - return $searchFields; - } - - private function findDefintion(SPIContent\Type $contentType, Field $field): ?FieldDefinition - { - foreach ($contentType->fieldDefinitions as $definition) { - if ($field->fieldDefinitionId === $definition->id) { - return $definition; - } - } - - return null; + return $this->indexDataProvider->getSearchData($content); } } From 95769d87cc049c1c6bc8c0185349b43ad7e1c2bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20W=C3=B3js?= Date: Sat, 16 Mar 2024 18:52:32 +0100 Subject: [PATCH 3/5] fixup! Added ES support --- composer.json | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index a5d9960..70dca3c 100644 --- a/composer.json +++ b/composer.json @@ -8,11 +8,11 @@ }, "autoload": { "psr-4": { + "EzSystems\\EzPlatformMatrixFieldtypeBundle\\": "src/bundle/", + "EzSystems\\EzPlatformMatrixFieldtype\\": "src/lib/", "Ibexa\\FieldTypeMatrix\\": "src/lib/", "Ibexa\\Bundle\\FieldTypeMatrix\\": "src/bundle/", - "Ibexa\\Contracts\\FieldTypeMatrix\\": "src/contracts/", - "EzSystems\\EzPlatformMatrixFieldtypeBundle\\": "src/bundle/", - "EzSystems\\EzPlatformMatrixFieldtype\\": "src/lib/" + "Ibexa\\Contracts\\FieldTypeMatrix\\": "src/contracts/" } }, "autoload-dev": { @@ -47,7 +47,6 @@ "ibexa/design-engine": "~4.6.0@dev", "ibexa/code-style": "^1.0", "ibexa/solr": "~4.6.0@dev", - "ibexa/elasticsearch": "~4.6.0@dev", "friendsofphp/php-cs-fixer": "^3.0", "phpunit/phpunit": "^9.5" }, @@ -60,8 +59,5 @@ "branch-alias": { "dev-main": "4.6.x-dev" } - }, - "repositories": [ - { "type": "composer", "url": "https://updates.ibexa.co" } - ] + } } From c849ce58c67fad0ad19aab1287722b037deb9d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20W=C3=B3js?= Date: Sat, 16 Mar 2024 19:14:43 +0100 Subject: [PATCH 4/5] Added integration tests --- tests/lib/Repository/SearchServiceTest.php | 40 +++++++++++++++++----- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/tests/lib/Repository/SearchServiceTest.php b/tests/lib/Repository/SearchServiceTest.php index f7c2456..ee5d86c 100644 --- a/tests/lib/Repository/SearchServiceTest.php +++ b/tests/lib/Repository/SearchServiceTest.php @@ -11,6 +11,7 @@ use Ibexa\Contracts\Core\Repository\Values\Content\Content; use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType; +use Ibexa\Contracts\FieldTypeMatrix\Search\Criterion\Column; use Ibexa\FieldTypeMatrix\FieldType\Value; use Ibexa\FieldTypeMatrix\FieldType\Value\Row; use Ibexa\Tests\Integration\Core\Repository\BaseTest; @@ -42,6 +43,35 @@ public function testFindContentWithMatrixFieldType(): void $this->assertEquals($content->id, $searchResults->searchHits[0]->valueObject->id); } + public function testFindContentWithMatrixColumnValue(): void + { + if (!in_array(getenv('SEARCH_ENGINE'), ['solr', 'elasticsearch'], true)) { + $this->markTestSkipped(Column::class . ' criterion is not supported by legacy search engine'); + } + + $content = $this->createAndPublishContentWithMatrixFieldType( + 'Content with table', + new Value([ + new Row([ + 'foo' => 'Foo', + 'bar' => 'Bar', + 'baz' => 'Baz', + ]), + ]) + ); + + $searchService = $this->getRepository()->getSearchService(); + + $searchResults = $searchService->findContent( + new Query([ + 'filter' => new Column('table', 'foo', 'Foo'), + ]) + ); + + $this->assertEquals(1, $searchResults->totalCount); + $this->assertEquals($content->id, $searchResults->searchHits[0]->valueObject->id); + } + private function createAndPublishContentWithMatrixFieldType(string $title, Value $table): Content { $contentType = $this->createContentTypeWithMatrixFieldType('content_with_table'); @@ -50,14 +80,8 @@ private function createAndPublishContentWithMatrixFieldType(string $title, Value $locationService = $this->getRepository()->getLocationService(); $contentCreateStruct = $contentService->newContentCreateStruct($contentType, 'eng-GB'); - $contentCreateStruct->setField('title', 'Content with table'); - $contentCreateStruct->setField('table', new Value([ - new Row([ - 'foo' => 'Foo', - 'bar' => 'Bar', - 'baz' => 'Baz', - ]), - ])); + $contentCreateStruct->setField('title', $title); + $contentCreateStruct->setField('table', $table); $contentCreateStruct->remoteId = 'abcdef0123456789abcdef0123456789'; $contentCreateStruct->alwaysAvailable = true; From f2c786ad27062c23c55a99dd15e77ed1d5a91690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20W=C3=B3js?= Date: Sat, 16 Mar 2024 19:15:48 +0100 Subject: [PATCH 5/5] Added missing service --- src/bundle/Resources/config/services.yaml | 1 + src/bundle/Resources/config/services/search.yaml | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 src/bundle/Resources/config/services/search.yaml diff --git a/src/bundle/Resources/config/services.yaml b/src/bundle/Resources/config/services.yaml index acedc05..4d31677 100644 --- a/src/bundle/Resources/config/services.yaml +++ b/src/bundle/Resources/config/services.yaml @@ -2,3 +2,4 @@ imports: - { resource: services/fieldtype.yaml } - { resource: services/command.yaml } - { resource: services/graphql.yaml } + - { resource: services/search.yaml } diff --git a/src/bundle/Resources/config/services/search.yaml b/src/bundle/Resources/config/services/search.yaml new file mode 100644 index 0000000..eab26ad --- /dev/null +++ b/src/bundle/Resources/config/services/search.yaml @@ -0,0 +1,7 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + Ibexa\FieldTypeMatrix\Search\Common\IndexDataProvider: ~