Skip to content

Commit 00386c8

Browse files
committed
Refactor range aggregation and add date range support
Refactors range aggregation output to use count and metric keys with bucket identifiers, and adds support for date range aggregations. Updates related methods, meta handling, and tests to reflect the new structure and ensure original bucket data is available in meta.
1 parent 184e217 commit 00386c8

9 files changed

Lines changed: 140 additions & 68 deletions

File tree

src/Data/MetaDTO.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public function getIndex(): ?string
4949
return Arr::get($this->result, '_index', '');
5050
}
5151

52+
public function getBucket(): ?array
53+
{
54+
return Arr::get($this->result, 'bucket', []);
55+
}
56+
5257
// ----------------------------------------------------------------------
5358
// Collection Level
5459
// ----------------------------------------------------------------------

src/Data/ModelMeta.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ final class ModelMeta
2222

2323
protected array $cursor = [];
2424

25+
protected array $bucket = [];
26+
2527
protected ?int $docCount = null;
2628

2729
// public array $_query = []; //TBD
@@ -76,6 +78,11 @@ public function getHighlights(): array
7678
return $this->highlights ?? [];
7779
}
7880

81+
public function getValue($key): mixed
82+
{
83+
return $this->{$key} ?? null;
84+
}
85+
7986
public function getHighlight($column, $deliminator = ''): ?string
8087
{
8188
return implode($deliminator, Arr::get($this->highlights, $column));
@@ -107,6 +114,7 @@ public function toArray(): array
107114
'sort' => $this->sort,
108115
'cursor' => $this->cursor,
109116
'highlights' => $this->highlights,
117+
'bucket' => $this->bucket,
110118
];
111119
}
112120

@@ -134,6 +142,7 @@ public function setMeta(?MetaDTO $meta = null): void
134142
$this->setCursor($meta->getCursor());
135143
$this->setSort($meta->getSort());
136144
$this->docCount = $meta->getDocCount();
145+
$this->bucket = $meta->getBucket();
137146
}
138147

139148
public function setSort(array $sort): void

src/Eloquent/Docs/ModelDocs.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@
169169
*-----------------------------------
170170
* @method static $this groupBy($groups)
171171
* @method static $this groupByRanges($column, array $ranges)
172+
* @method static $this groupByDateRanges($column, array $ranges, array $options = [])
172173
*-----------------------------------
173174
* @method static $this orderByGeo($column, $pin, $direction = 'asc', $options = [])
174175
* @method static $this orderByGeoDesc($column, $pin, $options = [])

src/Eloquent/ElasticsearchModel.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ public function getMetaAsArray(): array
7474
return $this->_meta->toArray();
7575
}
7676

77+
public function getMetaValue($key): mixed
78+
{
79+
return $this->_meta->getValue($key);
80+
}
81+
7782
// ----------------------------------------------------------------------
7883
// Highlights
7984
// ----------------------------------------------------------------------

src/Query/Builder.php

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,29 @@ public function groupBy(...$groups)
474474
return $this;
475475
}
476476

477+
public function groupByRanges($column, $ranges = [])
478+
{
479+
$args = [
480+
'field' => $column,
481+
'ranges' => $ranges,
482+
];
483+
$key = $column.'_range';
484+
485+
return $this->bucketAggregation($key, 'range', $args);
486+
}
487+
488+
public function groupByDateRanges($column, $ranges = [], $options = [])
489+
{
490+
$args = [
491+
'field' => $column,
492+
'ranges' => $ranges,
493+
'options' => $options,
494+
];
495+
$key = $column.'_range';
496+
497+
return $this->bucketAggregation($key, 'date_range', $args);
498+
}
499+
477500
/**
478501
* {@inheritdoc}
479502
*/
@@ -1917,17 +1940,6 @@ public function bucket($key, $type = null, $args = null, $aggregations = null):
19171940
return $this->bucketAggregation($key, $type, $args, $aggregations);
19181941
}
19191942

1920-
public function groupByRanges($column, $ranges = [])
1921-
{
1922-
$args = [
1923-
'field' => $column,
1924-
'ranges' => $ranges,
1925-
];
1926-
$key = $column.'_ranges';
1927-
1928-
return $this->bucketAggregation($key, 'range', $args);
1929-
}
1930-
19311943
/**
19321944
* Get the aggregations returned from query
19331945
*/

