Skip to content

Commit

Permalink
Add InCondition Filter (#447)
Browse files Browse the repository at this point in the history
This add supports for `InCondition` with the already support for
`OrCondition` and `EqualConditon` it should be possible to implement a
`InCondition` in all adapters as `IN` is just a shortcut for
`EqualCondition` on the same field `Or` connected.

So instead of writing:

```php
new Condition\OrCondition(
    new Condition\EqualCondition('tags', 'UI'),
    new Condition\EqualCondition('tags', 'UX'),
    new Condition\EqualCondition('tags', 'Frontend'),
);
```

It should now be possible to write:

```php
new Condition\InCondition('tags', ['UI','UX','Frontend'])
```

fix #444

### Todo

 - [x] Core: Add InCondition 
 - [x] Memory 
 - [x] Elasticsearch 
 - [x] Opensearch
 - [x] Meilisearch
 - [x] Algolia
 - [x] Loupe
 - [x] Redisearch
 - [x] Solr
 - [x] Typesense

Co-authored-by: ToshY <[email protected]>
  • Loading branch information
alexander-schranz and ToshY authored Oct 15, 2024
1 parent 663bc73 commit c23e9f4
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 0 deletions.
4 changes: 4 additions & 0 deletions packages/seal-algolia-adapter/src/AlgoliaSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => [
Expand Down
1 change: 1 addition & 0 deletions packages/seal-loupe-adapter/src/LoupeSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
4 changes: 4 additions & 0 deletions packages/seal-meilisearch-adapter/src/MeilisearchSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions packages/seal-memory-adapter/src/MemorySearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => [
Expand Down
4 changes: 4 additions & 0 deletions packages/seal-redisearch-adapter/src/RediSearchSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) . '}',
Expand Down
4 changes: 4 additions & 0 deletions packages/seal-solr-adapter/src/SolrSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
4 changes: 4 additions & 0 deletions packages/seal-typesense-adapter/src/TypesenseSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
42 changes: 42 additions & 0 deletions packages/seal/src/Search/Condition/InCondition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Schranz Search package.
*
* (c) Alexander Schranz <[email protected]>
*
* 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<string|int|float|bool> $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<EqualCondition|GreaterThanCondition|GreaterThanEqualCondition|IdentifierCondition|LessThanCondition|LessThanEqualCondition|NotEqualCondition|AndCondition|OrCondition> $conditions */
$conditions = [];
foreach ($this->values as $value) {
$conditions[] = new EqualCondition($this->field, $value);
}

return new OrCondition(...$conditions);
}
}
46 changes: 46 additions & 0 deletions packages/seal/src/Testing/AbstractSearcherTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit c23e9f4

Please sign in to comment.