Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"CONTAINS" support for BKD-backed geo_shape and shape fields #50141

Merged
merged 6 commits into from
Dec 16, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions docs/reference/query-dsl/geo-shape-query.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,7 @@ has nothing in common with the query geometry.
* `WITHIN` - Return all documents whose `geo_shape` field
is within the query geometry.
* `CONTAINS` - Return all documents whose `geo_shape` field
contains the query geometry. Note: this is only supported using the
`recursive` Prefix Tree Strategy deprecated[6.6]
contains the query geometry.

[float]
==== Ignore Unmapped
Expand Down
8 changes: 5 additions & 3 deletions docs/reference/query-dsl/shape-query.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,14 @@ GET /example/_search

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

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

[float]
==== Ignore Unmapped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public QueryRelation getLuceneRelation() {
case INTERSECTS: return QueryRelation.INTERSECTS;
case DISJOINT: return QueryRelation.DISJOINT;
case WITHIN: return QueryRelation.WITHIN;
case CONTAINS: return QueryRelation.CONTAINS;
default:
throw new IllegalArgumentException("ShapeRelation [" + this + "] not supported");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package org.elasticsearch.index.query;

import org.apache.lucene.document.LatLonShape;
import org.apache.lucene.document.ShapeField;
import org.apache.lucene.geo.Line;
import org.apache.lucene.geo.Polygon;
import org.apache.lucene.search.BooleanClause;
Expand Down Expand Up @@ -49,11 +50,6 @@ public class VectorGeoShapeQueryProcessor implements AbstractGeometryFieldMapper

@Override
public Query process(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context) {
// CONTAINS queries are not yet supported by VECTOR strategy
if (relation == ShapeRelation.CONTAINS) {
throw new QueryShardException(context,
ShapeRelation.CONTAINS + " query relation not supported for Field [" + fieldName + "]");
}
// wrap geoQuery as a ConstantScoreQuery
return getVectorQueryFromShape(shape, fieldName, relation, context);
}
Expand Down Expand Up @@ -95,12 +91,21 @@ public Query visit(GeometryCollection<?> collection) {
}

private void visit(BooleanQuery.Builder bqb, GeometryCollection<?> collection) {
BooleanClause.Occur occur;
if (relation == ShapeRelation.CONTAINS || relation == ShapeRelation.DISJOINT) {
// all shapes must be disjoint / must be contained in relation to the indexed shape.
occur = BooleanClause.Occur.MUST;
} else {
// at least one shape must intersect / contain the indexed shape.
occur = BooleanClause.Occur.SHOULD;
}
for (Geometry shape : collection) {
if (shape instanceof MultiPoint) {
// Flatten multipoints
// Flatten multi-points
// We do not support multi-point queries?
visit(bqb, (GeometryCollection<?>) shape);
} else {
bqb.add(shape.visit(this), BooleanClause.Occur.SHOULD);
bqb.add(shape.visit(this), occur);
}
}
}
Expand Down Expand Up @@ -144,7 +149,13 @@ public Query visit(MultiPolygon multiPolygon) {
@Override
public Query visit(Point point) {
validateIsGeoShapeFieldType();
return LatLonShape.newBoxQuery(fieldName, relation.getLuceneRelation(),
ShapeField.QueryRelation lucenRelation = relation.getLuceneRelation();
iverase marked this conversation as resolved.
Show resolved Hide resolved
if (lucenRelation == ShapeField.QueryRelation.CONTAINS) {
// contains and intersects are equivalent but the implementation of
// intersects is more efficient.
lucenRelation = ShapeField.QueryRelation.INTERSECTS;
}
return LatLonShape.newBoxQuery(fieldName, lucenRelation,
point.getY(), point.getY(), point.getX(), point.getX());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -449,8 +449,13 @@ public void testContainsShapeQuery() throws Exception {
Rectangle mbr = xRandomRectangle(random(), xRandomPoint(random()), true);
GeometryCollectionBuilder gcb = createGeometryCollectionWithin(random(), mbr);

client().admin().indices().prepareCreate("test").addMapping("type", "location", "type=geo_shape,tree=quadtree" )
.get();
if (randomBoolean()) {
client().admin().indices().prepareCreate("test").addMapping("type", "location", "type=geo_shape")
.execute().actionGet();
} else {
client().admin().indices().prepareCreate("test").addMapping("type", "location", "type=geo_shape,tree=quadtree")
.execute().actionGet();
}

XContentBuilder docSource = gcb.toXContent(jsonBuilder().startObject().field("location"), null).endObject();
client().prepareIndex("test").setId("1").setSource(docSource).setRefreshPolicy(IMMEDIATE).get();
Expand Down Expand Up @@ -727,4 +732,77 @@ public void testEnvelopeSpanningDateline() throws IOException {
assertNotEquals("1", response.getHits().getAt(0).getId());
assertNotEquals("1", response.getHits().getAt(1).getId());
}

public void testGeometryCollectionRelations() throws IOException {
XContentBuilder mapping = XContentFactory.jsonBuilder().startObject()
.startObject("doc")
.startObject("properties")
.startObject("geo").field("type", "geo_shape").endObject()
.endObject()
.endObject()
.endObject();

createIndex("test", Settings.builder().put("index.number_of_shards", 1).build(), "doc", mapping);

EnvelopeBuilder envelopeBuilder = new EnvelopeBuilder(new Coordinate(-10, 10), new Coordinate(10, -10));

client().index(new IndexRequest("test")
.source(jsonBuilder().startObject().field("geo", envelopeBuilder).endObject())
.setRefreshPolicy(IMMEDIATE)).actionGet();

{
// A geometry collection that is fully within the indexed shape
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
builder.shape(new PointBuilder(1, 2));
builder.shape(new PointBuilder(-2, -1));
SearchResponse response = client().prepareSearch("test")
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
.get();
assertEquals(1, response.getHits().getTotalHits().value);
response = client().prepareSearch("test")
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
.get();
assertEquals(1, response.getHits().getTotalHits().value);
response = client().prepareSearch("test")
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
.get();
assertEquals(0, response.getHits().getTotalHits().value);
}
// A geometry collection that is partially within the indexed shape
{
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
builder.shape(new PointBuilder(1, 2));
builder.shape(new PointBuilder(20, 30));
SearchResponse response = client().prepareSearch("test")
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
.get();
assertEquals(0, response.getHits().getTotalHits().value);
response = client().prepareSearch("test")
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
.get();
assertEquals(1, response.getHits().getTotalHits().value);
response = client().prepareSearch("test")
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
.get();
assertEquals(0, response.getHits().getTotalHits().value);
}
{
// A geometry collection that is disjoint with the indexed shape
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
builder.shape(new PointBuilder(-20, -30));
builder.shape(new PointBuilder(20, 30));
SearchResponse response = client().prepareSearch("test")
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
.get();
assertEquals(0, response.getHits().getTotalHits().value);
response = client().prepareSearch("test")
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
.get();
assertEquals(0, response.getHits().getTotalHits().value);
response = client().prepareSearch("test")
.setQuery(geoShapeQuery("geo", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
.get();
assertEquals(1, response.getHits().getTotalHits().value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
package org.elasticsearch.xpack.spatial.index.query;

import org.apache.lucene.document.ShapeField;
import org.apache.lucene.document.XYShape;
import org.apache.lucene.geo.XYLine;
import org.apache.lucene.geo.XYPolygon;
Expand Down Expand Up @@ -38,11 +39,6 @@ public class ShapeQueryProcessor implements AbstractGeometryFieldMapper.QueryPro

@Override
public Query process(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context) {
// CONTAINS queries are not yet supported by VECTOR strategy
if (relation == ShapeRelation.CONTAINS) {
throw new QueryShardException(context,
ShapeRelation.CONTAINS + " query relation not supported for Field [" + fieldName + "]");
}
if (shape == null) {
return new MatchNoDocsQuery();
}
Expand Down Expand Up @@ -76,12 +72,21 @@ public Query visit(GeometryCollection<?> collection) {
}

private void visit(BooleanQuery.Builder bqb, GeometryCollection<?> collection) {
BooleanClause.Occur occur;
if (relation == ShapeRelation.CONTAINS || relation == ShapeRelation.DISJOINT) {
// all shapes must be disjoint / must be contained in relation to the indexed shape.
occur = BooleanClause.Occur.MUST;
} else {
// at least one shape must intersect / contain the indexed shape.
occur = BooleanClause.Occur.SHOULD;
}
for (Geometry shape : collection) {
if (shape instanceof MultiPoint) {
// Flatten multipoints
// We do not support multi-point queries?
visit(bqb, (GeometryCollection<?>) shape);
} else {
bqb.add(shape.visit(this), BooleanClause.Occur.SHOULD);
bqb.add(shape.visit(this), occur);
}
}
}
Expand Down Expand Up @@ -128,7 +133,13 @@ private Query visitMultiPolygon(XYPolygon... polygons) {

@Override
public Query visit(Point point) {
return XYShape.newBoxQuery(fieldName, relation.getLuceneRelation(),
ShapeField.QueryRelation lucenRelation = relation.getLuceneRelation();
if (lucenRelation == ShapeField.QueryRelation.CONTAINS) {
// contains and intersects are equivalent but the implementation of
// intersects is more efficient.
lucenRelation = ShapeField.QueryRelation.INTERSECTS;
}
return XYShape.newBoxQuery(fieldName, lucenRelation,
(float)point.getX(), (float)point.getX(), (float)point.getY(), (float)point.getY());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
package org.elasticsearch.xpack.spatial.search;

import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.geo.GeoJson;
import org.elasticsearch.common.geo.ShapeRelation;
import org.elasticsearch.common.geo.builders.EnvelopeBuilder;
import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder;
import org.elasticsearch.common.geo.builders.PointBuilder;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
Expand All @@ -25,6 +29,7 @@
import org.elasticsearch.xpack.spatial.util.ShapeTestUtils;
import org.locationtech.jts.geom.Coordinate;

import java.io.IOException;
import java.util.Collection;
import java.util.Locale;

Expand Down Expand Up @@ -239,4 +244,94 @@ public void testFieldAlias() {
.get();
assertTrue(response.getHits().getTotalHits().value > 0);
}

public void testContainsShapeQuery() {

client().admin().indices().prepareCreate("test_contains").addMapping("type", "location", "type=shape")
.execute().actionGet();

String doc = "{\"location\" : {\"type\":\"envelope\", \"coordinates\":[ [-100.0, 100.0], [100.0, -100.0]]}}";
client().prepareIndex("test_contains").setId("1").setSource(doc, XContentType.JSON).setRefreshPolicy(IMMEDIATE).get();

// index the mbr of the collection
EnvelopeBuilder queryShape = new EnvelopeBuilder(new Coordinate(-50, 50), new Coordinate(50, -50));
ShapeQueryBuilder queryBuilder = new ShapeQueryBuilder("location", queryShape.buildGeometry()).relation(ShapeRelation.CONTAINS);
SearchResponse response = client().prepareSearch("test_contains").setQuery(queryBuilder).get();
assertSearchResponse(response);

assertThat(response.getHits().getTotalHits().value, equalTo(1L));
}

public void testGeometryCollectionRelations() throws IOException {
XContentBuilder mapping = XContentFactory.jsonBuilder().startObject()
.startObject("doc")
.startObject("properties")
.startObject("geometry").field("type", "shape").endObject()
.endObject()
.endObject()
.endObject();

createIndex("test_collections", Settings.builder().put("index.number_of_shards", 1).build(), "doc", mapping);

EnvelopeBuilder envelopeBuilder = new EnvelopeBuilder(new Coordinate(-10, 10), new Coordinate(10, -10));

client().index(new IndexRequest("test_collections")
.source(jsonBuilder().startObject().field("geometry", envelopeBuilder).endObject())
.setRefreshPolicy(IMMEDIATE)).actionGet();

{
// A geometry collection that is fully within the indexed shape
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
builder.shape(new PointBuilder(1, 2));
builder.shape(new PointBuilder(-2, -1));
SearchResponse response = client().prepareSearch("test_collections")
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
.get();
assertEquals(1, response.getHits().getTotalHits().value);
response = client().prepareSearch("test_collections")
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
.get();
assertEquals(1, response.getHits().getTotalHits().value);
response = client().prepareSearch("test_collections")
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
.get();
assertEquals(0, response.getHits().getTotalHits().value);
}
{
// A geometry collection that is partially within the indexed shape
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
builder.shape(new PointBuilder(1, 2));
builder.shape(new PointBuilder(20, 30));
SearchResponse response = client().prepareSearch("test_collections")
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
.get();
assertEquals(0, response.getHits().getTotalHits().value);
response = client().prepareSearch("test_collections")
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
.get();
assertEquals(1, response.getHits().getTotalHits().value);
response = client().prepareSearch("test_collections")
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
.get();
assertEquals(0, response.getHits().getTotalHits().value);
}
{
// A geometry collection that is disjoint with the indexed shape
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
builder.shape(new PointBuilder(-20, -30));
builder.shape(new PointBuilder(20, 30));
SearchResponse response = client().prepareSearch("test_collections")
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
.get();
assertEquals(0, response.getHits().getTotalHits().value);
response = client().prepareSearch("test_collections")
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
.get();
assertEquals(0, response.getHits().getTotalHits().value);
response = client().prepareSearch("test_collections")
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
.get();
assertEquals(1, response.getHits().getTotalHits().value);
}
}
}