diff --git a/packages/seal-algolia-adapter/src/AlgoliaSearcher.php b/packages/seal-algolia-adapter/src/AlgoliaSearcher.php index 564eaf29..69fd681a 100644 --- a/packages/seal-algolia-adapter/src/AlgoliaSearcher.php +++ b/packages/seal-algolia-adapter/src/AlgoliaSearcher.php @@ -161,6 +161,10 @@ private function recursiveResolveFilterConditions(Index $index, array $condition $filters = []; foreach ($conditions as $filter) { + if ($filter instanceof Condition\InCondition) { + $filter = $filter->createOrCondition(); + } + match (true) { $filter instanceof Condition\IdentifierCondition => $filters[] = $index->getIdentifierField()->name . ':' . $this->escapeFilterValue($filter->identifier), $filter instanceof Condition\SearchCondition => $query = $filter->query, diff --git a/packages/seal-elasticsearch-adapter/src/ElasticsearchSearcher.php b/packages/seal-elasticsearch-adapter/src/ElasticsearchSearcher.php index df7a6d32..534294b3 100644 --- a/packages/seal-elasticsearch-adapter/src/ElasticsearchSearcher.php +++ b/packages/seal-elasticsearch-adapter/src/ElasticsearchSearcher.php @@ -196,6 +196,7 @@ private function recursiveResolveFilterConditions(array $indexes, array $filters $filter instanceof Condition\GreaterThanEqualCondition => $filterQueries[]['range'][$this->getFilterField($indexes, $filter->field)]['gte'] = $filter->value, $filter instanceof Condition\LessThanCondition => $filterQueries[]['range'][$this->getFilterField($indexes, $filter->field)]['lt'] = $filter->value, $filter instanceof Condition\LessThanEqualCondition => $filterQueries[]['range'][$this->getFilterField($indexes, $filter->field)]['lte'] = $filter->value, + $filter instanceof Condition\InCondition => $filterQueries[]['terms'][$this->getFilterField($indexes, $filter->field)] = $filter->values, $filter instanceof Condition\GeoDistanceCondition => $filterQueries[]['geo_distance'] = [ 'distance' => $filter->distance, $this->getFilterField($indexes, $filter->field) => [ diff --git a/packages/seal-loupe-adapter/src/LoupeSearcher.php b/packages/seal-loupe-adapter/src/LoupeSearcher.php index bc7eb7b0..75a62460 100644 --- a/packages/seal-loupe-adapter/src/LoupeSearcher.php +++ b/packages/seal-loupe-adapter/src/LoupeSearcher.php @@ -150,6 +150,7 @@ private function recursiveResolveFilterConditions(Index $index, array $condition $filter instanceof Condition\GreaterThanEqualCondition => $filters[] = $this->loupeHelper->formatField($filter->field) . ' >= ' . $this->escapeFilterValue($filter->value), $filter instanceof Condition\LessThanCondition => $filters[] = $this->loupeHelper->formatField($filter->field) . ' < ' . $this->escapeFilterValue($filter->value), $filter instanceof Condition\LessThanEqualCondition => $filters[] = $this->loupeHelper->formatField($filter->field) . ' <= ' . $this->escapeFilterValue($filter->value), + $filter instanceof Condition\InCondition => $filters[] = $this->loupeHelper->formatField($filter->field) . ' IN (' . \implode(', ', \array_map(fn ($value) => $this->escapeFilterValue($value), $filter->values)) . ')', $filter instanceof Condition\GeoDistanceCondition => $filters[] = \sprintf( '_geoRadius(%s, %s, %s, %s)', $this->loupeHelper->formatField($filter->field), diff --git a/packages/seal-meilisearch-adapter/src/MeilisearchSearcher.php b/packages/seal-meilisearch-adapter/src/MeilisearchSearcher.php index fd8c8236..a7135c02 100644 --- a/packages/seal-meilisearch-adapter/src/MeilisearchSearcher.php +++ b/packages/seal-meilisearch-adapter/src/MeilisearchSearcher.php @@ -134,6 +134,10 @@ private function recursiveResolveFilterConditions(Index $index, array $condition $filters = []; foreach ($conditions as $filter) { + if ($filter instanceof Condition\InCondition) { + $filter = $filter->createOrCondition(); + } + match (true) { $filter instanceof Condition\IdentifierCondition => $filters[] = $index->getIdentifierField()->name . ' = ' . $this->escapeFilterValue($filter->identifier), $filter instanceof Condition\SearchCondition => $query = $filter->query, diff --git a/packages/seal-memory-adapter/src/MemorySearcher.php b/packages/seal-memory-adapter/src/MemorySearcher.php index 6d3ffa68..a857ec7f 100644 --- a/packages/seal-memory-adapter/src/MemorySearcher.php +++ b/packages/seal-memory-adapter/src/MemorySearcher.php @@ -115,6 +115,16 @@ private function filterDocuments(Index $index, array $documents, object $filter) if (!\in_array($filter->value, $values, true)) { continue; } + } elseif ($filter instanceof Condition\InCondition) { + if (\str_contains($filter->field, '.')) { + throw new \RuntimeException('Nested fields are not supported yet.'); + } + + $values = (array) ($document[$filter->field] ?? []); + + if ([] === \array_intersect($filter->values, $values)) { + continue; + } } elseif ($filter instanceof Condition\NotEqualCondition) { if (\str_contains($filter->field, '.')) { throw new \RuntimeException('Nested fields are not supported yet.'); diff --git a/packages/seal-opensearch-adapter/src/OpensearchSearcher.php b/packages/seal-opensearch-adapter/src/OpensearchSearcher.php index 1af6d4c0..f52a37db 100644 --- a/packages/seal-opensearch-adapter/src/OpensearchSearcher.php +++ b/packages/seal-opensearch-adapter/src/OpensearchSearcher.php @@ -173,6 +173,7 @@ private function recursiveResolveFilterConditions(array $indexes, array $filters $filter instanceof Condition\GreaterThanEqualCondition => $filterQueries[]['range'][$this->getFilterField($indexes, $filter->field)]['gte'] = $filter->value, $filter instanceof Condition\LessThanCondition => $filterQueries[]['range'][$this->getFilterField($indexes, $filter->field)]['lt'] = $filter->value, $filter instanceof Condition\LessThanEqualCondition => $filterQueries[]['range'][$this->getFilterField($indexes, $filter->field)]['lte'] = $filter->value, + $filter instanceof Condition\InCondition => $filterQueries[]['terms'][$this->getFilterField($indexes, $filter->field)] = $filter->values, $filter instanceof Condition\GeoDistanceCondition => $filterQueries[]['geo_distance'] = [ 'distance' => $filter->distance, $this->getFilterField($indexes, $filter->field) => [ diff --git a/packages/seal-redisearch-adapter/src/RediSearchSearcher.php b/packages/seal-redisearch-adapter/src/RediSearchSearcher.php index 5f82b6d0..7b0de08a 100644 --- a/packages/seal-redisearch-adapter/src/RediSearchSearcher.php +++ b/packages/seal-redisearch-adapter/src/RediSearchSearcher.php @@ -214,6 +214,10 @@ private function recursiveResolveFilterConditions(Index $index, array $condition $filters = []; foreach ($conditions as $filter) { + if ($filter instanceof Condition\InCondition) { + $filter = $filter->createOrCondition(); + } + match (true) { $filter instanceof Condition\SearchCondition => $filters[] = '%%' . \implode('%% ', \explode(' ', $this->escapeFilterValue($filter->query))) . '%%', // levenshtein of 2 per word $filter instanceof Condition\IdentifierCondition => $filters[] = '@' . $index->getIdentifierField()->name . ':{' . $this->escapeFilterValue($filter->identifier) . '}', diff --git a/packages/seal-solr-adapter/src/SolrSearcher.php b/packages/seal-solr-adapter/src/SolrSearcher.php index 88f79657..99ec42bf 100644 --- a/packages/seal-solr-adapter/src/SolrSearcher.php +++ b/packages/seal-solr-adapter/src/SolrSearcher.php @@ -180,6 +180,10 @@ private function recursiveResolveFilterConditions(Index $index, array $condition $filters = []; foreach ($conditions as $filter) { + if ($filter instanceof Condition\InCondition) { + $filter = $filter->createOrCondition(); + } + match (true) { $filter instanceof Condition\SearchCondition => $queryText = $filter->query, $filter instanceof Condition\IdentifierCondition => $filters[] = $index->getIdentifierField()->name . ':' . $this->escapeFilterValue($filter->identifier), diff --git a/packages/seal-typesense-adapter/src/TypesenseSearcher.php b/packages/seal-typesense-adapter/src/TypesenseSearcher.php index bbe17224..d7068368 100644 --- a/packages/seal-typesense-adapter/src/TypesenseSearcher.php +++ b/packages/seal-typesense-adapter/src/TypesenseSearcher.php @@ -143,6 +143,10 @@ private function recursiveResolveFilterConditions(Index $index, array $condition $filters = []; foreach ($conditions as $filter) { + if ($filter instanceof Condition\InCondition) { + $filter = $filter->createOrCondition(); + } + match (true) { $filter instanceof Condition\IdentifierCondition => $filters[] = 'id:=' . $this->escapeFilterValue($filter->identifier), $filter instanceof Condition\SearchCondition => $query = $filter->query, diff --git a/packages/seal/src/Search/Condition/InCondition.php b/packages/seal/src/Search/Condition/InCondition.php new file mode 100644 index 00000000..cbc75d04 --- /dev/null +++ b/packages/seal/src/Search/Condition/InCondition.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Schranz\Search\SEAL\Search\Condition; + +class InCondition +{ + /** + * @param list $values + */ + public function __construct( + public readonly string $field, + public readonly array $values, + ) { + } + + /** + * @internal This method is for internal use and should not be called from outside. + * + * Some search engines do not support the `IN` operator, so we need to convert it to an `OR` condition. + */ + public function createOrCondition(): OrCondition + { + /** @var array $conditions */ + $conditions = []; + foreach ($this->values as $value) { + $conditions[] = new EqualCondition($this->field, $value); + } + + return new OrCondition(...$conditions); + } +} diff --git a/packages/seal/src/Testing/AbstractSearcherTestCase.php b/packages/seal/src/Testing/AbstractSearcherTestCase.php index 2377fbad..f02c0e14 100644 --- a/packages/seal/src/Testing/AbstractSearcherTestCase.php +++ b/packages/seal/src/Testing/AbstractSearcherTestCase.php @@ -873,6 +873,52 @@ public function testLessThanEqualConditionMultiValue(): void } } + public function testInCondition(): void + { + $documents = TestingHelper::createComplexFixtures(); + + $schema = self::getSchema(); + + foreach ($documents as $document) { + self::$taskHelper->tasks[] = self::$indexer->save( + $schema->indexes[TestingHelper::INDEX_COMPLEX], + $document, + ['return_slow_promise_result' => true], + ); + } + self::$taskHelper->waitForAll(); + + $search = new SearchBuilder($schema, self::$searcher); + $search->addIndex(TestingHelper::INDEX_COMPLEX); + $search->addFilter(new Condition\InCondition('tags', ['UI'])); + + $expectedDocumentsVariantA = [ + $documents[0], + $documents[1], + ]; + $expectedDocumentsVariantB = [ + $documents[1], + $documents[0], + ]; + + $loadedDocuments = [...$search->getResult()]; + $this->assertCount(2, $loadedDocuments); + + $this->assertTrue( + $expectedDocumentsVariantA === $loadedDocuments + || $expectedDocumentsVariantB === $loadedDocuments, + 'Not correct documents where found.', + ); + + foreach ($documents as $document) { + self::$taskHelper->tasks[] = self::$indexer->delete( + $schema->indexes[TestingHelper::INDEX_COMPLEX], + $document['uuid'], + ['return_slow_promise_result' => true], + ); + } + } + public function testSortByAsc(): void { $documents = TestingHelper::createComplexFixtures();