Skip to content

Commit ba9d3c6

Browse files
authored
Add support for multipoint shape queries (#52564) (#52705)
1 parent 98bcf06 commit ba9d3c6

File tree

4 files changed

+47
-39
lines changed

4 files changed

+47
-39
lines changed

docs/reference/mapping/types/shape.asciidoc

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,8 @@ depends on the number of vertices that define the geometry.
6767

6868
*IMPORTANT NOTES*
6969

70-
The following features are not yet supported:
71-
72-
* `shape` query with `MultiPoint` geometry types - Elasticsearch currently prevents searching
73-
`shape` fields with a MultiPoint geometry type to avoid a brute force linear search
74-
over each individual point. For now, if this is absolutely needed, this can be achieved
75-
using a `bool` query with each individual point. (Note: this could be very costly)
76-
77-
* `CONTAINS` relation query - `shape` queries with `relation` defined as `contains` are supported
78-
for indices created with ElasticSearch 7.5.0 or higher.
70+
`CONTAINS` relation query - `shape` queries with `relation` defined as `contains` are supported
71+
for indices created with ElasticSearch 7.5.0 or higher.
7972

8073
[float]
8174
===== Example

x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import org.apache.lucene.search.MatchNoDocsQuery;
1616
import org.apache.lucene.search.Query;
1717
import org.elasticsearch.Version;
18-
import org.elasticsearch.common.geo.GeoShapeType;
1918
import org.elasticsearch.common.geo.ShapeRelation;
2019
import org.elasticsearch.geometry.Circle;
2120
import org.elasticsearch.geometry.Geometry;
@@ -33,13 +32,15 @@
3332
import org.elasticsearch.index.mapper.MappedFieldType;
3433
import org.elasticsearch.index.query.QueryShardContext;
3534
import org.elasticsearch.index.query.QueryShardException;
35+
import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper;
3636

3737
import static org.elasticsearch.xpack.spatial.index.mapper.ShapeIndexer.toLucenePolygon;
3838

3939
public class ShapeQueryProcessor implements AbstractGeometryFieldMapper.QueryProcessor {
4040

4141
@Override
4242
public Query process(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context) {
43+
validateIsShapeFieldType(fieldName, context);
4344
if (shape == null) {
4445
return new MatchNoDocsQuery();
4546
}
@@ -52,15 +53,21 @@ public Query process(Geometry shape, String fieldName, ShapeRelation relation, Q
5253
return new ConstantScoreQuery(shape.visit(new ShapeVisitor(context, fieldName, relation)));
5354
}
5455

56+
private void validateIsShapeFieldType(String fieldName, QueryShardContext context) {
57+
MappedFieldType fieldType = context.fieldMapper(fieldName);
58+
if (fieldType instanceof ShapeFieldMapper.ShapeFieldType == false) {
59+
throw new QueryShardException(context, "Expected " + ShapeFieldMapper.CONTENT_TYPE
60+
+ " field type for Field [" + fieldName + "] but found " + fieldType.typeName());
61+
}
62+
}
63+
5564
private class ShapeVisitor implements GeometryVisitor<Query, RuntimeException> {
5665
QueryShardContext context;
57-
MappedFieldType fieldType;
5866
String fieldName;
5967
ShapeRelation relation;
6068

6169
ShapeVisitor(QueryShardContext context, String fieldName, ShapeRelation relation) {
6270
this.context = context;
63-
this.fieldType = context.fieldMapper(fieldName);
6471
this.fieldName = fieldName;
6572
this.relation = relation;
6673
}
@@ -87,13 +94,7 @@ private void visit(BooleanQuery.Builder bqb, GeometryCollection<?> collection) {
8794
occur = BooleanClause.Occur.SHOULD;
8895
}
8996
for (Geometry shape : collection) {
90-
if (shape instanceof MultiPoint) {
91-
// Flatten multipoints
92-
// We do not support multi-point queries?
93-
visit(bqb, (GeometryCollection<?>) shape);
94-
} else {
95-
bqb.add(shape.visit(this), occur);
96-
}
97+
bqb.add(shape.visit(this), occur);
9798
}
9899
}
99100

@@ -120,8 +121,11 @@ public Query visit(MultiLine multiLine) {
120121

121122
@Override
122123
public Query visit(MultiPoint multiPoint) {
123-
throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + GeoShapeType.MULTIPOINT +
124-
" queries");
124+
float[][] points = new float[multiPoint.size()][2];
125+
for (int i = 0; i < multiPoint.size(); i++) {
126+
points[i] = new float[] {(float) multiPoint.get(i).getX(), (float) multiPoint.get(i).getY()};
127+
}
128+
return XYShape.newPointQuery(fieldName, relation.getLuceneRelation(), points);
125129
}
126130

127131
@Override
@@ -145,8 +149,8 @@ public Query visit(Point point) {
145149
// intersects is more efficient.
146150
luceneRelation = ShapeField.QueryRelation.INTERSECTS;
147151
}
148-
return XYShape.newBoxQuery(fieldName, luceneRelation,
149-
(float)point.getX(), (float)point.getX(), (float)point.getY(), (float)point.getY());
152+
float[][] pointArray = new float[][] {{(float)point.getX(), (float)point.getY()}};
153+
return XYShape.newPointQuery(fieldName, luceneRelation, pointArray);
150154
}
151155

152156
@Override

x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderTests.java

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.apache.lucene.search.MatchNoDocsQuery;
1111
import org.apache.lucene.search.Query;
1212
import org.elasticsearch.ElasticsearchException;
13+
import org.elasticsearch.Version;
1314
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
1415
import org.elasticsearch.action.get.GetRequest;
1516
import org.elasticsearch.action.get.GetResponse;
@@ -82,11 +83,7 @@ protected ShapeQueryBuilder doCreateTestQueryBuilder() {
8283
}
8384

8485
protected ShapeQueryBuilder doCreateTestQueryBuilder(boolean indexedShape) {
85-
Geometry shape;
86-
// multipoint queries not (yet) supported
87-
do {
88-
shape = ShapeTestUtils.randomGeometry(false);
89-
} while (shape.type() == ShapeType.MULTIPOINT || shape.type() == ShapeType.GEOMETRYCOLLECTION);
86+
Geometry shape = ShapeTestUtils.randomGeometry(false);
9087

9188
ShapeQueryBuilder builder;
9289
clearShapeFields();
@@ -111,11 +108,22 @@ protected ShapeQueryBuilder doCreateTestQueryBuilder(boolean indexedShape) {
111108
}
112109
}
113110

114-
if (shape.type() == ShapeType.LINESTRING || shape.type() == ShapeType.MULTILINESTRING) {
115-
builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS));
116-
} else {
117-
// XYShape does not support CONTAINS:
118-
builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, ShapeRelation.WITHIN));
111+
if (randomBoolean()) {
112+
QueryShardContext context = createShardContext();
113+
if (context.indexVersionCreated().onOrAfter(Version.V_7_5_0)) { // CONTAINS is only supported from version 7.5
114+
if (shape.type() == ShapeType.LINESTRING || shape.type() == ShapeType.MULTILINESTRING) {
115+
builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, ShapeRelation.CONTAINS));
116+
} else {
117+
builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS,
118+
ShapeRelation.WITHIN, ShapeRelation.CONTAINS));
119+
}
120+
} else {
121+
if (shape.type() == ShapeType.LINESTRING || shape.type() == ShapeType.MULTILINESTRING) {
122+
builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS));
123+
} else {
124+
builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, ShapeRelation.WITHIN));
125+
}
126+
}
119127
}
120128

