From 2e946bdfdc06c6c7c73daea70817d8e1bb398242 Mon Sep 17 00:00:00 2001 From: Andrey Kapitanov Date: Tue, 15 Apr 2025 18:46:38 +0300 Subject: [PATCH 1/5] V8 where between + post filter --- src/Aggregating/Metrics/RangesAggregation.php | 52 +++++++++++++++++++ src/Concerns/DecoratesBoolQuery.php | 7 +++ src/Contracts/BoolQuery.php | 1 + src/Filtering/BoolQueryBuilder.php | 8 +++ src/Filtering/Criterias/Between.php | 19 +++++++ src/Search/SearchQuery.php | 12 +++++ 6 files changed, 99 insertions(+) create mode 100644 src/Aggregating/Metrics/RangesAggregation.php create mode 100644 src/Filtering/Criterias/Between.php diff --git a/src/Aggregating/Metrics/RangesAggregation.php b/src/Aggregating/Metrics/RangesAggregation.php new file mode 100644 index 0000000..fed5632 --- /dev/null +++ b/src/Aggregating/Metrics/RangesAggregation.php @@ -0,0 +1,52 @@ +name; + } + + public function add(int|float|null $from = null, int|float|null $to = null, ?string $key = null): void + { + $this->ranges[] = array_filter(["from" => $from, "to" => $to, "key" => $key]); + } + + public function toDSL(): array + { + if (empty($this->ranges)) { + return []; + } + + return [ + $this->name => [ + "range" => [ + "field" => $this->field, + "ranges" => $this->ranges, + ] + ] + ]; + } + + public function parseResults(array $response): array + { + return array_map( + callback: fn (array $bucket) => Result::parseBucket($bucket), + array: $response[$this->name]['buckets'] ?? [] + ); + } +} diff --git a/src/Concerns/DecoratesBoolQuery.php b/src/Concerns/DecoratesBoolQuery.php index ce04194..3f130ae 100644 --- a/src/Concerns/DecoratesBoolQuery.php +++ b/src/Concerns/DecoratesBoolQuery.php @@ -154,6 +154,13 @@ public function whereMoreLikeThis(array $fields, MoreLikeThis $likeThis, ?MoreLi return $this; } + public function whereBetween(string $field, mixed $from, mixed $to): static + { + $this->forwardCallTo($this->boolQuery(), __FUNCTION__, func_get_args()); + + return $this; + } + /** * @param array $functions * @param ?DSLAware $query diff --git a/src/Contracts/BoolQuery.php b/src/Contracts/BoolQuery.php index 7d3723a..edddd13 100644 --- a/src/Contracts/BoolQuery.php +++ b/src/Contracts/BoolQuery.php @@ -44,6 +44,7 @@ public function orWhereWildcard(string $field, string $query, ?WildcardOptions $ public function addMustBool(callable $fn): static; public function whereMoreLikeThis(array $fields, MoreLikeThis $likeThis, ?MoreLikeOptions $options = null): static; + public function whereBetween(string $field, mixed $from, mixed $to): static; /** * @param array $functions diff --git a/src/Filtering/BoolQueryBuilder.php b/src/Filtering/BoolQueryBuilder.php index 49d6daf..4932ee0 100644 --- a/src/Filtering/BoolQueryBuilder.php +++ b/src/Filtering/BoolQueryBuilder.php @@ -14,6 +14,7 @@ use Ensi\LaravelElasticQuery\Contracts\MoreLikeThis; use Ensi\LaravelElasticQuery\Contracts\MultiMatchOptions; use Ensi\LaravelElasticQuery\Contracts\WildcardOptions; +use Ensi\LaravelElasticQuery\Filtering\Criterias\Between; use Ensi\LaravelElasticQuery\Filtering\Criterias\Exists; use Ensi\LaravelElasticQuery\Filtering\Criterias\FunctionScore; use Ensi\LaravelElasticQuery\Filtering\Criterias\MoreLike; @@ -259,6 +260,13 @@ public function whereMoreLikeThis(array $fields, MoreLikeThis $likeThis, ?MoreLi return $this; } + public function whereBetween(string $field, mixed $from, mixed $to): static + { + $this->filter->add(new Between($this->absolutePath($field), $from, $to)); + + return $this; + } + /** * @param array $functions * @param ?DSLAware $query diff --git a/src/Filtering/Criterias/Between.php b/src/Filtering/Criterias/Between.php new file mode 100644 index 0000000..f0fdd0b --- /dev/null +++ b/src/Filtering/Criterias/Between.php @@ -0,0 +1,19 @@ + [$this->field => ['gte' => $this->from, 'lt' => $this->to]]]; + } +} diff --git a/src/Search/SearchQuery.php b/src/Search/SearchQuery.php index afc75b0..07fc884 100644 --- a/src/Search/SearchQuery.php +++ b/src/Search/SearchQuery.php @@ -31,6 +31,7 @@ class SearchQuery implements SortableQuery, CollapsibleQuery, HighlightingQuery use ExtendsSort; protected BoolQueryBuilder $boolQuery; + protected ?BoolQueryBuilder $postFilter = null; protected SortCollection $sorts; protected ?Collapse $collapse = null; protected ?Highlight $highlight = null; @@ -171,6 +172,10 @@ protected function execute( $dsl['highlight'] = $this->highlight->toDSL(); } + if (!is_null($this->postFilter)) { + $dsl['post_filter'] = $this->postFilter->toDSL(); + } + if ($cursor !== null && !$cursor->isBOF()) { $dsl['search_after'] = $cursor->toDSL(); } @@ -233,6 +238,13 @@ public function highlight(Highlight $highlight): static return $this; } + public function setPostFilter(BoolQueryBuilder $boolQueryBuilder): static + { + $this->postFilter = $boolQueryBuilder; + + return $this; + } + public function addAggregations(Aggregation $aggregation): static { $this->aggregations ??= new AggregationCollection(); From 960e29340ec533d8e0a03b4882fe9bdd6c1e1b50 Mon Sep 17 00:00:00 2001 From: DimionX Date: Tue, 15 Apr 2025 15:46:57 +0000 Subject: [PATCH 2/5] Fix styling --- src/Aggregating/Metrics/RangesAggregation.php | 4 ++-- src/Contracts/BoolQuery.php | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Aggregating/Metrics/RangesAggregation.php b/src/Aggregating/Metrics/RangesAggregation.php index fed5632..c205437 100644 --- a/src/Aggregating/Metrics/RangesAggregation.php +++ b/src/Aggregating/Metrics/RangesAggregation.php @@ -37,8 +37,8 @@ public function toDSL(): array "range" => [ "field" => $this->field, "ranges" => $this->ranges, - ] - ] + ], + ], ]; } diff --git a/src/Contracts/BoolQuery.php b/src/Contracts/BoolQuery.php index edddd13..6cd991c 100644 --- a/src/Contracts/BoolQuery.php +++ b/src/Contracts/BoolQuery.php @@ -44,6 +44,7 @@ public function orWhereWildcard(string $field, string $query, ?WildcardOptions $ public function addMustBool(callable $fn): static; public function whereMoreLikeThis(array $fields, MoreLikeThis $likeThis, ?MoreLikeOptions $options = null): static; + public function whereBetween(string $field, mixed $from, mixed $to): static; /** From 21a863d7a57009b26bc268ad62341fc654831537 Mon Sep 17 00:00:00 2001 From: Andrey Kapitanov Date: Wed, 16 Apr 2025 11:54:36 +0300 Subject: [PATCH 3/5] V8 --- src/Aggregating/Metrics/RangesAggregation.php | 19 +++++++++++++------ src/Aggregating/Range.php | 18 ++++++++++++++++++ src/Concerns/ConstructsAggregations.php | 8 ++++++++ src/Contracts/AggregationsBuilder.php | 9 +++++++++ .../AggregationQueryIntegrationTest.php | 15 +++++++++++++++ .../FilteringSearchQueryIntegrationTest.php | 8 ++++++++ 6 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 src/Aggregating/Range.php diff --git a/src/Aggregating/Metrics/RangesAggregation.php b/src/Aggregating/Metrics/RangesAggregation.php index c205437..38039a1 100644 --- a/src/Aggregating/Metrics/RangesAggregation.php +++ b/src/Aggregating/Metrics/RangesAggregation.php @@ -2,18 +2,23 @@ namespace Ensi\LaravelElasticQuery\Aggregating\Metrics; +use Ensi\LaravelElasticQuery\Aggregating\Range; use Ensi\LaravelElasticQuery\Aggregating\Result; use Ensi\LaravelElasticQuery\Contracts\Aggregation; use Webmozart\Assert\Assert; class RangesAggregation implements Aggregation { - protected array $ranges = []; - - public function __construct(protected string $name, protected string $field) + /** + * @param string $name + * @param string $field + * @param Range[] $ranges + */ + public function __construct(protected string $name, protected string $field, protected array $ranges) { Assert::stringNotEmpty(trim($name)); Assert::stringNotEmpty(trim($field)); + Assert::allIsInstanceOf($ranges, Range::class); } public function name(): string @@ -21,9 +26,11 @@ public function name(): string return $this->name; } - public function add(int|float|null $from = null, int|float|null $to = null, ?string $key = null): void + public function add(Range $range): self { - $this->ranges[] = array_filter(["from" => $from, "to" => $to, "key" => $key]); + $this->ranges[] = $range; + + return $this; } public function toDSL(): array @@ -36,7 +43,7 @@ public function toDSL(): array $this->name => [ "range" => [ "field" => $this->field, - "ranges" => $this->ranges, + "ranges" => array_map(fn (Range $range) => $range->toDSL(), $this->ranges), ], ], ]; diff --git a/src/Aggregating/Range.php b/src/Aggregating/Range.php new file mode 100644 index 0000000..1fe8bfa --- /dev/null +++ b/src/Aggregating/Range.php @@ -0,0 +1,18 @@ + $this->from, "to" => $this->to, "key" => $this->key]); + } +} diff --git a/src/Concerns/ConstructsAggregations.php b/src/Concerns/ConstructsAggregations.php index d59ce53..5913d2d 100644 --- a/src/Concerns/ConstructsAggregations.php +++ b/src/Concerns/ConstructsAggregations.php @@ -14,6 +14,7 @@ use Ensi\LaravelElasticQuery\Aggregating\Metrics\MaxAggregation; use Ensi\LaravelElasticQuery\Aggregating\Metrics\MinAggregation; use Ensi\LaravelElasticQuery\Aggregating\Metrics\MinMaxAggregation; +use Ensi\LaravelElasticQuery\Aggregating\Metrics\RangesAggregation; use Ensi\LaravelElasticQuery\Aggregating\Metrics\ValueCountAggregation; use Ensi\LaravelElasticQuery\Contracts\Aggregation; use Ensi\LaravelElasticQuery\Contracts\Criteria; @@ -87,6 +88,13 @@ public function count(string $name, string $field): static return $this; } + public function ranges(string $name, string $field, array $ranges): static + { + $this->aggregations->add(new RangesAggregation($name, $this->absolutePath($field), $ranges)); + + return $this; + } + public function cardinality(string $name, string $field): static { $this->aggregations->add(new CardinalityAggregation($name, $this->absolutePath($field))); diff --git a/src/Contracts/AggregationsBuilder.php b/src/Contracts/AggregationsBuilder.php index 6f4b100..0b614f6 100644 --- a/src/Contracts/AggregationsBuilder.php +++ b/src/Contracts/AggregationsBuilder.php @@ -3,6 +3,7 @@ namespace Ensi\LaravelElasticQuery\Contracts; use Closure; +use Ensi\LaravelElasticQuery\Aggregating\Range; interface AggregationsBuilder extends BoolQuery { @@ -10,6 +11,14 @@ public function terms(string $name, string $field, ?int $size = null): static; public function minmax(string $name, string $field): static; + /** + * @param string $name + * @param string $field + * @param Range[] $ranges + * @return $this + */ + public function ranges(string $name, string $field, array $ranges): static; + public function min(string $name, string $field, mixed $missing = null): static; public function max(string $name, string $field, mixed $missing = null): static; diff --git a/tests/IntegrationTests/AggregationQueryIntegrationTest.php b/tests/IntegrationTests/AggregationQueryIntegrationTest.php index bfdf770..f591d1b 100644 --- a/tests/IntegrationTests/AggregationQueryIntegrationTest.php +++ b/tests/IntegrationTests/AggregationQueryIntegrationTest.php @@ -5,6 +5,7 @@ use Ensi\LaravelElasticQuery\Aggregating\Metrics\MinMaxScoreAggregation; use Ensi\LaravelElasticQuery\Aggregating\Metrics\TopHitsAggregation; use Ensi\LaravelElasticQuery\Aggregating\MinMax; +use Ensi\LaravelElasticQuery\Aggregating\Range; use Ensi\LaravelElasticQuery\Contracts\AggregationsBuilder; use Ensi\LaravelElasticQuery\Filtering\Criterias\RangeBound; use Ensi\LaravelElasticQuery\Filtering\Criterias\Term; @@ -103,6 +104,20 @@ assertEquals(2, $results->get('cardinality')); }); +test('aggregation query ranges', function () { + /** @var IntegrationTestCase $this */ + + $rangeFromTo = new Range(from: 0, to: 7, key: 'from-0-to-6'); + $rangeFrom = new Range(from: 7, key: 'from-0-to-6'); + $rangeTo = new Range(to: 7, key: 'from-0-to-6'); + + $results = ProductsIndex::aggregate() + ->ranges('ranges', 'rating', [$rangeFromTo, $rangeFrom, $rangeTo]) + ->get(); + + # todo +}); + test('aggregation query count all', function () { /** @var IntegrationTestCase $this */ diff --git a/tests/IntegrationTests/Search/FilteringSearchQueryIntegrationTest.php b/tests/IntegrationTests/Search/FilteringSearchQueryIntegrationTest.php index a697704..2697dbd 100644 --- a/tests/IntegrationTests/Search/FilteringSearchQueryIntegrationTest.php +++ b/tests/IntegrationTests/Search/FilteringSearchQueryIntegrationTest.php @@ -54,6 +54,14 @@ $this->assertDocumentIds($query, [1, 319, 328, 471]); }); +test('filtering search query whereBetween', function () { + /** @var SearchIntegrationTestCase $this */ + + $query = ProductsIndex::query()->whereBetween(field: 'rating', from: 2, to: 9); + + $this->assertDocumentIds($query, [471, 328, 1, 405]); +}); + test('filtering search query whereNotNull', function () { /** @var SearchIntegrationTestCase $this */ From 94042a042c7fb9cacb825b70ee2e479a7dc67a10 Mon Sep 17 00:00:00 2001 From: Andrey Kapitanov Date: Wed, 16 Apr 2025 12:11:30 +0300 Subject: [PATCH 4/5] V8 --- .../AggregationQueryIntegrationTest.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/IntegrationTests/AggregationQueryIntegrationTest.php b/tests/IntegrationTests/AggregationQueryIntegrationTest.php index f591d1b..89e7c93 100644 --- a/tests/IntegrationTests/AggregationQueryIntegrationTest.php +++ b/tests/IntegrationTests/AggregationQueryIntegrationTest.php @@ -107,15 +107,25 @@ test('aggregation query ranges', function () { /** @var IntegrationTestCase $this */ - $rangeFromTo = new Range(from: 0, to: 7, key: 'from-0-to-6'); - $rangeFrom = new Range(from: 7, key: 'from-0-to-6'); - $rangeTo = new Range(to: 7, key: 'from-0-to-6'); + $rangeFromTo = new Range(from: 0, to: 5, key: 'from-0-to-5'); + $rangeFrom = new Range(from: 7, key: 'from-7'); + $rangeTo = new Range(to: 8, key: 'to-8'); $results = ProductsIndex::aggregate() ->ranges('ranges', 'rating', [$rangeFromTo, $rangeFrom, $rangeTo]) ->get(); - # todo + /** @var Bucket $result */ + foreach ($results as $result) { + $expected = match ($result->key) { + 'from-0-to-5' => 2, + 'from-7' => 3, + 'to-8' => 4, + default => null, + }; + + assertEquals($expected, $result->count); + } }); test('aggregation query count all', function () { From 9ef32ac360cae799fcdccdcc799b074ef7a908e5 Mon Sep 17 00:00:00 2001 From: Andrey Kapitanov Date: Wed, 16 Apr 2025 12:58:51 +0300 Subject: [PATCH 5/5] V8 --- src/Filtering/Criterias/Between.php | 2 +- .../Search/FilteringSearchQueryIntegrationTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Filtering/Criterias/Between.php b/src/Filtering/Criterias/Between.php index f0fdd0b..7de92f9 100644 --- a/src/Filtering/Criterias/Between.php +++ b/src/Filtering/Criterias/Between.php @@ -14,6 +14,6 @@ public function __construct(private string $field, private mixed $from, private public function toDSL(): array { - return ['range' => [$this->field => ['gte' => $this->from, 'lt' => $this->to]]]; + return ['range' => [$this->field => ['gte' => $this->from, 'lte' => $this->to]]]; } } diff --git a/tests/IntegrationTests/Search/FilteringSearchQueryIntegrationTest.php b/tests/IntegrationTests/Search/FilteringSearchQueryIntegrationTest.php index 2697dbd..a37f8be 100644 --- a/tests/IntegrationTests/Search/FilteringSearchQueryIntegrationTest.php +++ b/tests/IntegrationTests/Search/FilteringSearchQueryIntegrationTest.php @@ -57,7 +57,7 @@ test('filtering search query whereBetween', function () { /** @var SearchIntegrationTestCase $this */ - $query = ProductsIndex::query()->whereBetween(field: 'rating', from: 2, to: 9); + $query = ProductsIndex::query()->whereBetween(field: 'rating', from: 2, to: 8); $this->assertDocumentIds($query, [471, 328, 1, 405]); });