Skip to content

Commit c89b66a

Browse files
authored
[SPATIAL] New ShapeQueryBuilder for querying indexed cartesian geometry (#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.
1 parent f114ef6 commit c89b66a

File tree

15 files changed

+1296
-17
lines changed

15 files changed

+1296
-17
lines changed

docs/reference/mapping/types/shape.asciidoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ with arbitrary `x, y` cartesian shapes such as rectangles and polygons. It can b
1111
used to index and query geometries whose coordinates fall in a 2-dimensional planar
1212
coordinate system.
1313

14+
You can query documents using this type using
15+
<<query-dsl-shape-query,shape Query>>.
16+
1417
[[shape-mapping-options]]
1518
[float]
1619
==== Mapping Options

docs/reference/query-dsl.asciidoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ include::query-dsl/full-text-queries.asciidoc[]
3535

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

38+
include::query-dsl/shape-queries.asciidoc[]
39+
3840
include::query-dsl/joining-queries.asciidoc[]
3941

4042
include::query-dsl/match-all-query.asciidoc[]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[[shape-queries]]
2+
[role="xpack"]
3+
[testenv="basic"]
4+
== Shape queries
5+
6+
Like <<geo-shape,`geo_shape`>> Elasticsearch supports the ability to index
7+
arbitrary two dimension (non Geospatial) geometries making it possible to
8+
map out virtual worlds, sporting venues, theme parks, and CAD diagrams. The
9+
<<shape,`shape`>> field type supports points, lines, polygons, multi-polygons,
10+
envelope, etc.
11+
12+
The queries in this group are:
13+
14+
<<query-dsl-shape-query,`shape`>> query::
15+
Finds documents with shapes that either intersect, are within, or do not
16+
intersect a specified shape.
17+
18+
include::shape-query.asciidoc[]
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
[[query-dsl-shape-query]]
2+
[role="xpack"]
3+
[testenv="basic"]
4+
=== Shape query
5+
++++
6+
<titleabbrev>Shape</titleabbrev>
7+
++++
8+
9+
Queries documents that contain fields indexed using the `shape` type.
10+
11+
Requires the <<shape,`shape` Mapping>>.
12+
13+
The query supports two ways of defining the target shape, either by
14+
providing a whole shape definition, or by referencing the name, or id, of a shape
15+
pre-indexed in another index. Both formats are defined below with
16+
examples.
17+
18+
==== Inline Shape Definition
19+
20+
Similar to the `geo_shape` query, the `shape` query uses
21+
http://www.geojson.org[GeoJSON] or
22+
https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry[Well Known Text]
23+
(WKT) to represent shapes.
24+
25+
Given the following index:
26+
27+
[source,js]
28+
--------------------------------------------------
29+
PUT /example
30+
{
31+
"mappings": {
32+
"properties": {
33+
"geometry": {
34+
"type": "shape"
35+
}
36+
}
37+
}
38+
}
39+
40+
POST /example/_doc?refresh
41+
{
42+
"name": "Lucky Landing",
43+
"location": {
44+
"type": "point",
45+
"coordinates": [1355.400544, 5255.530286]
46+
}
47+
}
48+
--------------------------------------------------
49+
// CONSOLE
50+
// TESTSETUP
51+
52+
The following query will find the point using the Elasticsearch's
53+
`envelope` GeoJSON extension:
54+
55+
[source,js]
56+
--------------------------------------------------
57+
GET /example/_search
58+
{
59+
"query":{
60+
"shape": {
61+
"geometry": {
62+
"shape": {
63+
"type": "envelope",
64+
"coordinates" : [[1355.0, 5355.0], [1400.0, 5200.0]]
65+
},
66+
"relation": "within"
67+
}
68+
}
69+
}
70+
}
71+
--------------------------------------------------
72+
// CONSOLE
73+
74+
==== Pre-Indexed Shape
75+
76+
The Query also supports using a shape which has already been indexed in
77+
another index. This is particularly useful for when
78+
you have a pre-defined list of shapes which are useful to your
79+
application and you want to reference this using a logical name (for
80+
example 'New Zealand') rather than having to provide their coordinates
81+
each time. In this situation it is only necessary to provide:
82+
83+
* `id` - The ID of the document that containing the pre-indexed shape.
84+
* `index` - Name of the index where the pre-indexed shape is. Defaults
85+
to 'shapes'.
86+
* `path` - The field specified as path containing the pre-indexed shape.
87+
Defaults to 'shape'.
88+
* `routing` - The routing of the shape document if required.
89+
90+
The following is an example of using the Filter with a pre-indexed
91+
shape:
92+
93+
[source,js]
94+
--------------------------------------------------
95+
PUT /shapes
96+
{
97+
"mappings": {
98+
"properties": {
99+
"geometry": {
100+
"type": "shape"
101+
}
102+
}
103+
}
104+
}
105+
106+
PUT /shapes/_doc/footprint
107+
{
108+
"geometry": {
109+
"type": "envelope",
110+
"coordinates" : [[1355.0, 5355.0], [1400.0, 5200.0]]
111+
}
112+
}
113+
114+
GET /example/_search
115+
{
116+
"query": {
117+
"shape": {
118+
"geometry": {
119+
"indexed_shape": {
120+
"index": "shapes",
121+
"id": "footprint",
122+
"path": "geometry"
123+
}
124+
}
125+
}
126+
}
127+
}
128+
--------------------------------------------------
129+
// CONSOLE
130+
131+
==== Spatial Relations
132+
133+
The following is a complete list of spatial relation operators available:
134+
135+
* `INTERSECTS` - (default) Return all documents whose `geo_shape` field
136+
intersects the query geometry.
137+
* `DISJOINT` - Return all documents whose `geo_shape` field
138+
has nothing in common with the query geometry.
139+
* `WITHIN` - Return all documents whose `geo_shape` field
140+
is within the query geometry.
141+
142+
[float]
143+
==== Ignore Unmapped
144+
145+
When set to `true` the `ignore_unmapped` option will ignore an unmapped field
146+
and will not match any documents for this query. This can be useful when
147+
querying multiple indexes which might have different mappings. When set to
148+
`false` (the default value) the query will throw an exception if the field
149+
is not mapped.

server/src/main/java/org/elasticsearch/index/query/AbstractGeometryQueryBuilder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws
544544
}
545545

