Skip to content

Commit

Permalink
[SPATIAL] New ShapeQueryBuilder for querying indexed cartesian geomet…
Browse files Browse the repository at this point in the history
…ry (#45108)

This commit adds a new ShapeQueryBuilder to the xpack spatial module for
querying arbitrary Cartesian geometries indexed using the new shape field
type.

The query builder extends AbstractGeometryQueryBuilder and leverages the
ShapeQueryProcessor added in the previous field mapper commit.

Tests are provided in ShapeQueryTests in the same manner as
GeoShapeQueryTests and docs are updated to explain how the query works.
  • Loading branch information
nknize authored Aug 8, 2019
1 parent f114ef6 commit c89b66a
Show file tree
Hide file tree
Showing 15 changed files with 1,296 additions and 17 deletions.
3 changes: 3 additions & 0 deletions docs/reference/mapping/types/shape.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ with arbitrary `x, y` cartesian shapes such as rectangles and polygons. It can b
used to index and query geometries whose coordinates fall in a 2-dimensional planar
coordinate system.

You can query documents using this type using
<<query-dsl-shape-query,shape Query>>.

[[shape-mapping-options]]
[float]
==== Mapping Options
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/query-dsl.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ include::query-dsl/full-text-queries.asciidoc[]

include::query-dsl/geo-queries.asciidoc[]

include::query-dsl/shape-queries.asciidoc[]

include::query-dsl/joining-queries.asciidoc[]

include::query-dsl/match-all-query.asciidoc[]
Expand Down
18 changes: 18 additions & 0 deletions docs/reference/query-dsl/shape-queries.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[[shape-queries]]
[role="xpack"]
[testenv="basic"]
== Shape queries

Like <<geo-shape,`geo_shape`>> Elasticsearch supports the ability to index
arbitrary two dimension (non Geospatial) geometries making it possible to
map out virtual worlds, sporting venues, theme parks, and CAD diagrams. The
<<shape,`shape`>> field type supports points, lines, polygons, multi-polygons,
envelope, etc.

The queries in this group are:

<<query-dsl-shape-query,`shape`>> query::
Finds documents with shapes that either intersect, are within, or do not
intersect a specified shape.

include::shape-query.asciidoc[]
149 changes: 149 additions & 0 deletions docs/reference/query-dsl/shape-query.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
[[query-dsl-shape-query]]
[role="xpack"]
[testenv="basic"]
=== Shape query
++++
<titleabbrev>Shape</titleabbrev>
++++

Queries documents that contain fields indexed using the `shape` type.

Requires the <<shape,`shape` Mapping>>.

The query supports two ways of defining the target shape, either by
providing a whole shape definition, or by referencing the name, or id, of a shape
pre-indexed in another index. Both formats are defined below with
examples.

==== Inline Shape Definition

Similar to the `geo_shape` query, the `shape` query uses
http://www.geojson.org[GeoJSON] or
https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry[Well Known Text]
(WKT) to represent shapes.

Given the following index:

[source,js]
--------------------------------------------------
PUT /example
{
"mappings": {
"properties": {
"geometry": {
"type": "shape"
}
}
}
}
POST /example/_doc?refresh
{
"name": "Lucky Landing",
"location": {
"type": "point",
"coordinates": [1355.400544, 5255.530286]
}
}
--------------------------------------------------
// CONSOLE
// TESTSETUP

The following query will find the point using the Elasticsearch's
`envelope` GeoJSON extension:

[source,js]
--------------------------------------------------
GET /example/_search
{
"query":{
"shape": {
"geometry": {
"shape": {
"type": "envelope",
"coordinates" : [[1355.0, 5355.0], [1400.0, 5200.0]]
},
"relation": "within"
}
}
}
}
--------------------------------------------------
// CONSOLE

==== Pre-Indexed Shape

The Query also supports using a shape which has already been indexed in
another index. This is particularly useful for when
you have a pre-defined list of shapes which are useful to your
application and you want to reference this using a logical name (for
example 'New Zealand') rather than having to provide their coordinates
each time. In this situation it is only necessary to provide:

* `id` - The ID of the document that containing the pre-indexed shape.
* `index` - Name of the index where the pre-indexed shape is. Defaults
to 'shapes'.
* `path` - The field specified as path containing the pre-indexed shape.
Defaults to 'shape'.
* `routing` - The routing of the shape document if required.

The following is an example of using the Filter with a pre-indexed
shape:

[source,js]
--------------------------------------------------
PUT /shapes
{
"mappings": {
"properties": {
"geometry": {
"type": "shape"
}
}
}
}
PUT /shapes/_doc/footprint
{
"geometry": {
"type": "envelope",
"coordinates" : [[1355.0, 5355.0], [1400.0, 5200.0]]
}
}
GET /example/_search
{
"query": {
"shape": {
"geometry": {
"indexed_shape": {
"index": "shapes",
"id": "footprint",
"path": "geometry"
}
}
}
}
}
--------------------------------------------------
// CONSOLE

==== Spatial Relations

The following is a complete list of spatial relation operators available:

* `INTERSECTS` - (default) Return all documents whose `geo_shape` field
intersects the query geometry.
* `DISJOINT` - Return all documents whose `geo_shape` field
has nothing in common with the query geometry.
* `WITHIN` - Return all documents whose `geo_shape` field
is within the query geometry.

[float]
==== Ignore Unmapped

When set to `true` the `ignore_unmapped` option will ignore an unmapped field
and will not match any documents for this query. This can be useful when
querying multiple indexes which might have different mappings. When set to
`false` (the default value) the query will throw an exception if the field
is not mapped.
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws
}

