diff --git a/src/Aggregating/Metrics/RangesAggregation.php b/src/Aggregating/Metrics/RangesAggregation.php new file mode 100644 index 0000000..38039a1 --- /dev/null +++ b/src/Aggregating/Metrics/RangesAggregation.php @@ -0,0 +1,59 @@ +name; + } + + public function add(Range $range): self + { + $this->ranges[] = $range; + + return $this; + } + + public function toDSL(): array + { + if (empty($this->ranges)) { + return []; + } + + return [ + $this->name => [ + "range" => [ + "field" => $this->field, + "ranges" => array_map(fn (Range $range) => $range->toDSL(), $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/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/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/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/src/Contracts/BoolQuery.php b/src/Contracts/BoolQuery.php index 7d3723a..6cd991c 100644 --- a/src/Contracts/BoolQuery.php +++ b/src/Contracts/BoolQuery.php @@ -45,6 +45,8 @@ 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 * @param ?DSLAware $query 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..7de92f9 --- /dev/null +++ b/src/Filtering/Criterias/Between.php @@ -0,0 +1,19 @@ + [$this->field => ['gte' => $this->from, 'lte' => $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(); diff --git a/tests/IntegrationTests/AggregationQueryIntegrationTest.php b/tests/IntegrationTests/AggregationQueryIntegrationTest.php index bfdf770..89e7c93 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,30 @@ assertEquals(2, $results->get('cardinality')); }); +test('aggregation query ranges', function () { + /** @var IntegrationTestCase $this */ + + $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(); + + /** @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 () { /** @var IntegrationTestCase $this */ diff --git a/tests/IntegrationTests/Search/FilteringSearchQueryIntegrationTest.php b/tests/IntegrationTests/Search/FilteringSearchQueryIntegrationTest.php index a697704..a37f8be 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: 8); + + $this->assertDocumentIds($query, [471, 328, 1, 405]); +}); + test('filtering search query whereNotNull', function () { /** @var SearchIntegrationTestCase $this */