Skip to content
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();
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);
}
}
}