Skip to content

Commit 2122bb9

Browse files
Spamerczclaude
andcommitted
feat(aggregation): add missing aggregation
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 5314aaa commit 2122bb9

3 files changed

Lines changed: 146 additions & 0 deletions

File tree

doc/03-aggregation-objects.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,16 @@ new \Spameri\ElasticQuery\Aggregation\DateRange(
336336
);
337337
```
338338

339+
##### Missing Aggregation
340+
Single bucket containing documents missing a field value.
341+
- Class: `\Spameri\ElasticQuery\Aggregation\Missing`
342+
- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-missing-aggregation.html)
343+
- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Aggregation/Missing.php)
344+
345+
```php
346+
new \Spameri\ElasticQuery\Aggregation\Missing(field: 'price');
347+
```
348+
339349
##### Nested Aggregation
340350
Aggregates on nested documents.
341351
- Class: `\Spameri\ElasticQuery\Aggregation\Nested`

src/Aggregation/Missing.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Spameri\ElasticQuery\Aggregation;
6+
7+
/**
8+
* @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-missing-aggregation.html
9+
*/
10+
class Missing implements \Spameri\ElasticQuery\Aggregation\LeafAggregationInterface
11+
{
12+
13+
public function __construct(
14+
private string $field,
15+
)
16+
{
17+
}
18+
19+
20+
public function key(): string
21+
{
22+
return 'missing_' . $this->field;
23+
}
24+
25+
26+
/**
27+
* @return array<string, array<string, string>>
28+
*/
29+
public function toArray(): array
30+
{
31+
return [
32+
'missing' => [
33+
'field' => $this->field,
34+
],
35+
];
36+
}
37+
38+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SpameriTests\ElasticQuery\Aggregation;
4+
5+
require_once __DIR__ . '/../../bootstrap.php';
6+
7+
8+
class Missing extends \Tester\TestCase
9+
{
10+
11+
private const INDEX = 'spameri_test_aggregation_missing';
12+
13+
14+
public function setUp(): void
15+
{
16+
$ch = \curl_init();
17+
\curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX);
18+
\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1);
19+
\curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'PUT');
20+
\curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
21+
22+
\curl_exec($ch);
23+
}
24+
25+
26+
public function testToArray(): void
27+
{
28+
$missing = new \Spameri\ElasticQuery\Aggregation\Missing('price');
29+
30+
$array = $missing->toArray();
31+
32+
\Tester\Assert::same('price', $array['missing']['field']);
33+
}
34+
35+
36+
public function testKey(): void
37+
{
38+
$missing = new \Spameri\ElasticQuery\Aggregation\Missing('price');
39+
40+
\Tester\Assert::same('missing_price', $missing->key());
41+
}
42+
43+
44+
public function testCreate(): void
45+
{
46+
$missing = new \Spameri\ElasticQuery\Aggregation\Missing('price');
47+
48+
$elasticQuery = new \Spameri\ElasticQuery\ElasticQuery();
49+
$elasticQuery->aggregation()->add(
50+
new \Spameri\ElasticQuery\Aggregation\LeafAggregationCollection(
51+
'no_price',
52+
null,
53+
$missing,
54+
),
55+
);
56+
57+
$document = new \Spameri\ElasticQuery\Document(
58+
self::INDEX,
59+
new \Spameri\ElasticQuery\Document\Body\Plain(
60+
$elasticQuery->toArray(),
61+
),
62+
);
63+
64+
$ch = \curl_init();
65+
\curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . $document->index . '/_search');
66+
\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1);
67+
\curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'GET');
68+
\curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
69+
\curl_setopt(
70+
$ch,
71+
\CURLOPT_POSTFIELDS,
72+
\json_encode($document->toArray()['body']),
73+
);
74+
75+
\Tester\Assert::noError(static function () use ($ch): void {
76+
$response = \curl_exec($ch);
77+
$resultMapper = new \Spameri\ElasticQuery\Response\ResultMapper();
78+
/** @var \Spameri\ElasticQuery\Response\ResultSearch $result */
79+
$result = $resultMapper->map(\json_decode($response, true));
80+
\Tester\Assert::type(\Spameri\ElasticQuery\Response\ResultSearch::class, $result);
81+
});
82+
}
83+
84+
85+
public function tearDown(): void
86+
{
87+
$ch = \curl_init();
88+
\curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX);
89+
\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1);
90+
\curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE');
91+
\curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
92+
93+
\curl_exec($ch);
94+
}
95+
96+
}
97+
98+
(new Missing())->run();

0 commit comments

Comments
 (0)