546546
/** local class that encapsulates xcontent parsed shape parameters */
547-
protected abstract static class ParsedShapeQueryParams {
547+
protected abstract static class ParsedGeometryQueryParams {
548548
public String fieldName;
549549
public ShapeRelation relation;
550550
public ShapeBuilder shape;
@@ -562,7 +562,7 @@ protected abstract static class ParsedShapeQueryParams {
562562
protected abstract boolean parseXContentField(XContentParser parser) throws IOException;
563563
}
564564

565-
public static ParsedShapeQueryParams parsedParamsFromXContent(XContentParser parser, ParsedShapeQueryParams params)
565+
public static ParsedGeometryQueryParams parsedParamsFromXContent(XContentParser parser, ParsedGeometryQueryParams params)
566566
throws IOException {
567567
String fieldName = null;
568568
XContentParser.Token token;

server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ protected GeoShapeQueryBuilder doRewrite(QueryRewriteContext queryRewriteContext
237237
return builder;
238238
}
239239

240-
private static class ParsedGeoShapeQueryParams extends ParsedShapeQueryParams {
240+
private static class ParsedGeoShapeQueryParams extends ParsedGeometryQueryParams {
241241
SpatialStrategy strategy;
242242

243243
@Override

test/framework/src/main/java/org/elasticsearch/geo/GeometryTestUtils.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,15 @@ public static Circle randomCircle(boolean hasAlt) {
6161
}
6262

6363
public static Line randomLine(boolean hasAlts) {
64-
int size = ESTestCase.randomIntBetween(2, 10);
64+
// we use nextPolygon because it guarantees no duplicate points
65+
org.apache.lucene.geo.Polygon lucenePolygon = GeoTestUtil.nextPolygon();
66+
int size = lucenePolygon.numPoints() - 1;
6567
double[] lats = new double[size];
6668
double[] lons = new double[size];
6769
double[] alts = hasAlts ? new double[size] : null;
6870
for (int i = 0; i < size; i++) {
69-
lats[i] = randomLat();
70-
lons[i] = randomLon();
71+
lats[i] = lucenePolygon.getPolyLat(i);
72+
lons[i] = lucenePolygon.getPolyLon(i);
7173
if (hasAlts) {
7274
alts[i] = randomAlt();
7375
}
@@ -96,11 +98,12 @@ public static Polygon randomPolygon(boolean hasAlt) {
9698
org.apache.lucene.geo.Polygon[] luceneHoles = lucenePolygon.getHoles();
9799
List<LinearRing> holes = new ArrayList<>();
98100
for (int i = 0; i < lucenePolygon.numHoles(); i++) {
99-
holes.add(linearRing(luceneHoles[i], hasAlt));
101+
org.apache.lucene.geo.Polygon poly = luceneHoles[i];
102+
holes.add(linearRing(poly.getPolyLats(), poly.getPolyLons(), hasAlt));
100103
}
101-
return new Polygon(linearRing(lucenePolygon, hasAlt), holes);
104+
return new Polygon(linearRing(lucenePolygon.getPolyLats(), lucenePolygon.getPolyLons(), hasAlt), holes);
102105
}
103-
return new Polygon(linearRing(lucenePolygon, hasAlt));
106+
return new Polygon(linearRing(lucenePolygon.getPolyLats(), lucenePolygon.getPolyLons(), hasAlt));
104107
}
105108

106109

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

116-
private static LinearRing linearRing(org.apache.lucene.geo.Polygon polygon, boolean generateAlts) {
119+
public static LinearRing linearRing(double[] lats, double[] lons, boolean generateAlts) {
117120
if (generateAlts) {
118-
return new LinearRing(polygon.getPolyLats(), polygon.getPolyLons(), randomAltRing(polygon.numPoints()));
119-
} else {
120-
return new LinearRing(polygon.getPolyLats(), polygon.getPolyLons());
121+
return new LinearRing(lats, lons, randomAltRing(lats.length));
121122
}
123+
return new LinearRing(lats, lons);
122124
}
123125

124126
public static Rectangle randomRectangle() {
@@ -170,7 +172,7 @@ public static Geometry randomGeometry(boolean hasAlt) {
170172
return randomGeometry(0, hasAlt);
171173
}
172174

173-
private static Geometry randomGeometry(int level, boolean hasAlt) {
175+
protected static Geometry randomGeometry(int level, boolean hasAlt) {
174176
@SuppressWarnings("unchecked") Function<Boolean, Geometry> geometry = ESTestCase.randomFrom(
175177
GeometryTestUtils::randomCircle,
176178
GeometryTestUtils::randomLine,

x-pack/plugin/spatial/build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ dependencies {
1717
}
1818
}
1919

20+
licenseHeaders {
21+
// This class was sourced from apache lucene's sandbox module tests
22+
excludes << 'org/apache/lucene/geo/XShapeTestUtil.java'
23+
}
24+
2025
// xpack modules are installed in real clusters as the meta plugin, so
2126
// installing them as individual plugins for integ tests doesn't make sense,
2227
// so we disable integ tests

x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialPlugin.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,21 @@
1212
import org.elasticsearch.plugins.ActionPlugin;
1313
import org.elasticsearch.plugins.MapperPlugin;
1414
import org.elasticsearch.plugins.Plugin;
15+
import org.elasticsearch.plugins.SearchPlugin;
1516
import org.elasticsearch.xpack.core.action.XPackInfoFeatureAction;
1617
import org.elasticsearch.xpack.core.action.XPackUsageFeatureAction;
1718
import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper;
19+
import org.elasticsearch.xpack.spatial.index.query.ShapeQueryBuilder;
1820

1921
import java.util.Arrays;
2022
import java.util.Collections;
2123
import java.util.LinkedHashMap;
2224
import java.util.List;
2325
import java.util.Map;
2426

25-
public class SpatialPlugin extends Plugin implements ActionPlugin, MapperPlugin {
27+
import static java.util.Collections.singletonList;
28+
29+
public class SpatialPlugin extends Plugin implements ActionPlugin, MapperPlugin, SearchPlugin {
2630

2731
public SpatialPlugin(Settings settings) {
2832
}
@@ -40,4 +44,9 @@ public Map<String, Mapper.TypeParser> getMappers() {
4044
mappers.put(ShapeFieldMapper.CONTENT_TYPE, new ShapeFieldMapper.TypeParser());
4145
return Collections.unmodifiableMap(mappers);
4246
}
47+
48+
@Override
49+
public List<QuerySpec<?>> getQueries() {
50+
return singletonList(new QuerySpec<>(ShapeQueryBuilder.NAME, ShapeQueryBuilder::new, ShapeQueryBuilder::fromXContent));
51+
}
4352
}

x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialUsageTransportAction.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ public SpatialUsageTransportAction(TransportService transportService, ClusterSer
4040
@Override
4141
protected void masterOperation(Task task, XPackUsageRequest request, ClusterState state,
4242
ActionListener<XPackUsageFeatureResponse> listener) {
43-
SpatialFeatureSetUsage usage =
44-
new SpatialFeatureSetUsage(licenseState.isSpatialAllowed(), true);
43+
SpatialFeatureSetUsage usage = new SpatialFeatureSetUsage(licenseState.isSpatialAllowed(), true);
4544
listener.onResponse(new XPackUsageFeatureResponse(usage));
4645
}
4746
}

0 commit comments

Comments
 (0)