src/Query/Concerns/ProcessesBucketAggregations.php

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ protected function parseBucket($bucketAggregation, $rawAggs)
2525
$result = collect($rawAggs[$key]['buckets'])->map(function ($bucket) use ($key, $type) {
2626
$metricAggs = $this->appendMetricsToBucket($bucket);
2727

28-
if ($type === 'range') {
29-
return $this->makeRangeBucket($bucket, $metricAggs);
28+
if (in_array($type, ['range', 'date_range', 'ip_range'])) {
29+
return $this->unpackRangeBucket($key, $bucket, $metricAggs);
3030
}
3131

3232
if (! is_array($bucket['key'])) {
@@ -36,29 +36,29 @@ protected function parseBucket($bucketAggregation, $rawAggs)
3636
return [
3737
...$bucket['key'],
3838
...$metricAggs,
39-
'_meta' => $this->metaFromResult(['doc_count' => $bucket['doc_count']]),
39+
'_meta' => $this->metaFromResult(['doc_count' => $bucket['doc_count'], 'bucket' => $bucket]),
4040
];
4141

4242
});
4343

4444
return $result->toArray();
4545
}
4646

47-
protected function makeRangeBucket($bucket, $metricAggs)
47+
protected function unpackRangeBucket($key, $bucket, $metricAggs)
4848
{
49-
$cleanBucket = [
50-
'key' => $bucket['key'],
51-
'from' => $bucket['from'] ?? null,
52-
'to' => $bucket['to'] ?? null,
53-
];
49+
$recordKey = $key.'_'.$bucket['key'];
50+
$aggs['count_'.$recordKey] = $bucket['doc_count'];
51+
if ($metricAggs) {
52+
foreach ($metricAggs as $metricAgg => $value) {
53+
$aggs[$metricAgg.'_'.$recordKey] = $value;
54+
}
55+
}
56+
5457
$docCount = $bucket['doc_count'] ?? 0;
55-
unset($bucket['doc_count']);
56-
$cleanBucket['count'] = $docCount;
5758

5859
return [
59-
...$cleanBucket,
60-
...$metricAggs,
61-
'_meta' => $this->metaFromResult(['doc_count' => $docCount]),
60+
...$aggs,
61+
'_meta' => $this->metaFromResult(['doc_count' => $docCount, 'bucket' => $bucket]),
6262
];
6363
}
6464

src/Query/DSL/DslFactory.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -438,13 +438,15 @@ public static function dateHistogram(
438438
];
439439
}
440440

441-
public static function dateRange(array $args, array $options = []): array
441+
public static function dateRange($field, array $ranges, array $options = []): array
442442
{
443+
$payload = [];
444+
$payload['field'] = $field;
445+
$payload = [...$payload, ...$options];
446+
$payload['ranges'] = Helpers::sanitizeRanges($ranges);
447+
443448
return [
444-
'date_range' => array_merge(
445-
$args,
446-
$options
447-
),
449+
'date_range' => $payload,
448450
];
449451
}
450452

src/Query/Grammar.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ public function compileSelect($query): array
243243

244244
$dsl->setBody(['aggs'], $this->compileBucketAggregations($query, $sorts));
245245
$dsl->setBody(['size'], $query->getSetLimit() ?? 0);
246+
// dd($dsl->getDsl());
246247
}
247248

