diff --git a/src/Aggregating/Bucket/FiltersAggregation.php b/src/Aggregating/Bucket/FiltersAggregation.php new file mode 100644 index 0000000..d51cc94 --- /dev/null +++ b/src/Aggregating/Bucket/FiltersAggregation.php @@ -0,0 +1,62 @@ +count(), 0); + } + + public function name(): string + { + return $this->name; + } + + public function toDSL(): array + { + $body['filters']['filters'] = $this->filters->toDSL(); + + if ($this->otherBucketKey != null) { + $body['filters']['other_bucket_key'] = $this->otherBucketKey; + } + + if ($this->isComposite()) { + $body['aggs'] = $this->composite->toDSL(); + } + + return [$this->name => $body]; + } + + public function parseResults(array $response): array + { + $buckets = array_map( + function (mixed $key, array $bucket) { + $values = $this->isComposite() ? $this->composite->parseResults($bucket) : []; + + return Result::parseBucketWithKey($key, $bucket, $values); + }, + array_keys($response[$this->name]['buckets'] ?? []), + $response[$this->name]['buckets'] ?? [] + ); + + return [$this->name => new BucketCollection($buckets)]; + } + + public function isComposite(): bool + { + return isset($this->composite); + } +} diff --git a/src/Aggregating/FiltersCollection.php b/src/Aggregating/FiltersCollection.php new file mode 100644 index 0000000..36e1d24 --- /dev/null +++ b/src/Aggregating/FiltersCollection.php @@ -0,0 +1,34 @@ +items = new Collection(); + } + + public function count(): int + { + return $this->items->count(); + } + + public function toDSL(): array + { + return $this->items + ->mapWithKeys(fn (Criteria $criteria, string $key) => [$key => $criteria->toDSL()]) + ->all(); + } + + public function add(string $name, Criteria $criteria): void + { + $this->items->put($name, $criteria); + } +} diff --git a/src/Aggregating/Result.php b/src/Aggregating/Result.php index 37070c6..990ee5b 100644 --- a/src/Aggregating/Result.php +++ b/src/Aggregating/Result.php @@ -14,6 +14,11 @@ public static function parseBucket(array $source, array $compositeValues = []): return new Bucket(self::parse($source, 'key'), (int)($source['doc_count'] ?? 0), $compositeValues); } + public static function parseBucketWithKey(mixed $key, array $source, array $compositeValues = []): Bucket + { + return new Bucket($key, (int)($source['doc_count'] ?? 0), $compositeValues); + } + public static function parse(array $source, string $key): mixed { $stringValue = $source["{$key}_as_string"] ?? null; diff --git a/src/Concerns/ConstructsAggregations.php b/src/Concerns/ConstructsAggregations.php index 3265fd7..33adca7 100644 --- a/src/Concerns/ConstructsAggregations.php +++ b/src/Concerns/ConstructsAggregations.php @@ -5,9 +5,11 @@ use Closure; use Ensi\LaravelElasticQuery\Aggregating\AggregationCollection; use Ensi\LaravelElasticQuery\Aggregating\Bucket\FilterAggregation; +use Ensi\LaravelElasticQuery\Aggregating\Bucket\FiltersAggregation; use Ensi\LaravelElasticQuery\Aggregating\Bucket\NestedAggregation; use Ensi\LaravelElasticQuery\Aggregating\Bucket\TermsAggregation; use Ensi\LaravelElasticQuery\Aggregating\CompositeAggregationBuilder; +use Ensi\LaravelElasticQuery\Aggregating\FiltersCollection; use Ensi\LaravelElasticQuery\Aggregating\Metrics\CardinalityAggregation; use Ensi\LaravelElasticQuery\Aggregating\Metrics\MinMaxAggregation; use Ensi\LaravelElasticQuery\Aggregating\Metrics\ValueCountAggregation; @@ -43,6 +45,17 @@ public function filter(string $name, Criteria $criteria, AggregationCollection $ return $this; } + public function filters( + string $name, + FiltersCollection $filters, + ?Aggregation $composite = null, + ?string $otherBucketKey = null, + ): static { + $this->aggregations->add(new FiltersAggregation($name, $filters, $composite, $otherBucketKey)); + + return $this; + } + public function minmax(string $name, string $field): static { $this->aggregations->add(new MinMaxAggregation($name, $this->absolutePath($field))); diff --git a/tests/IntegrationTests/AggregationQueryIntegrationTest.php b/tests/IntegrationTests/AggregationQueryIntegrationTest.php index 0fc29db..ab64ddb 100644 --- a/tests/IntegrationTests/AggregationQueryIntegrationTest.php +++ b/tests/IntegrationTests/AggregationQueryIntegrationTest.php @@ -1,10 +1,13 @@ get('codes')); assertGreaterThanOrEqual($scores->first(), $scores->last()); }); + + +test('aggregation query filters', function (?string $defaultBucket) { + /** @var IntegrationTestCase $this */ + + $filters = new FiltersCollection(); + $filters->add('filter_tags', new Term('tags', 'video')); + $filters->add('filter_rating', new RangeBound('rating', '>=', 7)); + + $topHits = new TopHitsAggregation('top_hits'); + + $results = ProductsIndex::aggregate() + ->filters('group_by_filters', $filters, $topHits, otherBucketKey: $defaultBucket) + ->get() + ->get('group_by_filters') + ->keyBy(fn (Bucket $bucket) => $bucket->key); + + $additionResult = $defaultBucket !== null ? 1 : 0; + assertCount($filters->count() + $additionResult, $results); + + assertEqualsCanonicalizing( + [1, 328], + extractBucketValues($results, 'filter_tags', $topHits->name(), 'product_id') + ); + + assertEqualsCanonicalizing( + [1, 150, 405], + extractBucketValues($results, 'filter_rating', $topHits->name(), 'product_id') + ); + + if ($defaultBucket != null) { + assertEqualsCanonicalizing( + [319, 471], + extractBucketValues($results, $defaultBucket, $topHits->name(), 'product_id') + ); + } +})->with([null, 'default_bucket']); diff --git a/tests/Pest.php b/tests/Pest.php index e6a6a10..8f34bb6 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,5 +1,7 @@ get($bucketName); + + $hits = $bucket->getCompositeValue($aggregationName); + + return array_map(fn (array $hit) => $hit['_source'][$key], $hits); +};