Skip to content

Commit 451b311

Browse files
committed
Add geo shape query support
1 parent 60686cc commit 451b311

File tree

6 files changed

+247
-0
lines changed

6 files changed

+247
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ Available methods are listed below:
112112
* [exists](docs/term-queries.md#exists)
113113
* [fuzzy](docs/term-queries.md#fuzzy)
114114
* [geoDistance](docs/geo-queries.md#geo-distance)
115+
* [geoShape](docs/geo-queries.md#geo-shape)
115116
* [ids](docs/term-queries.md#ids)
116117
* [matchAll](docs/full-text-queries.md#match-all)
117118
* [matchNone](docs/full-text-queries.md#match-none)

docs/geo-queries.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Geo Queries
22

33
* [Geo-Distance](#geo-distance)
4+
* [Geo-Shape](#geo-shape)
45

56
## Geo-Distance
67

@@ -126,3 +127,76 @@ $query = Query::geoDistance()
126127

127128
$searchResult = Store::searchQuery($query)->execute();
128129
```
130+
131+
## Geo-Shape
132+
133+
You can use `Elastic\ScoutDriverPlus\Support\Query::geoShape()` to build a [geo-shape query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-shape-query.html#query-dsl-geo-shape-query):
134+
135+
```php
136+
$query = Query::geoShape()
137+
->field('location')
138+
->shape('envelope', [[13.0, 53.0], [14.0, 52.0]])
139+
->relation('within');
140+
141+
$searchResult = Store::searchQuery($query)->execute();
142+
```
143+
144+
Available methods:
145+
146+
* [field](#geo-shape-field)
147+
* [relation](#geo-shape-relation)
148+
* [shape](#geo-shape-shape)
149+
* [ignoreUnmapped](#geo-shape-ignore-unmapped)
150+
151+
### <a name="geo-shape-field"></a> field
152+
153+
Use `field` to specify the field, which represents a geo field:
154+
155+
```php
156+
$query = Query::geoShape()
157+
->field('location')
158+
->shape('envelope', [[13.0, 53.0], [14.0, 52.0]])
159+
->relation('within');
160+
161+
$searchResult = Store::searchQuery($query)->execute();
162+
```
163+
164+
### <a name="geo-shape-relation"></a> relation
165+
166+
`relation` [defines a spatial relation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-shape-query.html#geo-shape-spatial-relations) when searching a geo field:
167+
168+
```php
169+
$query = Query::geoShape()
170+
->field('location')
171+
->shape('envelope', [[13.0, 53.0], [14.0, 52.0]])
172+
->relation('within');
173+
174+
$searchResult = Store::searchQuery($query)->execute();
175+
```
176+
177+
### <a name="geo-shape-shape"></a> shape
178+
179+
Use `shape` to define a [GeoJSON](https://geojson.org) representation of a shape:
180+
181+
```php
182+
$query = Query::geoShape()
183+
->field('location')
184+
->shape('envelope', [[13.0, 53.0], [14.0, 52.0]])
185+
->relation('within');
186+
187+
$searchResult = Store::searchQuery($query)->execute();
188+
```
189+
190+
### <a name="geo-shape-ignore-unmapped"></a> ignoreUnmapped
191+
192+
You can use `ignoreUnmapped` to query [multiple indexes which might have different mappings](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-shape-query.html#_ignore_unmapped_4):
193+
194+
```php
195+
$query = Query::geoShape()
196+
->field('location')
197+
->shape('envelope', [[13.0, 53.0], [14.0, 52.0]])
198+
->relation('within')
199+
->ignoreUnmapped(true);
200+
201+
$searchResult = Store::searchQuery($query)->execute();
202+
```
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Elastic\ScoutDriverPlus\Builders;
4+
5+
use Elastic\ScoutDriverPlus\QueryParameters\ParameterCollection;
6+
use Elastic\ScoutDriverPlus\QueryParameters\Shared\FieldParameter;
7+
use Elastic\ScoutDriverPlus\QueryParameters\Shared\IgnoreUnmappedParameter;
8+
use Elastic\ScoutDriverPlus\QueryParameters\Shared\RelationParameter;
9+
use Elastic\ScoutDriverPlus\QueryParameters\Transformers\GroupedArrayTransformer;
10+
use Elastic\ScoutDriverPlus\QueryParameters\Validators\AllOfValidator;
11+
12+
final class GeoShapeQueryBuilder extends AbstractParameterizedQueryBuilder
13+
{
14+
use FieldParameter;
15+
use RelationParameter;
16+
use IgnoreUnmappedParameter;
17+
18+
protected string $type = 'geo_shape';
19+
20+
public function __construct()
21+
{
22+
$this->parameters = new ParameterCollection();
23+
$this->parameterValidator = new AllOfValidator(['shape', 'relation']);
24+
$this->parameterTransformer = new GroupedArrayTransformer('field');
25+
}
26+
27+
public function shape(string $type, array $coordinates): self
28+
{
29+
$this->parameters->put('shape', compact('type', 'coordinates'));
30+
return $this;
31+
}
32+
}

src/Support/Query.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Elastic\ScoutDriverPlus\Builders\ExistsQueryBuilder;
77
use Elastic\ScoutDriverPlus\Builders\FuzzyQueryBuilder;
88
use Elastic\ScoutDriverPlus\Builders\GeoDistanceQueryBuilder;
9+
use Elastic\ScoutDriverPlus\Builders\GeoShapeQueryBuilder;
910
use Elastic\ScoutDriverPlus\Builders\IdsQueryBuilder;
1011
use Elastic\ScoutDriverPlus\Builders\MatchAllQueryBuilder;
1112
use Elastic\ScoutDriverPlus\Builders\MatchNoneQueryBuilder;
@@ -115,4 +116,9 @@ public static function geoDistance(): GeoDistanceQueryBuilder
115116
{
116117
return new GeoDistanceQueryBuilder();
117118
}
119+
120+
public static function geoShape(): GeoShapeQueryBuilder
121+
{
122+
return new GeoShapeQueryBuilder();
123+
}
118124
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Integration\Queries;
4+
5+
use Elastic\ScoutDriverPlus\Support\Query;
6+
use Elastic\ScoutDriverPlus\Tests\App\Store;
7+
use Elastic\ScoutDriverPlus\Tests\Integration\TestCase;
8+
9+
/**
10+
* @covers \Elastic\ScoutDriverPlus\Builders\AbstractParameterizedQueryBuilder
11+
* @covers \Elastic\ScoutDriverPlus\Builders\GeoShapeQuery
12+
* @covers \Elastic\ScoutDriverPlus\Engine
13+
* @covers \Elastic\ScoutDriverPlus\Factories\LazyModelFactory
14+
* @covers \Elastic\ScoutDriverPlus\Factories\ModelFactory
15+
* @covers \Elastic\ScoutDriverPlus\Support\Query
16+
*
17+
* @uses \Elastic\ScoutDriverPlus\Builders\DatabaseQueryBuilder
18+
* @uses \Elastic\ScoutDriverPlus\Builders\SearchParametersBuilder
19+
* @uses \Elastic\ScoutDriverPlus\Decorators\Hit
20+
* @uses \Elastic\ScoutDriverPlus\Decorators\SearchResult
21+
* @uses \Elastic\ScoutDriverPlus\Factories\DocumentFactory
22+
* @uses \Elastic\ScoutDriverPlus\Factories\ParameterFactory
23+
* @uses \Elastic\ScoutDriverPlus\Factories\RoutingFactory
24+
* @uses \Elastic\ScoutDriverPlus\QueryParameters\ParameterCollection
25+
* @uses \Elastic\ScoutDriverPlus\QueryParameters\Transformers\GroupedArrayTransformer
26+
* @uses \Elastic\ScoutDriverPlus\QueryParameters\Validators\AllOfValidator
27+
* @uses \Elastic\ScoutDriverPlus\Searchable
28+
*/
29+
final class GeoShapeQueryTest extends TestCase
30+
{
31+
public function test_models_can_be_found_using_field_and_shape_and_relation(): void
32+
{
33+
// additional mixin
34+
factory(Store::class, rand(2, 10))->create([
35+
'lat' => 20,
36+
'lon' => 20,
37+
]);
38+
39+
$target = factory(Store::class)->create([
40+
'lat' => 10,
41+
'lon' => 10,
42+
]);
43+
44+
$query = Query::geoShape()
45+
->field('location')
46+
->shape('polygon', [[[0, 0], [15, 0], [15, 15], [0, 15]]])
47+
->relation('within');
48+
49+
$found = Store::searchQuery($query)->execute();
50+
51+
$this->assertFoundModel($target, $found);
52+
}
53+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Elastic\ScoutDriverPlus\Tests\Unit\Builders;
4+
5+
use Elastic\ScoutDriverPlus\Builders\GeoShapeQueryBuilder;
6+
use Elastic\ScoutDriverPlus\Exceptions\QueryBuilderValidationException;
7+
use PHPUnit\Framework\TestCase;
8+
9+
/**
10+
* @covers \Elastic\ScoutDriverPlus\Builders\AbstractParameterizedQueryBuilder
11+
* @covers \Elastic\ScoutDriverPlus\Builders\GeoShapeQueryBuilder
12+
*
13+
* @uses \Elastic\ScoutDriverPlus\QueryParameters\ParameterCollection
14+
* @uses \Elastic\ScoutDriverPlus\QueryParameters\Transformers\GroupedArrayTransformer
15+
* @uses \Elastic\ScoutDriverPlus\QueryParameters\Validators\AllOfValidator
16+
*/
17+
final class GeoShapeQueryBuilderTest extends TestCase
18+
{
19+
private GeoShapeQueryBuilder $builder;
20+
21+
protected function setUp(): void
22+
{
23+
parent::setUp();
24+
25+
$this->builder = new GeoShapeQueryBuilder();
26+
}
27+
28+
public function test_exception_is_thrown_when_required_parameters_are_not_specified(): void
29+
{
30+
$this->expectException(QueryBuilderValidationException::class);
31+
$this->builder->buildQuery();
32+
}
33+
34+
public function test_query_with_field_and_shape_and_relation_can_be_built(): void
35+
{
36+
$expected = [
37+
'geo_shape' => [
38+
'location' => [
39+
'shape' => [
40+
'type' => 'envelope',
41+
'coordinates' => [[13.0, 53.0], [14.0, 52.0]],
42+
],
43+
'relation' => 'within',
44+
],
45+
],
46+
];
47+
48+
$actual = $this->builder
49+
->field('location')
50+
->shape('envelope', [[13.0, 53.0], [14.0, 52.0]])
51+
->relation('within')
52+
->buildQuery();
53+
54+
$this->assertSame($expected, $actual);
55+
}
56+
57+
public function test_query_with_field_and_shape_and_relation_and_ignore_unmapped_can_be_built(): void
58+
{
59+
$expected = [
60+
'geo_shape' => [
61+
'location' => [
62+
'shape' => [
63+
'type' => 'envelope',
64+
'coordinates' => [[13.0, 53.0], [14.0, 52.0]],
65+
],
66+
'relation' => 'within',
67+
'ignore_unmapped' => true,
68+
],
69+
],
70+
];
71+
72+
$actual = $this->builder
73+
->field('location')
74+
->shape('envelope', [[13.0, 53.0], [14.0, 52.0]])
75+
->relation('within')
76+
->ignoreUnmapped(true)
77+
->buildQuery();
78+
79+
$this->assertSame($expected, $actual);
80+
}
81+
}

0 commit comments

Comments
 (0)