248249
// Else if we have metrics aggregations
@@ -1258,9 +1259,11 @@ protected function compileDateHistogramAggregation(Builder $builder, array $aggr
12581259
protected function compileDateRangeAggregation(Builder $builder, array $aggregation): array
12591260
{
12601261
$args = $aggregation['args'];
1261-
$options = $aggregation['options'] ?? [];
1262+
$field = $args['field'];
1263+
$ranges = $args['ranges'];
1264+
$options = $args['options'] ?? [];
12621265

1263-
return DslFactory::dateRange($args, $options);
1266+
return DslFactory::dateRange($field, $ranges, $options);
12641267
}
12651268

12661269
/**

tests/QueryBuilderRangeAggregationTest.php

Lines changed: 71 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,21 @@
44

55
use Illuminate\Support\Facades\DB;
66
use PDPhilip\Elasticsearch\Tests\Models\Item;
7-
use PDPhilip\Elasticsearch\Tests\Models\User;
87

98
beforeEach(function () {
10-
User::executeSchema();
119
Item::executeSchema();
1210

1311
DB::table('items')->insert([
14-
['name' => 'sword alpha', 'type' => 'sharp', 'amount' => 1500, 'stock' => 1],
15-
['name' => 'sword beta', 'type' => 'sharp', 'amount' => 900, 'stock' => 10],
16-
['name' => 'teddy', 'type' => 'fluffy', 'amount' => 10, 'stock' => 5],
17-
['name' => 'spoon', 'type' => 'round', 'amount' => 3, 'stock' => 15],
18-
['name' => 'sword charlie', 'type' => 'sharp', 'amount' => 350, 'stock' => 111],
19-
['name' => 'sword delta', 'type' => 'sharp', 'amount' => 99, 'stock' => 12],
20-
['name' => 'knife', 'type' => 'sharp', 'amount' => 8, 'stock' => 1],
21-
['name' => 'fork', 'type' => 'sharp', 'amount' => 6, 'stock' => 5],
22-
['name' => 'golf ball', 'type' => 'round', 'amount' => 14, 'stock' => 7],
23-
['name' => 'ball', 'type' => 'round', 'amount' => 14, 'stock' => 7],
12+
['name' => 'sword alpha', 'type' => 'sharp', 'price' => 1500, 'stock' => 1, 'last_sale_at' => '2024-01-01'],
13+
['name' => 'sword beta', 'type' => 'sharp', 'price' => 900, 'stock' => 10, 'last_sale_at' => '2024-02-01'],
14+
['name' => 'teddy', 'type' => 'fluffy', 'price' => 10, 'stock' => 5, 'last_sale_at' => '2024-03-01'],
15+
['name' => 'spoon', 'type' => 'round', 'price' => 3, 'stock' => 15, 'last_sale_at' => '2024-04-01'],
16+
['name' => 'sword charlie', 'type' => 'sharp', 'price' => 350, 'stock' => 111, 'last_sale_at' => '2024-05-01'],
17+
['name' => 'sword delta', 'type' => 'sharp', 'price' => 99, 'stock' => 12, 'last_sale_at' => '2024-06-01'],
18+
['name' => 'knife', 'type' => 'sharp', 'price' => 8, 'stock' => 1, 'last_sale_at' => '2024-07-01'],
19+
['name' => 'fork', 'type' => 'sharp', 'price' => 6, 'stock' => 5, 'last_sale_at' => '2024-08-01'],
20+
['name' => 'golf ball', 'type' => 'round', 'price' => 14, 'stock' => 7, 'last_sale_at' => '2024-09-01'],
21+
['name' => 'ball', 'type' => 'round', 'price' => 14, 'stock' => 7, 'last_sale_at' => '2024-10-01'],
2422
]);
2523

2624
});
@@ -33,12 +31,9 @@
3331
[15, null],
3432
])->get();
3533
expect($groups)->toHaveCount(3)
36-
->and($groups[0]['key'])->toBe('*-5.0')
37-
->and($groups[1]['key'])->toBe('5.0-15.0')
38-
->and($groups[2]['key'])->toBe('15.0-*')
39-
->and($groups[0]['count'])->toBe(2)
40-
->and($groups[1]['count'])->toBe(6)
41-
->and($groups[2]['count'])->toBe(2);
34+
->and($groups[0]['count_stock_range_*-5.0'])->toBe(2)
35+
->and($groups[1]['count_stock_range_5.0-15.0'])->toBe(6)
36+
->and($groups[2]['count_stock_range_15.0-*'])->toBe(2);
4237

4338
});
4439
it('groups by ranges using associative arrays for ranges', function () {
@@ -56,12 +51,9 @@
5651
],
5752
])->get();
5853
expect($groups)->toHaveCount(3)
59-
->and($groups[0]['key'])->toBe('*-5.0')
60-
->and($groups[1]['key'])->toBe('5.0-15.0')
61-
->and($groups[2]['key'])->toBe('15.0-*')
62-
->and($groups[0]['count'])->toBe(2)
63-
->and($groups[1]['count'])->toBe(6)
64-
->and($groups[2]['count'])->toBe(2);
54+
->and($groups[0]['count_stock_range_*-5.0'])->toBe(2)
55+
->and($groups[1]['count_stock_range_5.0-15.0'])->toBe(6)
56+
->and($groups[2]['count_stock_range_15.0-*'])->toBe(2);
6557
});
6658

6759
it('groups by ranges using associative arrays for ranges with custom keys', function () {
@@ -82,12 +74,9 @@
8274
],
8375
])->get();
8476
expect($groups)->toHaveCount(3)
85-
->and($groups[0]['key'])->toBe('low-stock')
86-
->and($groups[1]['key'])->toBe('medium-stock')
87-
->and($groups[2]['key'])->toBe('high-stock')
88-
->and($groups[0]['count'])->toBe(2)
89-
->and($groups[1]['count'])->toBe(6)
90-
->and($groups[2]['count'])->toBe(2);
77+
->and($groups[0]['count_stock_range_low-stock'])->toBe(2)
78+
->and($groups[1]['count_stock_range_medium-stock'])->toBe(6)
79+
->and($groups[2]['count_stock_range_high-stock'])->toBe(2);
9180
});
9281

9382
it('groups by ranges and aggregates', function () {
@@ -106,11 +95,57 @@
10695
'key' => 'high-stock',
10796
'from' => 15,
10897
],
109-
])->agg(['min', 'max', 'count'], 'amount');
98+
])->agg(['min', 'max', 'count'], 'price');
11099
expect($groups)->toHaveCount(3)
111-
->and($groups[1]['key'])->toBe('medium-stock')
112-
->and($groups[1]['count'])->toBe(6)
113-
->and($groups[1]['count'])->toBe($groups[1]['count_amount']) // the aggregate `count_amount` reflects the same as the group count
114-
->and((int) $groups[1]['min_amount'])->toBe(6)
115-
->and((int) $groups[1]['max_amount'])->toBe(900);
100+
->and($groups[1]['count_stock_range_medium-stock'])->toBe(6)
101+
->and($groups[1]['count_stock_range_medium-stock'])->toBe($groups[1]['count_price_stock_range_medium-stock']) // the aggregate `count_amount` reflects the same as the group count
102+
->and((int) $groups[1]['min_price_stock_range_medium-stock'])->toBe(6)
103+
->and((int) $groups[1]['max_price_stock_range_medium-stock'])->toBe(900);
104+
});
105+
106+
it('groups by ranges and aggregates with original bucket in meta', function () {
107+
108+
$groups = Item::groupByRanges('stock', [
109+
[
110+
'key' => 'low-stock',
111+
'to' => 5,
112+
],
113+
[
114+
'key' => 'medium-stock',
115+
'from' => 5,
116+
'to' => 15,
117+
],
118+
[
119+
'key' => 'high-stock',
120+
'from' => 15,
121+
],
122+
])->agg(['min', 'max', 'count'], 'price');
123+
$bucketMediumStock = $groups[1]->getMetaValue('bucket');
124+
expect($bucketMediumStock)->toHaveCount(7)
125+
->and($bucketMediumStock['key'])->toBe('medium-stock')
126+
->and((int) $bucketMediumStock['from'])->toBe(5)
127+
->and((int) $bucketMediumStock['to'])->toBe(15)
128+
->and((int) $bucketMediumStock['doc_count'])->toBe(6)
129+
->and((int) $bucketMediumStock['count_price']['value'])->toBe(6)
130+
->and((int) $bucketMediumStock['min_price']['value'])->toBe(6)
131+
->and((int) $bucketMediumStock['max_price']['value'])->toBe(900);
132+
});
133+
134+
it('groups by date range and aggregates', function () {
135+
136+
$ranges = [
137+
['to' => '2024-05-01', 'key' => 'before-campaign'],
138+
['from' => '2024-05-01', 'key' => 'after-campaign'],
139+
];
140+
$options = ['format' => 'yyyy-MM-dd'];
141+
$groups = Item::groupByDateRanges('last_sale_at', $ranges, $options)->agg(['min', 'max', 'count'], 'price');
142+
expect($groups)->toHaveCount(2)
143+
->and((int) $groups[0]['count_last_sale_at_range_before-campaign'])->toBe(4)
144+
->and((int) $groups[0]['min_price_last_sale_at_range_before-campaign'])->toBe(3)
145+
->and((int) $groups[0]['max_price_last_sale_at_range_before-campaign'])->toBe(1500)
146+
->and((int) $groups[0]['count_price_last_sale_at_range_before-campaign'])->toBe(4)
147+
->and((int) $groups[1]['count_last_sale_at_range_after-campaign'])->toBe(6)
148+
->and((int) $groups[1]['min_price_last_sale_at_range_after-campaign'])->toBe(6)
149+
->and((int) $groups[1]['max_price_last_sale_at_range_after-campaign'])->toBe(350)
150+
->and((int) $groups[1]['count_price_last_sale_at_range_after-campaign'])->toBe(6);
116151
});

0 commit comments

Comments
 (0)