Skip to content

Commit 8e4d7d8

Browse files
Spamerczclaude
andcommitted
feat(query): add shape query
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ead03cd commit 8e4d7d8

3 files changed

Lines changed: 133 additions & 0 deletions

File tree

doc/02-query-objects.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,20 @@ new \Spameri\ElasticQuery\Query\GeoDistance(
441441
);
442442
```
443443

444+
##### Shape Query
445+
Same as `geo_shape` but for Cartesian `shape` fields (non-geographic plane).
446+
- Class: `\Spameri\ElasticQuery\Query\Shape`
447+
- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-shape-query.html)
448+
- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Shape.php)
449+
450+
```php
451+
new \Spameri\ElasticQuery\Query\Shape(
452+
field: 'geometry',
453+
shape: ['type' => 'envelope', 'coordinates' => [[0, 100], [100, 0]]],
454+
relation: 'intersects',
455+
);
456+
```
457+
444458
##### GeoShape Query
445459
Match `geo_shape`-indexed documents against an arbitrary geometry.
446460
- Class: `\Spameri\ElasticQuery\Query\GeoShape`

src/Query/Shape.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Spameri\ElasticQuery\Query;
6+
7+
/**
8+
* @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-shape-query.html
9+
*/
10+
class Shape implements \Spameri\ElasticQuery\Query\LeafQueryInterface
11+
{
12+
13+
/**
14+
* @param array<string, mixed> $shape
15+
*/
16+
public function __construct(
17+
private string $field,
18+
private array $shape,
19+
private string $relation = 'intersects',
20+
private bool|null $ignoreUnmapped = null,
21+
)
22+
{
23+
if ( ! \in_array($relation, ['intersects', 'disjoint', 'within', 'contains'], true)) {
24+
throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException(
25+
'Shape relation must be one of: intersects, disjoint, within, contains.',
26+
);
27+
}
28+
}
29+
30+
31+
public function key(): string
32+
{
33+
return 'shape_' . $this->field;
34+
}
35+
36+
37+
/**
38+
* @return array<string, array<string, array<string, mixed>>>
39+
*/
40+
public function toArray(): array
41+
{
42+
$inner = [
43+
'shape' => $this->shape,
44+
'relation' => $this->relation,
45+
];
46+
47+
if ($this->ignoreUnmapped !== null) {
48+
$inner['ignore_unmapped'] = $this->ignoreUnmapped;
49+
}
50+
51+
return [
52+
'shape' => [
53+
$this->field => $inner,
54+
],
55+
];
56+
}
57+
58+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SpameriTests\ElasticQuery\Query;
4+
5+
require_once __DIR__ . '/../../bootstrap.php';
6+
7+
8+
class Shape extends \Tester\TestCase
9+
{
10+
11+
private const INDEX = 'spameri_test_query_shape';
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+
$shape = new \Spameri\ElasticQuery\Query\Shape(
29+
field: 'geometry',
30+
shape: ['type' => 'envelope', 'coordinates' => [[0, 100], [100, 0]]],
31+
relation: 'intersects',
32+
);
33+
34+
$array = $shape->toArray();
35+
36+
\Tester\Assert::same('envelope', $array['shape']['geometry']['shape']['type']);
37+
}
38+
39+
40+
public function testKey(): void
41+
{
42+
$shape = new \Spameri\ElasticQuery\Query\Shape('geometry', ['type' => 'point', 'coordinates' => [0, 0]]);
43+
44+
\Tester\Assert::same('shape_geometry', $shape->key());
45+
}
46+
47+
48+
public function tearDown(): void
49+
{
50+
$ch = \curl_init();
51+
\curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX);
52+
\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1);
53+
\curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE');
54+
\curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
55+
56+
\curl_exec($ch);
57+
}
58+
59+
}
60+
61+
(new Shape())->run();

0 commit comments

Comments
 (0)