Skip to content

Commit 3e01df9

Browse files
iveraseSivagurunathanV
authored andcommitted
"CONTAINS" support for BKD-backed geo_shape and shape fields (elastic#50141)
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 8b33f15 commit 3e01df9

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
@@ -170,12 +170,14 @@ GET /example/_search
170170

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

173-
* `INTERSECTS` - (default) Return all documents whose `geo_shape` field
173+
* `INTERSECTS` - (default) Return all documents whose `shape` field
174174
intersects the query geometry.
175-
* `DISJOINT` - Return all documents whose `geo_shape` field
175+
* `DISJOINT` - Return all documents whose `shape` field
176176
has nothing in common with the query geometry.
177-
* `WITHIN` - Return all documents whose `geo_shape` field
177+
* `WITHIN` - Return all documents whose `shape` field
178178
is within the query geometry.
179+
* `CONTAINS` - Return all documents whose `shape` field
180+
contains the query geometry.
179181

180182
[float]
181183
==== 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
@@ -447,10 +447,30 @@ public void testPointQuery() throws Exception {
447447
public void testContainsShapeQuery() throws Exception {
448448
// Create a random geometry collection.
449449
Rectangle mbr = xRandomRectangle(random(), xRandomPoint(random()), true);
450-
GeometryCollectionBuilder gcb = createGeometryCollectionWithin(random(), mbr);
450+
boolean usePrefixTrees = randomBoolean();
451+
GeometryCollectionBuilder gcb;
452+
if (usePrefixTrees) {
453+
gcb = createGeometryCollectionWithin(random(), mbr);
454+
} else {
455+
// vector strategy does not yet support multipoint queries
456+
gcb = new GeometryCollectionBuilder();
457+
int numShapes = RandomNumbers.randomIntBetween(random(), 1, 4);
458+
for (int i = 0; i < numShapes; ++i) {
459+
ShapeBuilder shape;
460+
do {
461+
shape = RandomShapeGenerator.createShapeWithin(random(), mbr);
462+
} while (shape instanceof MultiPointBuilder);
463+
gcb.shape(shape);
464+
}
465+
}
451466

452-
client().admin().indices().prepareCreate("test").addMapping("type", "location", "type=geo_shape,tree=quadtree" )
453-
.get();
467+
if (usePrefixTrees) {
468+
client().admin().indices().prepareCreate("test").addMapping("type", "location", "type=geo_shape,tree=quadtree")
469+
.execute().actionGet();
470+
} else {
471+
client().admin().indices().prepareCreate("test").addMapping("type", "location", "type=geo_shape")
472+
.execute().actionGet();
473+
}
454474

455475
XContentBuilder docSource = gcb.toXContent(jsonBuilder().startObject().field("location"), null).endObject();
456476
client().prepareIndex("test").setId("1").setSource(docSource).setRefreshPolicy(IMMEDIATE).get();
@@ -727,4 +747,77 @@ public void testEnvelopeSpanningDateline() throws IOException {
727747
assertNotEquals("1", response.getHits().getAt(0).getId());
728748
assertNotEquals("1", response.getHits().getAt(1).getId());
729749
}
750+
751+
public void testGeometryCollectionRelations() throws IOException {
752+
XContentBuilder mapping = XContentFactory.jsonBuilder().startObject()
753+
.startObject("doc")
754+
.startObject("properties")
755+
.startObject("geo").field("type", "geo_shape").endObject()
756+
.endObject()
757+
.endObject()
758+
.endObject();
759+
760+
createIndex("test", Settings.builder().put("index.number_of_shards", 1).build(), "doc", mapping);
761+
762+
EnvelopeBuilder envelopeBuilder = new EnvelopeBuilder(new Coordinate(-10, 10), new Coordinate(10, -10));
763+
764+
client().index(new IndexRequest("test")
765+
.source(jsonBuilder().startObject().field("geo", envelopeBuilder).endObject())
766+
.setRefreshPolicy(IMMEDIATE)).actionGet();
767+
768+
{
769+
// A geometry collection that is fully within the indexed shape
770+
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
771+
builder.shape(new PointBuilder(1, 2));
772+
builder.shape(new PointBuilder(-2, -1));
773+
SearchResponse response = client().prepareSearch("test")
774+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
775+
.get();
776+
assertEquals(1, response.getHits().getTotalHits().value);
777+
response = client().prepareSearch("test")
778+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
779+
.get();
780+
assertEquals(1, response.getHits().getTotalHits().value);
781+
response = client().prepareSearch("test")
782+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
783+
.get();
784+
assertEquals(0, response.getHits().getTotalHits().value);
785+
}
786+
// A geometry collection that is partially within the indexed shape
787+
{
788+
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
789+
builder.shape(new PointBuilder(1, 2));
790+
builder.shape(new PointBuilder(20, 30));
791+
SearchResponse response = client().prepareSearch("test")
792+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
793+
.get();
794+
assertEquals(0, response.getHits().getTotalHits().value);
795+
response = client().prepareSearch("test")
796+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
797+
.get();
798+
assertEquals(1, response.getHits().getTotalHits().value);
799+
response = client().prepareSearch("test")
800+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
801+
.get();
802+
assertEquals(0, response.getHits().getTotalHits().value);
803+
}
804+
{
805+
// A geometry collection that is disjoint with the indexed shape
806+
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
807+
builder.shape(new PointBuilder(-20, -30));
808+
builder.shape(new PointBuilder(20, 30));
809+
SearchResponse response = client().prepareSearch("test")
810+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
811+
.get();
812+
assertEquals(0, response.getHits().getTotalHits().value);
813+
response = client().prepareSearch("test")
814+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
815+
.get();
816+
assertEquals(0, response.getHits().getTotalHits().value);
817+
response = client().prepareSearch("test")
818+
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
819+
.get();
820+
assertEquals(1, response.getHits().getTotalHits().value);
821+
}
822+
}
730823
}

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)