diff --git a/composer.json b/composer.json index 179c4622..17eb2fd5 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "php": "^7.4 || ^8.0", "ext-json": "*", "ext-xmlwriter": "*", - "ibexa/core": "~4.6.0@dev", + "ibexa/core": "dev-taxonomy-suggestions as 4.6.x-dev", "netgen/query-translator": "^1.0.2", "symfony/http-kernel": "^5.0", "symfony/dependency-injection": "^5.0", diff --git a/src/contracts/Query/EmbeddingVisitor.php b/src/contracts/Query/EmbeddingVisitor.php new file mode 100644 index 00000000..721e65b7 --- /dev/null +++ b/src/contracts/Query/EmbeddingVisitor.php @@ -0,0 +1,16 @@ + + */ + protected iterable $visitors = []; + + /** + * @param \Ibexa\Contracts\Solr\Query\EmbeddingVisitor[] $visitors + */ + public function __construct(iterable $visitors = []) + { + $this->visitors = $visitors; + } + + public function canVisit(Embedding $embedding): bool + { + return $this->findVisitor($embedding) !== null; + } + + /** + * Map field value to a proper Solr representation. + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotImplementedException + */ + public function visit(Embedding $embedding, int $limit): string + { + foreach ($this->visitors as $visitor) { + if ($visitor->canVisit($embedding)) { + return $visitor->visit($embedding, $limit); + } + } + + throw new NotImplementedException('No visitor available for: ' . \get_class($embedding)); + } + + private function findVisitor(Embedding $embedding): ?EmbeddingVisitor + { + foreach ($this->visitors as $visitor) { + if ($visitor->canVisit($embedding)) { + return $visitor; + } + } + + return null; + } +} diff --git a/src/lib/Query/Common/QueryConverter/NativeQueryConverter.php b/src/lib/Query/Common/QueryConverter/NativeQueryConverter.php index 2ef6aaea..f16ef15c 100644 --- a/src/lib/Query/Common/QueryConverter/NativeQueryConverter.php +++ b/src/lib/Query/Common/QueryConverter/NativeQueryConverter.php @@ -6,9 +6,11 @@ */ namespace Ibexa\Solr\Query\Common\QueryConverter; +use Ibexa\Contracts\Core\Repository\Values\Content\EmbeddingQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Solr\Query\AggregationVisitor; use Ibexa\Contracts\Solr\Query\CriterionVisitor; +use Ibexa\Contracts\Solr\Query\EmbeddingVisitor; use Ibexa\Contracts\Solr\Query\SortClauseVisitor; use Ibexa\Solr\Query\FacetFieldVisitor; use Ibexa\Solr\Query\QueryConverter; @@ -44,6 +46,8 @@ class NativeQueryConverter extends QueryConverter */ private $aggregationVisitor; + private EmbeddingVisitor $embeddingVisitor; + /** * Construct from visitors. * @@ -55,19 +59,21 @@ public function __construct( CriterionVisitor $criterionVisitor, SortClauseVisitor $sortClauseVisitor, FacetFieldVisitor $facetBuilderVisitor, - AggregationVisitor $aggregationVisitor + AggregationVisitor $aggregationVisitor, + EmbeddingVisitor $embeddingVisitor ) { $this->criterionVisitor = $criterionVisitor; $this->sortClauseVisitor = $sortClauseVisitor; $this->facetBuilderVisitor = $facetBuilderVisitor; $this->aggregationVisitor = $aggregationVisitor; + $this->embeddingVisitor = $embeddingVisitor; } public function convert(Query $query, array $languageSettings = []) { $params = [ 'q' => '{!lucene}' . $this->criterionVisitor->visit($query->query), - 'fq' => '{!lucene}' . $this->criterionVisitor->visit($query->filter), + 'fq' => ['{!lucene}' . $this->criterionVisitor->visit($query->filter)], 'sort' => $this->getSortClauses($query->sortClauses), 'start' => $query->offset, 'rows' => $query->limit, @@ -75,6 +81,10 @@ public function convert(Query $query, array $languageSettings = []) 'wt' => 'json', ]; + if ($query instanceof EmbeddingQuery && $query->getEmbedding() !== null) { + $params['fq'][] = $this->embeddingVisitor->visit($query->getEmbedding(), $query->limit); + } + $facetParams = $this->getFacetParams($query->facetBuilders); if (!empty($facetParams)) { $params['facet'] = 'true'; diff --git a/src/lib/Resources/config/container/solr.yml b/src/lib/Resources/config/container/solr.yml index ee461403..8bfbe0ca 100644 --- a/src/lib/Resources/config/container/solr.yml +++ b/src/lib/Resources/config/container/solr.yml @@ -100,6 +100,7 @@ services: - '@ibexa.solr.query.content.sort_clause_visitor.aggregate' - '@ibexa.solr.query.content.facet_builder_visitor.aggregate' - '@ibexa.solr.query.content.aggregation_visitor.dispatcher' + - '@Ibexa\Solr\Query\Common\EmbeddingVisitor\Aggregate' ibexa.solr.query_converter.location: class: Ibexa\Solr\Query\Common\QueryConverter\NativeQueryConverter @@ -108,6 +109,7 @@ services: - '@ibexa.solr.query.location.sort_clause_visitor.aggregate' - '@ibexa.solr.query.location.facet_builder_visitor.aggregate' - '@ibexa.solr.query.location.aggregation_visitor.dispatcher' + - '@Ibexa\Solr\Query\Common\EmbeddingVisitor\Aggregate' Ibexa\Solr\Gateway\UpdateSerializer: arguments: diff --git a/src/lib/Resources/config/container/solr/services.yml b/src/lib/Resources/config/container/solr/services.yml index 784d866e..8f511637 100644 --- a/src/lib/Resources/config/container/solr/services.yml +++ b/src/lib/Resources/config/container/solr/services.yml @@ -28,6 +28,10 @@ services: arguments: $client: '@ibexa.solr.http_client' + Ibexa\Solr\Query\Common\EmbeddingVisitor\Aggregate: + arguments: + $visitors: !tagged ibexa.search.solr.query.content.embedding.visitor + # Note: services tagged with 'ibexa.search.solr.query.content.criterion.visitor' # are registered to this one using compilation pass ibexa.solr.query.content.criterion_visitor.aggregate: diff --git a/src/lib/Resources/config/solr/managed-schema.xml b/src/lib/Resources/config/solr/managed-schema.xml new file mode 100755 index 00000000..e4242a03 --- /dev/null +++ b/src/lib/Resources/config/solr/managed-schema.xml @@ -0,0 +1,169 @@ + + + +]> + + + + + &langfields; + + + &customfields; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + diff --git a/tests/lib/Search/Query/Common/EmbeddingVisitor/AggregateTest.php b/tests/lib/Search/Query/Common/EmbeddingVisitor/AggregateTest.php new file mode 100644 index 00000000..c07f4b2a --- /dev/null +++ b/tests/lib/Search/Query/Common/EmbeddingVisitor/AggregateTest.php @@ -0,0 +1,79 @@ +createMock(Embedding::class); + + $dispatcher = new Aggregate([ + $this->createVisitorMock($embedding, false), + $this->createVisitorMock($embedding, true), + $this->createVisitorMock($embedding, false), + ]); + + $this->assertTrue($dispatcher->canVisit($embedding)); + } + + public function testCanVisitOnNonSupportedEmbedding(): void + { + $embedding = $this->createMock(Embedding::class); + + $dispatcher = new Aggregate([ + $this->createVisitorMock($embedding, false), + $this->createVisitorMock($embedding, false), + $this->createVisitorMock($embedding, false), + ]); + + $this->assertFalse($dispatcher->canVisit($embedding)); + } + + public function testVisit(): void + { + $embedding = $this->createMock(Embedding::class); + + $visitorA = $this->createVisitorMock($embedding, false); + $visitorB = $this->createVisitorMock($embedding, true); + $visitorC = $this->createVisitorMock($embedding, false); + + $dispatcher = new Aggregate([$visitorA, $visitorB, $visitorC]); + + $visitorB + ->method('visit') + ->with($embedding, 3) + ->willReturn(self::EXAMPLE_VISITOR_RESULT); + + $this->assertEquals( + self::EXAMPLE_VISITOR_RESULT, + $dispatcher->visit($embedding, 3) + ); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject&\Ibexa\Contracts\Solr\Query\EmbeddingVisitor + */ + private function createVisitorMock( + Embedding $embedding, + bool $supports + ): EmbeddingVisitor { + $visitor = $this->createMock(EmbeddingVisitor::class); + $visitor->method('canVisit')->with($embedding)->willReturn($supports); + + return $visitor; + } +}