Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NotInCondition Filter #448

Merged
merged 14 commits into from
Oct 16, 2024
8 changes: 5 additions & 3 deletions packages/seal-algolia-adapter/src/AlgoliaSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,11 @@ private function recursiveResolveFilterConditions(Index $index, array $condition
$filters = [];

foreach ($conditions as $filter) {
if ($filter instanceof Condition\InCondition) {
$filter = $filter->createOrCondition();
}
$filter = match (true) {
$filter instanceof Condition\InCondition => $filter->createOrCondition(),
$filter instanceof Condition\NotInCondition => $filter->createAndCondition(),
default => $filter,
};

match (true) {
$filter instanceof Condition\IdentifierCondition => $filters[] = $index->getIdentifierField()->name . ':' . $this->escapeFilterValue($filter->identifier),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@ 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\InCondition, => $filterQueries[]['terms'][$this->getFilterField($indexes, $filter->field)] = $filter->values,
$filter instanceof Condition\NotInCondition => $filterQueries[]['bool']['must_not']['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 @@ -151,6 +151,7 @@ private function recursiveResolveFilterConditions(Index $index, array $condition
$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\NotInCondition => $filters[] = $this->loupeHelper->formatField($filter->field) . ' NOT 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
9 changes: 9 additions & 0 deletions packages/seal-meilisearch-adapter/src/MeilisearchSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ private function escapeFilterValue(string|int|float|bool $value): string
};
}

private function escapeArrayFilterValues(array $value): string
{
return implode(
', ',
array_map([$this, 'escapeFilterValue'], $value)
);
}

/**
* @param object[] $conditions
*/
Expand All @@ -143,6 +151,7 @@ private function recursiveResolveFilterConditions(Index $index, array $condition
$filter instanceof Condition\SearchCondition => $query = $filter->query,
$filter instanceof Condition\EqualCondition => $filters[] = $filter->field . ' = ' . $this->escapeFilterValue($filter->value),
$filter instanceof Condition\NotEqualCondition => $filters[] = $filter->field . ' != ' . $this->escapeFilterValue($filter->value),
$filter instanceof Condition\NotInCondition => $filters[] = $filter->field . ' NOT IN [' . $this->escapeArrayFilterValues($filter->values) . ']',
ToshY marked this conversation as resolved.
Show resolved Hide resolved
$filter instanceof Condition\GreaterThanCondition => $filters[] = $filter->field . ' > ' . $this->escapeFilterValue($filter->value),
$filter instanceof Condition\GreaterThanEqualCondition => $filters[] = $filter->field . ' >= ' . $this->escapeFilterValue($filter->value),
$filter instanceof Condition\LessThanCondition => $filters[] = $filter->field . ' < ' . $this->escapeFilterValue($filter->value),
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 @@ -125,6 +125,16 @@ private function filterDocuments(Index $index, array $documents, object $filter)
if ([] === \array_intersect($filter->values, $values)) {
continue;
}
} elseif ($filter instanceof Condition\NotInCondition) {
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 @@ -174,6 +174,7 @@ private function recursiveResolveFilterConditions(array $indexes, array $filters
$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\NotInCondition => $filterQueries[]['bool']['must_not']['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
8 changes: 5 additions & 3 deletions packages/seal-redisearch-adapter/src/RediSearchSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,11 @@ private function recursiveResolveFilterConditions(Index $index, array $condition
$filters = [];

foreach ($conditions as $filter) {
if ($filter instanceof Condition\InCondition) {
$filter = $filter->createOrCondition();
}
$filter = match (true) {
$filter instanceof Condition\InCondition => $filter->createOrCondition(),
$filter instanceof Condition\NotInCondition => $filter->createAndCondition(),
default => $filter,
};

match (true) {
$filter instanceof Condition\SearchCondition => $filters[] = '%%' . \implode('%% ', \explode(' ', $this->escapeFilterValue($filter->query))) . '%%', // levenshtein of 2 per word
Expand Down
8 changes: 5 additions & 3 deletions packages/seal-solr-adapter/src/SolrSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,11 @@ private function recursiveResolveFilterConditions(Index $index, array $condition
$filters = [];

foreach ($conditions as $filter) {
if ($filter instanceof Condition\InCondition) {
$filter = $filter->createOrCondition();
}
$filter = match (true) {
$filter instanceof Condition\InCondition => $filter->createOrCondition(),
$filter instanceof Condition\NotInCondition => $filter->createAndCondition(),
default => $filter,
};

match (true) {
$filter instanceof Condition\SearchCondition => $queryText = $filter->query,
Expand Down
8 changes: 5 additions & 3 deletions packages/seal-typesense-adapter/src/TypesenseSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,11 @@ private function recursiveResolveFilterConditions(Index $index, array $condition
$filters = [];

foreach ($conditions as $filter) {
if ($filter instanceof Condition\InCondition) {
$filter = $filter->createOrCondition();
}
$filter = match (true) {
$filter instanceof Condition\InCondition => $filter->createOrCondition(),
$filter instanceof Condition\NotInCondition => $filter->createAndCondition(),
default => $filter,
};

match (true) {
$filter instanceof Condition\IdentifierCondition => $filters[] = 'id:=' . $this->escapeFilterValue($filter->identifier),
Expand Down
42 changes: 42 additions & 0 deletions packages/seal/src/Search/Condition/NotInCondition.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 NotInCondition
{
/**
* @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 `NOT IN` operator, so we need to convert it to an `AND` condition.
*/
public function createAndCondition(): AndCondition
{
/** @var array<EqualCondition|GreaterThanCondition|GreaterThanEqualCondition|IdentifierCondition|LessThanCondition|LessThanEqualCondition|NotEqualCondition|AndCondition|OrCondition> $conditions */
$conditions = [];
foreach ($this->values as $value) {
$conditions[] = new NotEqualCondition($this->field, $value);
}

return new AndCondition(...$conditions);
}
}
47 changes: 47 additions & 0 deletions packages/seal/src/Testing/AbstractSearcherTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,53 @@ public function testInCondition(): void
}
}

public function testNotInCondition(): 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\NotInCondition('tags', ['UI']));

$expectedDocumentsVariantA = [
$documents[2],
$documents[3],
];

$expectedDocumentsVariantB = [
$documents[3],
$documents[2],
];

$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
Loading