Skip to content

Commit

Permalink
Add geo shape query support
Browse files Browse the repository at this point in the history
  • Loading branch information
babenkoivan committed Jun 13, 2024
1 parent 60686cc commit 451b311
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
74 changes: 74 additions & 0 deletions docs/geo-queries.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Geo Queries

* [Geo-Distance](#geo-distance)
* [Geo-Shape](#geo-shape)

## Geo-Distance

Expand Down Expand Up @@ -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)

### <a name="geo-shape-field"></a> 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();
```

### <a name="geo-shape-relation"></a> 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();
```

### <a name="geo-shape-shape"></a> 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();
```

### <a name="geo-shape-ignore-unmapped"></a> 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();
```
32 changes: 32 additions & 0 deletions src/Builders/GeoShapeQueryBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php declare(strict_types=1);

namespace Elastic\ScoutDriverPlus\Builders;

use Elastic\ScoutDriverPlus\QueryParameters\ParameterCollection;
use Elastic\ScoutDriverPlus\QueryParameters\Shared\FieldParameter;
use Elastic\ScoutDriverPlus\QueryParameters\Shared\IgnoreUnmappedParameter;
use Elastic\ScoutDriverPlus\QueryParameters\Shared\RelationParameter;
use Elastic\ScoutDriverPlus\QueryParameters\Transformers\GroupedArrayTransformer;
use Elastic\ScoutDriverPlus\QueryParameters\Validators\AllOfValidator;

final class GeoShapeQueryBuilder extends AbstractParameterizedQueryBuilder
{
use FieldParameter;
use RelationParameter;
use IgnoreUnmappedParameter;

protected string $type = 'geo_shape';

public function __construct()
{
$this->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;
}
}
6 changes: 6 additions & 0 deletions src/Support/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -115,4 +116,9 @@ public static function geoDistance(): GeoDistanceQueryBuilder
{
return new GeoDistanceQueryBuilder();
}

public static function geoShape(): GeoShapeQueryBuilder
{
return new GeoShapeQueryBuilder();
}
}
53 changes: 53 additions & 0 deletions tests/Integration/Queries/GeoShapeQueryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php declare(strict_types=1);

namespace Integration\Queries;

use Elastic\ScoutDriverPlus\Support\Query;
use Elastic\ScoutDriverPlus\Tests\App\Store;
use Elastic\ScoutDriverPlus\Tests\Integration\TestCase;

/**
* @covers \Elastic\ScoutDriverPlus\Builders\AbstractParameterizedQueryBuilder
* @covers \Elastic\ScoutDriverPlus\Builders\GeoShapeQuery
* @covers \Elastic\ScoutDriverPlus\Engine
* @covers \Elastic\ScoutDriverPlus\Factories\LazyModelFactory
* @covers \Elastic\ScoutDriverPlus\Factories\ModelFactory
* @covers \Elastic\ScoutDriverPlus\Support\Query
*
* @uses \Elastic\ScoutDriverPlus\Builders\DatabaseQueryBuilder
* @uses \Elastic\ScoutDriverPlus\Builders\SearchParametersBuilder
* @uses \Elastic\ScoutDriverPlus\Decorators\Hit
* @uses \Elastic\ScoutDriverPlus\Decorators\SearchResult
* @uses \Elastic\ScoutDriverPlus\Factories\DocumentFactory
* @uses \Elastic\ScoutDriverPlus\Factories\ParameterFactory
* @uses \Elastic\ScoutDriverPlus\Factories\RoutingFactory
* @uses \Elastic\ScoutDriverPlus\QueryParameters\ParameterCollection
* @uses \Elastic\ScoutDriverPlus\QueryParameters\Transformers\GroupedArrayTransformer
* @uses \Elastic\ScoutDriverPlus\QueryParameters\Validators\AllOfValidator
* @uses \Elastic\ScoutDriverPlus\Searchable
*/
final class GeoShapeQueryTest extends TestCase
{
public function test_models_can_be_found_using_field_and_shape_and_relation(): void
{
// additional mixin
factory(Store::class, rand(2, 10))->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);
}
}
81 changes: 81 additions & 0 deletions tests/Unit/Builders/GeoShapeQueryBuilderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php declare(strict_types=1);

namespace Elastic\ScoutDriverPlus\Tests\Unit\Builders;

use Elastic\ScoutDriverPlus\Builders\GeoShapeQueryBuilder;
use Elastic\ScoutDriverPlus\Exceptions\QueryBuilderValidationException;
use PHPUnit\Framework\TestCase;

/**
* @covers \Elastic\ScoutDriverPlus\Builders\AbstractParameterizedQueryBuilder
* @covers \Elastic\ScoutDriverPlus\Builders\GeoShapeQueryBuilder
*
* @uses \Elastic\ScoutDriverPlus\QueryParameters\ParameterCollection
* @uses \Elastic\ScoutDriverPlus\QueryParameters\Transformers\GroupedArrayTransformer
* @uses \Elastic\ScoutDriverPlus\QueryParameters\Validators\AllOfValidator
*/
final class GeoShapeQueryBuilderTest extends TestCase
{
private GeoShapeQueryBuilder $builder;

protected function setUp(): void
{
parent::setUp();

$this->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);
}
}

0 comments on commit 451b311

Please sign in to comment.