Skip to content

Commit 3717c73

Browse files
authored
"CONTAINS" support for BKD-backed geo_shape and shape fields (#50141) (#50213)
Lucene 8.4 added support for "CONTAINS", therefore in this commit those changes are integrated in Elasticsearch. This commit contains as well a bug fix when querying with a geometry collection with "DISJOINT" relation.
1 parent c732d99 commit 3717c73

File tree

9 files changed

+250
-27
lines changed

9 files changed

+250
-27
lines changed

docs/reference/mapping/types/geo-shape.asciidoc

+2-3
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,8 @@ The following features are not yet supported with the new indexing approach:
142142
using a `bool` query with each individual point.
143143

144144
* `CONTAINS` relation query - when using the new default vector indexing strategy, `geo_shape`
145-
queries with `relation` defined as `contains` are not yet supported. If this query relation
146-
is an absolute necessity, it is recommended to set `strategy` to `quadtree` and use the
147-
deprecated PrefixTree strategy indexing approach.
145+
queries with `relation` defined as `contains` are supported for indices created with
146+
ElasticSearch 7.5.0 or higher.
148147

149148
[[prefix-trees]]
150149
[float]

docs/reference/mapping/types/shape.asciidoc

+3-3
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ The following features are not yet supported:
7474
over each individual point. For now, if this is absolutely needed, this can be achieved
7575
using a `bool` query with each individual point. (Note: this could be very costly)
7676

77-
* `CONTAINS` relation query - `shape` queries with `relation` defined as `contains` are not
78-
yet supported.
77+
* `CONTAINS` relation query - `shape` queries with `relation` defined as `contains` are supported
78+
for indices created with ElasticSearch 7.5.0 or higher.
7979

8080
[float]
8181
===== Example
@@ -445,4 +445,4 @@ POST /example/_doc
445445
Due to the complex input structure and index representation of shapes,
446446
it is not currently possible to sort shapes or retrieve their fields
447447
directly. The `shape` value is only retrievable through the `_source`
448-
field.
448+
field.

docs/reference/query-dsl/geo-shape-query.asciidoc

+1-2
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,7 @@ has nothing in common with the query geometry.
151151
* `WITHIN` - Return all documents whose `geo_shape` field
152152
is within the query geometry.
153153
* `CONTAINS` - Return all documents whose `geo_shape` field
154-
contains the query geometry. Note: this is only supported using the
155-
`recursive` Prefix Tree Strategy deprecated[6.6]
154+
contains the query geometry.
156155

157156
[float]
158157
==== Ignore Unmapped

docs/reference/query-dsl/shape-query.asciidoc

+5-3
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,14 @@ GET /example/_search
171171

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

174-
* `INTERSECTS` - (default) Return all documents whose `geo_shape` field
174+
* `INTERSECTS` - (default) Return all documents whose `shape` field
175175
intersects the query geometry.
176-
* `DISJOINT` - Return all documents whose `geo_shape` field
176+
* `DISJOINT` - Return all documents whose `shape` field
177177
has nothing in common with the query geometry.
178-
* `WITHIN` - Return all documents whose `geo_shape` field
178+
* `WITHIN` - Return all documents whose `shape` field
179179
is within the query geometry.
180+
* `CONTAINS` - Return all documents whose `shape` field
181+
contains the query geometry.
180182

181183
[float]
182184
==== Ignore Unmapped

server/src/main/java/org/elasticsearch/common/geo/ShapeRelation.java

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public QueryRelation getLuceneRelation() {
6969
case INTERSECTS: return QueryRelation.INTERSECTS;
7070
case DISJOINT: return QueryRelation.DISJOINT;
7171
case WITHIN: return QueryRelation.WITHIN;
72+
case CONTAINS: return QueryRelation.CONTAINS;
7273
default:
7374
throw new IllegalArgumentException("ShapeRelation [" + this + "] not supported");
7475
}

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

+23-6
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020
package org.elasticsearch.index.query;
2121

2222
import org.apache.lucene.document.LatLonShape;
23+
import org.apache.lucene.document.ShapeField;
2324
import org.apache.lucene.geo.Line;
2425
import org.apache.lucene.geo.Polygon;
2526
import org.apache.lucene.search.BooleanClause;
2627
import org.apache.lucene.search.BooleanQuery;
2728
import org.apache.lucene.search.MatchNoDocsQuery;
2829
import org.apache.lucene.search.Query;
30+
import org.elasticsearch.Version;
2931
import org.elasticsearch.common.geo.GeoShapeType;
3032
import org.elasticsearch.common.geo.ShapeRelation;
3133
import org.elasticsearch.geometry.Circle;
@@ -49,10 +51,10 @@ public class VectorGeoShapeQueryProcessor implements AbstractGeometryFieldMapper
4951

5052
@Override
5153
public Query process(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context) {
52-
// CONTAINS queries are not yet supported by VECTOR strategy
53-
if (relation == ShapeRelation.CONTAINS) {
54+
// CONTAINS queries are not supported by VECTOR strategy for indices created before version 7.5.0 (Lucene 8.3.0)
55+
if (relation == ShapeRelation.CONTAINS && context.indexVersionCreated().before(Version.V_7_5_0)) {
5456
throw new QueryShardException(context,
55-
ShapeRelation.CONTAINS + " query relation not supported for Field [" + fieldName + "]");
57+
ShapeRelation.CONTAINS + " query relation not supported for Field [" + fieldName + "].");
5658
}
5759
// wrap geoQuery as a ConstantScoreQuery
5860
return getVectorQueryFromShape(shape, fieldName, relation, context);
@@ -95,12 +97,21 @@ public Query visit(GeometryCollection<?> collection) {
9597
}
9698

9799
private void visit(BooleanQuery.Builder bqb, GeometryCollection<?> collection) {
100+
BooleanClause.Occur occur;
101+
if (relation == ShapeRelation.CONTAINS || relation == ShapeRelation.DISJOINT) {
102+
// all shapes must be disjoint / must be contained in relation to the indexed shape.
103+
occur = BooleanClause.Occur.MUST;
104+
} else {
105+
// at least one shape must intersect / contain the indexed shape.
106+
occur = BooleanClause.Occur.SHOULD;
107+
}
98108
for (Geometry shape : collection) {
99109
if (shape instanceof MultiPoint) {
100-
// Flatten multipoints
110+
// Flatten multi-points
111+
// We do not support multi-point queries?
101112
visit(bqb, (GeometryCollection<?>) shape);
102113
} else {
103-
bqb.add(shape.visit(this), BooleanClause.Occur.SHOULD);
114+
bqb.add(shape.visit(this), occur);
104115
}
105116
}
106117
}
@@ -144,7 +155,13 @@ public Query visit(MultiPolygon multiPolygon) {
144155
@Override
145156
public Query visit(Point point) {
146157
validateIsGeoShapeFieldType();
147-
return LatLonShape.newBoxQuery(fieldName, relation.getLuceneRelation(),
158+
ShapeField.QueryRelation luceneRelation = relation.getLuceneRelation();
159+
if (luceneRelation == ShapeField.QueryRelation.CONTAINS) {
160+
// contains and intersects are equivalent but the implementation of
161+
// intersects is more efficient.
162+
luceneRelation = ShapeField.QueryRelation.INTERSECTS;
163+
}
164+
return LatLonShape.newBoxQuery(fieldName, luceneRelation,
148165
point.getY(), point.getY(), point.getX(), point.getX());
149166
}
150167

server/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java

+96-3
Original file line numberDiff line numberDiff line change
@@ -483,10 +483,30 @@ public void testPointQuery() throws Exception {
483483
public void testContainsShapeQuery() throws Exception {
484484
// Create a random geometry collection.
485485
Rectangle mbr = xRandomRectangle(random(), xRandomPoint(random()), true);
486-
GeometryCollectionBuilder gcb = createGeometryCollectionWithin(random(), mbr);
486+
boolean usePrefixTrees = randomBoolean();
487+
GeometryCollectionBuilder gcb;
488+
if (usePrefixTrees) {
489+
gcb = createGeometryCollectionWithin(random(), mbr);
490+
} else {
491+
// vector strategy does not yet support multipoint queries
492+
gcb = new GeometryCollectionBuilder();
493+
int numShapes = RandomNumbers.randomIntBetween(random(), 1, 4);
494+
for (int i = 0; i < numShapes; ++i) {
495+
ShapeBuilder shape;
496+
do {
497+
shape = RandomShapeGenerator.createShapeWithin(random(), mbr);
498+
} while (shape instanceof MultiPointBuilder);
499+
gcb.shape(shape);
500+
}
501+
}
487502

488-
client().admin().indices().prepareCreate("test").addMapping("type", "location", "type=geo_shape,tree=quadtree" )
489-
.get();
503+
if (usePrefixTrees) {
504+
client().admin().indices().prepareCreate("test").addMapping("type", "location", "type=geo_shape,tree=quadtree")
505+
.execute().actionGet();
506+
} else {
507+
client().admin().indices().prepareCreate("test").addMapping("type", "location", "type=geo_shape")
508+
.execute().actionGet();
509+
}
490510

491511
XContentBuilder docSource = gcb.toXContent(jsonBuilder().startObject().field("location"), null).endObject();
492512
client().prepareIndex("test", "type", "1").setSource(docSource).setRefreshPolicy(IMMEDIATE).get();
@@ -763,4 +783,77 @@ public void testEnvelopeSpanningDateline() throws IOException {
763783
assertNotEquals("1", response.getHits().getAt(0).getId());
764784
assertNotEquals("1", response.getHits().getAt(1).getId());
765785
}
786+
787+
public void testGeometryCollectionRelations() throws IOException {
788+
XContentBuilder mapping = XContentFactory.jsonBuilder().startObject()
789+
.startObject("doc")
790+
.startObject("properties")
791+
.startObject("geo").field("type", "geo_shape").endObject()
792+
.endObject()
793+
.endObject()
794+
.endObject();
795+
796+
createIndex("test", Settings.builder().put("index.number_of_shards", 1).build(), "doc", mapping);
797+
798+
EnvelopeBuilder envelopeBuilder = new EnvelopeBuilder(new Coordinate(-10, 10), new Coordinate(10, -10));
799+
800+
client().index(new IndexRequest("test")
801+
.source(jsonBuilder().startObject().field("geo", envelopeBuilder).endObject())
802+
.setRefreshPolicy(IMMEDIATE)).actionGet();
803+
804+
{
805+
// A geometry collection that is fully within the indexed shape
806+
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
807+
builder.shape(new PointBuilder(1, 2));
808+
builder.shape(new PointBuilder(-2, -1));
809+
SearchResponse response = client().prepareSearch("test")
810+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
811+
.get();
812+
assertEquals(1, response.getHits().getTotalHits().value);
813+
response = client().prepareSearch("test")
814+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
815+
.get();
816+
assertEquals(1, response.getHits().getTotalHits().value);
817+
response = client().prepareSearch("test")
818+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
819+
.get();
820+
assertEquals(0, response.getHits().getTotalHits().value);
821+
}
822+
// A geometry collection that is partially within the indexed shape
823+
{
824+
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
825+
builder.shape(new PointBuilder(1, 2));
826+
builder.shape(new PointBuilder(20, 30));
827+
SearchResponse response = client().prepareSearch("test")
828+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
829+
.get();
830+
assertEquals(0, response.getHits().getTotalHits().value);
831+
response = client().prepareSearch("test")
832+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
833+
.get();
834+
assertEquals(1, response.getHits().getTotalHits().value);
835+
response = client().prepareSearch("test")
836+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
837+
.get();
838+
assertEquals(0, response.getHits().getTotalHits().value);
839+
}
840+
{
841+
// A geometry collection that is disjoint with the indexed shape
842+
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
843+
builder.shape(new PointBuilder(-20, -30));
844+
builder.shape(new PointBuilder(20, 30));
845+
SearchResponse response = client().prepareSearch("test")
846+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
847+
.get();
848+
assertEquals(0, response.getHits().getTotalHits().value);
849+
response = client().prepareSearch("test")
850+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
851+
.get();
852+
assertEquals(0, response.getHits().getTotalHits().value);
853+
response = client().prepareSearch("test")
854+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
855+
.get();
856+
assertEquals(1, response.getHits().getTotalHits().value);
857+
}
858+
}
766859
}

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

+24-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
package org.elasticsearch.xpack.spatial.index.query;
77

8+
import org.apache.lucene.document.ShapeField;
89
import org.apache.lucene.document.XYShape;
910
import org.apache.lucene.geo.XYLine;
1011
import org.apache.lucene.geo.XYPolygon;
@@ -13,6 +14,7 @@
1314
import org.apache.lucene.search.ConstantScoreQuery;
1415
import org.apache.lucene.search.MatchNoDocsQuery;
1516
import org.apache.lucene.search.Query;
17+
import org.elasticsearch.Version;
1618
import org.elasticsearch.common.geo.GeoShapeType;
1719
import org.elasticsearch.common.geo.ShapeRelation;
1820
import org.elasticsearch.geometry.Circle;
@@ -38,14 +40,14 @@ public class ShapeQueryProcessor implements AbstractGeometryFieldMapper.QueryPro
3840

3941
@Override
4042
public Query process(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context) {
41-
// CONTAINS queries are not yet supported by VECTOR strategy
42-
if (relation == ShapeRelation.CONTAINS) {
43-
throw new QueryShardException(context,
44-
ShapeRelation.CONTAINS + " query relation not supported for Field [" + fieldName + "]");
45-
}
4643
if (shape == null) {
4744
return new MatchNoDocsQuery();
4845
}
46+
// CONTAINS queries are not supported by VECTOR strategy for indices created before version 7.5.0 (Lucene 8.3.0);
47+
if (relation == ShapeRelation.CONTAINS && context.indexVersionCreated().before(Version.V_7_5_0)) {
48+
throw new QueryShardException(context,
49+
ShapeRelation.CONTAINS + " query relation not supported for Field [" + fieldName + "].");
50+
}
4951
// wrap geometry Query as a ConstantScoreQuery
5052
return new ConstantScoreQuery(shape.visit(new ShapeVisitor(context, fieldName, relation)));
5153
}
@@ -76,12 +78,21 @@ public Query visit(GeometryCollection<?> collection) {
7678
}
7779

7880
private void visit(BooleanQuery.Builder bqb, GeometryCollection<?> collection) {
81+
BooleanClause.Occur occur;
82+
if (relation == ShapeRelation.CONTAINS || relation == ShapeRelation.DISJOINT) {
83+
// all shapes must be disjoint / must be contained in relation to the indexed shape.
84+
occur = BooleanClause.Occur.MUST;
85+
} else {
86+
// at least one shape must intersect / contain the indexed shape.
87+
occur = BooleanClause.Occur.SHOULD;
88+
}
7989
for (Geometry shape : collection) {
8090
if (shape instanceof MultiPoint) {
8191
// Flatten multipoints
92+
// We do not support multi-point queries?
8293
visit(bqb, (GeometryCollection<?>) shape);
8394
} else {
84-
bqb.add(shape.visit(this), BooleanClause.Occur.SHOULD);
95+
bqb.add(shape.visit(this), occur);
8596
}
8697
}
8798
}
@@ -128,7 +139,13 @@ private Query visitMultiPolygon(XYPolygon... polygons) {
128139

129140
@Override
130141
public Query visit(Point point) {
131-
return XYShape.newBoxQuery(fieldName, relation.getLuceneRelation(),
142+
ShapeField.QueryRelation luceneRelation = relation.getLuceneRelation();
143+
if (luceneRelation == ShapeField.QueryRelation.CONTAINS) {
144+
// contains and intersects are equivalent but the implementation of
145+
// intersects is more efficient.
146+
luceneRelation = ShapeField.QueryRelation.INTERSECTS;
147+
}
148+
return XYShape.newBoxQuery(fieldName, luceneRelation,
132149
(float)point.getX(), (float)point.getX(), (float)point.getY(), (float)point.getY());
133150
}
134151

0 commit comments

Comments
 (0)