From 451b311d4a8d2ee94643c3823d2a26c310889e6e Mon Sep 17 00:00:00 2001 From: Ivan Babenko Date: Thu, 13 Jun 2024 09:43:19 +0200 Subject: [PATCH] Add geo shape query support --- README.md | 1 + docs/geo-queries.md | 74 +++++++++++++++++ src/Builders/GeoShapeQueryBuilder.php | 32 ++++++++ src/Support/Query.php | 6 ++ .../Integration/Queries/GeoShapeQueryTest.php | 53 ++++++++++++ .../Builders/GeoShapeQueryBuilderTest.php | 81 +++++++++++++++++++ 6 files changed, 247 insertions(+) create mode 100644 src/Builders/GeoShapeQueryBuilder.php create mode 100644 tests/Integration/Queries/GeoShapeQueryTest.php create mode 100644 tests/Unit/Builders/GeoShapeQueryBuilderTest.php diff --git a/README.md b/README.md index 8631e9c..7929f68 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ Available methods are listed below: * [exists](docs/term-queries.md#exists) * [fuzzy](docs/term-queries.md#fuzzy) * [geoDistance](docs/geo-queries.md#geo-distance) +* [geoShape](docs/geo-queries.md#geo-shape) * [ids](docs/term-queries.md#ids) * [matchAll](docs/full-text-queries.md#match-all) * [matchNone](docs/full-text-queries.md#match-none) diff --git a/docs/geo-queries.md b/docs/geo-queries.md index 12d9034..150af4c 100644 --- a/docs/geo-queries.md +++ b/docs/geo-queries.md @@ -1,6 +1,7 @@ # Geo Queries * [Geo-Distance](#geo-distance) +* [Geo-Shape](#geo-shape) ## Geo-Distance @@ -126,3 +127,76 @@ $query = Query::geoDistance() $searchResult = Store::searchQuery($query)->execute(); ``` + +## Geo-Shape + +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): + +```php +$query = Query::geoShape() + ->field('location') + ->shape('envelope', [[13.0, 53.0], [14.0, 52.0]]) + ->relation('within'); + +$searchResult = Store::searchQuery($query)->execute(); +``` + +Available methods: + +* [field](#geo-shape-field) +* [relation](#geo-shape-relation) +* [shape](#geo-shape-shape) +* [ignoreUnmapped](#geo-shape-ignore-unmapped) + +### field + +Use `field` to specify the field, which represents a geo field: + +```php +$query = Query::geoShape() + ->field('location') + ->shape('envelope', [[13.0, 53.0], [14.0, 52.0]]) + ->relation('within'); + +$searchResult = Store::searchQuery($query)->execute(); +``` + +### relation + +`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: + +```php +$query = Query::geoShape() + ->field('location') + ->shape('envelope', [[13.0, 53.0], [14.0, 52.0]]) + ->relation('within'); + +$searchResult = Store::searchQuery($query)->execute(); +``` + +### shape + +Use `shape` to define a [GeoJSON](https://geojson.org) representation of a shape: + +```php +$query = Query::geoShape() + ->field('location') + ->shape('envelope', [[13.0, 53.0], [14.0, 52.0]]) + ->relation('within'); + +$searchResult = Store::searchQuery($query)->execute(); +``` + +### ignoreUnmapped + +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): + +```php +$query = Query::geoShape() + ->field('location') + ->shape('envelope', [[13.0, 53.0], [14.0, 52.0]]) + ->relation('within') + ->ignoreUnmapped(true); + +$searchResult = Store::searchQuery($query)->execute(); +``` diff --git a/src/Builders/GeoShapeQueryBuilder.php b/src/Builders/GeoShapeQueryBuilder.php new file mode 100644 index 0000000..e208e48 --- /dev/null +++ b/src/Builders/GeoShapeQueryBuilder.php @@ -0,0 +1,32 @@ +parameters = new ParameterCollection(); + $this->parameterValidator = new AllOfValidator(['shape', 'relation']); + $this->parameterTransformer = new GroupedArrayTransformer('field'); + } + + public function shape(string $type, array $coordinates): self + { + $this->parameters->put('shape', compact('type', 'coordinates')); + return $this; + } +} diff --git a/src/Support/Query.php b/src/Support/Query.php index 3ce4be6..b305e1c 100644 --- a/src/Support/Query.php +++ b/src/Support/Query.php @@ -6,6 +6,7 @@ use Elastic\ScoutDriverPlus\Builders\ExistsQueryBuilder; use Elastic\ScoutDriverPlus\Builders\FuzzyQueryBuilder; use Elastic\ScoutDriverPlus\Builders\GeoDistanceQueryBuilder; +use Elastic\ScoutDriverPlus\Builders\GeoShapeQueryBuilder; use Elastic\ScoutDriverPlus\Builders\IdsQueryBuilder; use Elastic\ScoutDriverPlus\Builders\MatchAllQueryBuilder; use Elastic\ScoutDriverPlus\Builders\MatchNoneQueryBuilder; @@ -115,4 +116,9 @@ public static function geoDistance(): GeoDistanceQueryBuilder { return new GeoDistanceQueryBuilder(); } + + public static function geoShape(): GeoShapeQueryBuilder + { + return new GeoShapeQueryBuilder(); + } } diff --git a/tests/Integration/Queries/GeoShapeQueryTest.php b/tests/Integration/Queries/GeoShapeQueryTest.php new file mode 100644 index 0000000..f33536e --- /dev/null +++ b/tests/Integration/Queries/GeoShapeQueryTest.php @@ -0,0 +1,53 @@ +create([ + 'lat' => 20, + 'lon' => 20, + ]); + + $target = factory(Store::class)->create([ + 'lat' => 10, + 'lon' => 10, + ]); + + $query = Query::geoShape() + ->field('location') + ->shape('polygon', [[[0, 0], [15, 0], [15, 15], [0, 15]]]) + ->relation('within'); + + $found = Store::searchQuery($query)->execute(); + + $this->assertFoundModel($target, $found); + } +} diff --git a/tests/Unit/Builders/GeoShapeQueryBuilderTest.php b/tests/Unit/Builders/GeoShapeQueryBuilderTest.php new file mode 100644 index 0000000..e05fc8f --- /dev/null +++ b/tests/Unit/Builders/GeoShapeQueryBuilderTest.php @@ -0,0 +1,81 @@ +builder = new GeoShapeQueryBuilder(); + } + + public function test_exception_is_thrown_when_required_parameters_are_not_specified(): void + { + $this->expectException(QueryBuilderValidationException::class); + $this->builder->buildQuery(); + } + + public function test_query_with_field_and_shape_and_relation_can_be_built(): void + { + $expected = [ + 'geo_shape' => [ + 'location' => [ + 'shape' => [ + 'type' => 'envelope', + 'coordinates' => [[13.0, 53.0], [14.0, 52.0]], + ], + 'relation' => 'within', + ], + ], + ]; + + $actual = $this->builder + ->field('location') + ->shape('envelope', [[13.0, 53.0], [14.0, 52.0]]) + ->relation('within') + ->buildQuery(); + + $this->assertSame($expected, $actual); + } + + public function test_query_with_field_and_shape_and_relation_and_ignore_unmapped_can_be_built(): void + { + $expected = [ + 'geo_shape' => [ + 'location' => [ + 'shape' => [ + 'type' => 'envelope', + 'coordinates' => [[13.0, 53.0], [14.0, 52.0]], + ], + 'relation' => 'within', + 'ignore_unmapped' => true, + ], + ], + ]; + + $actual = $this->builder + ->field('location') + ->shape('envelope', [[13.0, 53.0], [14.0, 52.0]]) + ->relation('within') + ->ignoreUnmapped(true) + ->buildQuery(); + + $this->assertSame($expected, $actual); + } +}