121129
if (randomBoolean()) {

x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryTests.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.elasticsearch.common.geo.ShapeRelation;
1313
import org.elasticsearch.common.geo.builders.EnvelopeBuilder;
1414
import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder;
15+
import org.elasticsearch.common.geo.builders.MultiPointBuilder;
1516
import org.elasticsearch.common.geo.builders.PointBuilder;
1617
import org.elasticsearch.common.settings.Settings;
1718
import org.elasticsearch.common.xcontent.XContentBuilder;
@@ -298,10 +299,10 @@ public void testGeometryCollectionRelations() throws IOException {
298299
assertEquals(0, response.getHits().getTotalHits().value);
299300
}
300301
{
301-
// A geometry collection that is partially within the indexed shape
302-
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
303-
builder.shape(new PointBuilder(1, 2));
304-
builder.shape(new PointBuilder(20, 30));
302+
// A geometry collection (as multi point) that is partially within the indexed shape
303+
MultiPointBuilder builder = new MultiPointBuilder();
304+
builder.coordinate(1, 2);
305+
builder.coordinate(20, 30);
305306
SearchResponse response = client().prepareSearch("test_collections")
306307
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
307308
.get();
@@ -318,8 +319,10 @@ public void testGeometryCollectionRelations() throws IOException {
318319
{
319320
// A geometry collection that is disjoint with the indexed shape
320321
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
321-
builder.shape(new PointBuilder(-20, -30));
322-
builder.shape(new PointBuilder(20, 30));
322+
MultiPointBuilder innerBuilder = new MultiPointBuilder();
323+
innerBuilder.coordinate(-20, -30);
324+
innerBuilder.coordinate(20, 30);
325+
builder.shape(innerBuilder);
323326
SearchResponse response = client().prepareSearch("test_collections")
324327
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
325328
.get();

0 commit comments

Comments
 (0)