Skip to content

Commit c13f137

Browse files
authored
Merge pull request #27 from ensi-platform/task-102033-v0
#102033 added composite terms aggregation
2 parents 693f0af + 6a8a468 commit c13f137

File tree

7 files changed

+227
-10
lines changed

7 files changed

+227
-10
lines changed

src/Aggregating/Bucket.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44

55
class Bucket
66
{
7-
public function __construct(public mixed $key, public int $count)
7+
public function __construct(public mixed $key, public int $count, protected array $compositeValues = [])
88
{
99
}
10+
11+
public function getCompositeValue(string $name): mixed
12+
{
13+
return $this->compositeValues[$name] ?? null;
14+
}
1015
}

src/Aggregating/Bucket/TermsAggregation.php

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@
55
use Ensi\LaravelElasticQuery\Aggregating\BucketCollection;
66
use Ensi\LaravelElasticQuery\Aggregating\Result;
77
use Ensi\LaravelElasticQuery\Contracts\Aggregation;
8+
use Ensi\LaravelElasticQuery\Search\Sorting\Sort;
89
use Webmozart\Assert\Assert;
910

1011
class TermsAggregation implements Aggregation
1112
{
12-
public function __construct(private string $name, private string $field, private ?int $size = null)
13-
{
13+
public function __construct(
14+
private string $name,
15+
private string $field,
16+
private ?int $size = null,
17+
private ?Sort $sort = null,
18+
private ?Aggregation $composite = null,
19+
) {
1420
Assert::stringNotEmpty(trim($name));
1521
Assert::stringNotEmpty(trim($field));
1622
Assert::nullOrGreaterThan($this->size, 0);
@@ -29,20 +35,39 @@ public function toDSL(): array
2935
$body['size'] = $this->size;
3036
}
3137

32-
return [
38+
if ($this->sort) {
39+
$body['order'] = $this->sort->toDSL();
40+
}
41+
42+
$dsl = [
3343
$this->name => [
3444
'terms' => $body,
3545
],
3646
];
47+
48+
if ($this->isComposite()) {
49+
$dsl[$this->name]['aggs'] = $this->composite->toDSL();
50+
}
51+
52+
return $dsl;
3753
}
3854

3955
public function parseResults(array $response): array
4056
{
4157
$buckets = array_map(
42-
fn (array $bucket) => Result::parseBucket($bucket),
58+
function (array $bucket) {
59+
$values = $this->isComposite() ? $this->composite->parseResults($bucket) : [];
60+
61+
return Result::parseBucket($bucket, $values);
62+
},
4363
$response[$this->name]['buckets'] ?? []
4464
);
4565

4666
return [$this->name => new BucketCollection($buckets)];
4767
}
68+
69+
public function isComposite(): bool
70+
{
71+
return isset($this->composite);
72+
}
4873
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace Ensi\LaravelElasticQuery\Aggregating\Metrics;
4+
5+
use Ensi\LaravelElasticQuery\Aggregating\MinMax;
6+
use Ensi\LaravelElasticQuery\Aggregating\Result;
7+
use Ensi\LaravelElasticQuery\Contracts\Aggregation;
8+
use Webmozart\Assert\Assert;
9+
10+
class MinMaxScoreAggregation implements Aggregation
11+
{
12+
public function __construct(protected string $name = 'score')
13+
{
14+
Assert::stringNotEmpty(trim($name));
15+
}
16+
17+
public function name(): string
18+
{
19+
return $this->name;
20+
}
21+
22+
public function toDSL(): array
23+
{
24+
return [
25+
"{$this->name}_min" => ['min' => ['script' => "_score"]],
26+
"{$this->name}_max" => ['max' => ['script' => "_score"]],
27+
];
28+
}
29+
30+
public function parseResults(array $response): array
31+
{
32+
return [$this->name => new MinMax(
33+
Result::parseValue($response["{$this->name}_min"]) ?? 0,
34+
Result::parseValue($response["{$this->name}_max"]) ?? 0,
35+
)];
36+
}
37+
}

src/Aggregating/Result.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ public static function parseValue(array $source): mixed
99
return self::parse($source, 'value');
1010
}
1111

12-
public static function parseBucket(array $source): Bucket
12+
public static function parseBucket(array $source, array $compositeValues = []): Bucket
1313
{
14-
return new Bucket(self::parse($source, 'key'), (int)($source['doc_count'] ?? 0));
14+
return new Bucket(self::parse($source, 'key'), (int)($source['doc_count'] ?? 0), $compositeValues);
1515
}
1616

1717
public static function parse(array $source, string $key): mixed

src/Concerns/ConstructsAggregations.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
use Ensi\LaravelElasticQuery\Aggregating\CompositeAggregationBuilder;
1010
use Ensi\LaravelElasticQuery\Aggregating\Metrics\MinMaxAggregation;
1111
use Ensi\LaravelElasticQuery\Aggregating\Metrics\ValueCountAggregation;
12+
use Ensi\LaravelElasticQuery\Contracts\Aggregation;
1213
use Ensi\LaravelElasticQuery\Filtering\BoolQueryBuilder;
14+
use Ensi\LaravelElasticQuery\Search\Sorting\Sort;
1315

1416
trait ConstructsAggregations
1517
{
@@ -19,9 +21,14 @@ trait ConstructsAggregations
1921
protected AggregationCollection $aggregations;
2022
protected BoolQueryBuilder $boolQuery;
2123

22-
public function terms(string $name, string $field, ?int $size = null): static
23-
{
24-
$this->aggregations->add(new TermsAggregation($name, $this->absolutePath($field), $size));
24+
public function terms(
25+
string $name,
26+
string $field,
27+
?int $size = null,
28+
?Sort $sort = null,
29+
?Aggregation $composite = null,
30+
): static {
31+
$this->aggregations->add(new TermsAggregation($name, $this->absolutePath($field), $size, $sort, $composite));
2532

2633
return $this;
2734
}

tests/Functional/Aggregating/AggregationQueryTest.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
namespace Ensi\LaravelElasticQuery\Tests\Functional\Aggregating;
44

55
use Ensi\LaravelElasticQuery\Aggregating\AggregationsQuery;
6+
use Ensi\LaravelElasticQuery\Aggregating\Bucket;
7+
use Ensi\LaravelElasticQuery\Aggregating\Metrics\MinMaxScoreAggregation;
68
use Ensi\LaravelElasticQuery\Aggregating\MinMax;
79
use Ensi\LaravelElasticQuery\Contracts\AggregationsBuilder;
10+
use Ensi\LaravelElasticQuery\Search\Sorting\Sort;
811
use Ensi\LaravelElasticQuery\Tests\Functional\ElasticTestCase;
912
use Ensi\LaravelElasticQuery\Tests\Models\ProductsIndex;
1013
use Ensi\LaravelElasticQuery\Tests\Seeds\ProductIndexSeeder;
@@ -77,4 +80,30 @@ public function testTermsSize(): void
7780

7881
$this->assertCount(1, $results->get('codes'));
7982
}
83+
84+
public function testTermsWithSortByCompositeValue(): void
85+
{
86+
$sort = new Sort('score_max');
87+
$composite = new MinMaxScoreAggregation();
88+
89+
$this->testing
90+
->whereMatch('description', 'water')
91+
->where('package', 'bottle')
92+
->terms(
93+
name: 'codes',
94+
field: 'code',
95+
size: 2,
96+
sort: $sort,
97+
composite: $composite
98+
);
99+
100+
$results = $this->testing->get();
101+
102+
$scores = $results->get('codes')->map(
103+
fn (Bucket $bucket) => $bucket->getCompositeValue('score')->max
104+
);
105+
106+
$this->assertCount(2, $results->get('codes'));
107+
$this->assertGreaterThanOrEqual($scores->first(), $scores->last());
108+
}
80109
}

tests/Unit/Aggregating/Buckets/TermsAggregationTest.php

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
use Ensi\LaravelElasticQuery\Aggregating\Bucket;
66
use Ensi\LaravelElasticQuery\Aggregating\Bucket\TermsAggregation;
77
use Ensi\LaravelElasticQuery\Aggregating\BucketCollection;
8+
use Ensi\LaravelElasticQuery\Aggregating\Metrics\MinMaxScoreAggregation;
9+
use Ensi\LaravelElasticQuery\Aggregating\MinMax;
10+
use Ensi\LaravelElasticQuery\Search\Sorting\Sort;
811
use Ensi\LaravelElasticQuery\Tests\AssertsArray;
912
use Ensi\LaravelElasticQuery\Tests\Unit\UnitTestCase;
1013

@@ -26,6 +29,58 @@ public function testToDSLWithSize(): void
2629
$this->assertArrayStructure(['agg1' => ['terms' => ['field', 'size']]], $testing->toDSL());
2730
}
2831