/** local class that encapsulates xcontent parsed shape parameters */
protected abstract static class ParsedShapeQueryParams {
protected abstract static class ParsedGeometryQueryParams {
public String fieldName;
public ShapeRelation relation;
public ShapeBuilder shape;
Expand All @@ -562,7 +562,7 @@ protected abstract static class ParsedShapeQueryParams {
protected abstract boolean parseXContentField(XContentParser parser) throws IOException;
}

public static ParsedShapeQueryParams parsedParamsFromXContent(XContentParser parser, ParsedShapeQueryParams params)
public static ParsedGeometryQueryParams parsedParamsFromXContent(XContentParser parser, ParsedGeometryQueryParams params)
throws IOException {
String fieldName = null;
XContentParser.Token token;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ protected GeoShapeQueryBuilder doRewrite(QueryRewriteContext queryRewriteContext
return builder;
}

private static class ParsedGeoShapeQueryParams extends ParsedShapeQueryParams {
private static class ParsedGeoShapeQueryParams extends ParsedGeometryQueryParams {
SpatialStrategy strategy;

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,15 @@ public static Circle randomCircle(boolean hasAlt) {
}

public static Line randomLine(boolean hasAlts) {
int size = ESTestCase.randomIntBetween(2, 10);
// we use nextPolygon because it guarantees no duplicate points
org.apache.lucene.geo.Polygon lucenePolygon = GeoTestUtil.nextPolygon();
int size = lucenePolygon.numPoints() - 1;
double[] lats = new double[size];
double[] lons = new double[size];
double[] alts = hasAlts ? new double[size] : null;
for (int i = 0; i < size; i++) {
lats[i] = randomLat();
lons[i] = randomLon();
lats[i] = lucenePolygon.getPolyLat(i);
lons[i] = lucenePolygon.getPolyLon(i);
if (hasAlts) {
alts[i] = randomAlt();
}
Expand Down Expand Up @@ -96,11 +98,12 @@ public static Polygon randomPolygon(boolean hasAlt) {
org.apache.lucene.geo.Polygon[] luceneHoles = lucenePolygon.getHoles();
List<LinearRing> holes = new ArrayList<>();
for (int i = 0; i < lucenePolygon.numHoles(); i++) {
holes.add(linearRing(luceneHoles[i], hasAlt));
org.apache.lucene.geo.Polygon poly = luceneHoles[i];
holes.add(linearRing(poly.getPolyLats(), poly.getPolyLons(), hasAlt));
}
return new Polygon(linearRing(lucenePolygon, hasAlt), holes);
return new Polygon(linearRing(lucenePolygon.getPolyLats(), lucenePolygon.getPolyLons(), hasAlt), holes);
}
return new Polygon(linearRing(lucenePolygon, hasAlt));
return new Polygon(linearRing(lucenePolygon.getPolyLats(), lucenePolygon.getPolyLons(), hasAlt));
}


Expand All @@ -113,12 +116,11 @@ private static double[] randomAltRing(int size) {
return alts;
}

private static LinearRing linearRing(org.apache.lucene.geo.Polygon polygon, boolean generateAlts) {
public static LinearRing linearRing(double[] lats, double[] lons, boolean generateAlts) {
if (generateAlts) {
return new LinearRing(polygon.getPolyLats(), polygon.getPolyLons(), randomAltRing(polygon.numPoints()));
} else {
return new LinearRing(polygon.getPolyLats(), polygon.getPolyLons());
return new LinearRing(lats, lons, randomAltRing(lats.length));
}
return new LinearRing(lats, lons);
}

public static Rectangle randomRectangle() {
Expand Down Expand Up @@ -170,7 +172,7 @@ public static Geometry randomGeometry(boolean hasAlt) {
return randomGeometry(0, hasAlt);
}

private static Geometry randomGeometry(int level, boolean hasAlt) {
protected static Geometry randomGeometry(int level, boolean hasAlt) {
@SuppressWarnings("unchecked") Function<Boolean, Geometry> geometry = ESTestCase.randomFrom(
GeometryTestUtils::randomCircle,
GeometryTestUtils::randomLine,
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugin/spatial/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ dependencies {
}
}

licenseHeaders {
// This class was sourced from apache lucene's sandbox module tests
excludes << 'org/apache/lucene/geo/XShapeTestUtil.java'
}

// xpack modules are installed in real clusters as the meta plugin, so
// installing them as individual plugins for integ tests doesn't make sense,
// so we disable integ tests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,21 @@
import org.elasticsearch.plugins.ActionPlugin;
import org.elasticsearch.plugins.MapperPlugin;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.SearchPlugin;
import org.elasticsearch.xpack.core.action.XPackInfoFeatureAction;
import org.elasticsearch.xpack.core.action.XPackUsageFeatureAction;
import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper;
import org.elasticsearch.xpack.spatial.index.query.ShapeQueryBuilder;

import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class SpatialPlugin extends Plugin implements ActionPlugin, MapperPlugin {
import static java.util.Collections.singletonList;

public class SpatialPlugin extends Plugin implements ActionPlugin, MapperPlugin, SearchPlugin {

public SpatialPlugin(Settings settings) {
}
Expand All @@ -40,4 +44,9 @@ public Map<String, Mapper.TypeParser> getMappers() {
mappers.put(ShapeFieldMapper.CONTENT_TYPE, new ShapeFieldMapper.TypeParser());
return Collections.unmodifiableMap(mappers);
}

@Override
public List<QuerySpec<?>> getQueries() {
return singletonList(new QuerySpec<>(ShapeQueryBuilder.NAME, ShapeQueryBuilder::new, ShapeQueryBuilder::fromXContent));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ public SpatialUsageTransportAction(TransportService transportService, ClusterSer
@Override
protected void masterOperation(Task task, XPackUsageRequest request, ClusterState state,
ActionListener<XPackUsageFeatureResponse> listener) {
SpatialFeatureSetUsage usage =
new SpatialFeatureSetUsage(licenseState.isSpatialAllowed(), true);
SpatialFeatureSetUsage usage = new SpatialFeatureSetUsage(licenseState.isSpatialAllowed(), true);
listener.onResponse(new XPackUsageFeatureResponse(usage));
}
}
Loading

0 comments on commit c89b66a

Please sign in to comment.