32+
public function testToDSLWithSort(): void
33+
{
34+
$orderField = 'name';
35+
$sort = new Sort($orderField);
36+
37+
$testing = new TermsAggregation(
38+
name: 'agg1',
39+
field: 'code',
40+
sort: $sort
41+
);
42+
43+
$this->assertArrayStructure(['agg1' => ['terms' => ['field', 'order' => [$orderField]]]], $testing->toDSL());
44+
}
45+
46+
public function testToDSLWithComposite(): void
47+
{
48+
$composite = new MinMaxScoreAggregation();
49+
50+
$testing = new TermsAggregation(
51+
name: 'agg1',
52+
field: 'code',
53+
composite: $composite
54+
);
55+
56+
$this->assertArrayStructure(['agg1' => ['terms' => ['field'], 'aggs' => [
57+
'score_min' => ['min' => ['script']],
58+
'score_max' => ['max' => ['script']],
59+
]]], $testing->toDSL());
60+
}
61+
62+
public function testToDSLWithAll(): void
63+
{
64+
$orderField = 'name';
65+
$sort = new Sort($orderField);
66+
$composite = new MinMaxScoreAggregation();
67+
68+
$testing = new TermsAggregation(
69+
name: 'agg1',
70+
field: 'code',
71+
size: 24,
72+
sort: $sort,
73+
composite: $composite
74+
);
75+
76+
$this->assertArrayStructure([
77+
'agg1' => ['terms' => ['field', 'size', 'order' => [$orderField]],
78+
'aggs' => [
79+
'score_min' => ['min' => ['script']],
80+
'score_max' => ['max' => ['script']],
81+
]]], $testing->toDSL());
82+
}
83+
2984
public function testParseResults(): void
3085
{
3186
$result = $this->executeParseResults('agg1');
@@ -47,6 +102,29 @@ public function testParseResultsReadsBuckets(): void
47102
$this->assertInstanceOf(Bucket::class, $result['agg1']->first());
48103
}
49104

105+
public function testParseResultsReadsBucketsWithComposite(): void
106+
{
107+
$buckets = [
108+
[
109+
'key' => 'tv',
110+
'doc_count' => 4,
111+
'score_max' => ['value' => 2],
112+
'score_min' => ['value' => 1],
113+
],
114+
];
115+
116+
$result = $this->executeParseResultsWithComposite('agg1', $buckets);
117+
118+
/** @var Bucket $bucket */
119+
$bucket = $result['agg1']->first();
120+
/** @var MinMax $score */
121+
$score = $bucket->getCompositeValue('score');
122+
123+
$this->assertInstanceOf(Bucket::class, $bucket);
124+
$this->assertEquals($buckets[0]['score_min']['value'], $score->min);
125+
$this->assertEquals($buckets[0]['score_max']['value'], $score->max);
126+
}
127+
50128
public function testParseEmptyResults(): void
51129
{
52130
$result = $this->executeParseResults('agg1', []);
@@ -55,6 +133,14 @@ public function testParseEmptyResults(): void
55133
$this->assertInstanceOf(BucketCollection::class, $result['agg1']);
56134
}
57135

136+
public function testParseEmptyResultsWithComposite(): void
137+
{
138+
$result = $this->executeParseResultsWithComposite('agg1', []);
139+
140+
$this->assertArrayHasKey('agg1', $result);
141+
$this->assertInstanceOf(BucketCollection::class, $result['agg1']);
142+
}
143+
58144
private function executeParseResults(string $aggName, ?array $buckets = null): array
59145
{
60146
if ($buckets === null) {
@@ -70,4 +156,32 @@ private function executeParseResults(string $aggName, ?array $buckets = null): a
70156

71157
return $testing->parseResults($response);
72158
}
159+
160+
private function executeParseResultsWithComposite(string $aggName, ?array $buckets = null): array
161+
{
162+
if ($buckets === null) {
163+
$buckets = [
164+
[
165+
'key' => 'tv',
166+
'doc_count' => 4,
167+
'score_max' => ['value' => 1],
168+
'score_min' => ['value' => 1],
169+
],
170+
];
171+
}
172+
173+
$response = [$aggName => [
174+
'doc_count_error_upper_bound' => 0,
175+
'buckets' => $buckets,
176+
]];
177+
178+
$composite = new MinMaxScoreAggregation();
179+
$testing = new TermsAggregation(
180+
name: $aggName,
181+
field: 'code',
182+
composite: $composite
183+
);
184+
185+
return $testing->parseResults($response);
186+
}
73187
}

0 commit comments

Comments
 (0)