From d1cbdfb753045a0eafebefe22cf7aa6b235a3da3 Mon Sep 17 00:00:00 2001 From: Dominic Page <11043991+djptek@users.noreply.github.com> Date: Wed, 18 Mar 2020 17:03:52 +0100 Subject: [PATCH] Geo shape query vs geo point (#52382) Enable geo_shape query to work on geo_point fields for shapes: circle, polygon, multipolygon, rectangle see: #48928 Co-Authored-By: @iverase --- docs/reference/query-dsl/geo-queries.asciidoc | 8 +- .../query-dsl/geo-shape-query.asciidoc | 20 +- .../elasticsearch/geometry/LinearRing.java | 2 +- .../mapper/AbstractGeometryFieldMapper.java | 28 +- .../AbstractSearchableGeometryFieldType.java | 63 +++ .../index/mapper/GeoPointFieldMapper.java | 14 +- .../index/mapper/GeoShapeIndexer.java | 3 +- .../query/AbstractGeometryQueryBuilder.java | 8 +- .../index/query/GeoShapeQueryBuilder.java | 28 +- .../query/LegacyGeoShapeQueryProcessor.java | 3 +- .../VectorGeoPointShapeQueryProcessor.java | 191 +++++++++ .../query/VectorGeoShapeQueryProcessor.java | 11 +- .../common/geo/GeometryIndexerTests.java | 3 +- .../GeoShapeQueryBuilderGeoPointTests.java | 65 +++ .../GeoShapeQueryBuilderGeoShapeTests.java | 84 ++++ .../query/GeoShapeQueryBuilderTests.java | 63 +-- .../search/geo/GeoPointShapeQueryTests.java | 141 ++++++- .../search/geo/GeoQueryTests.java | 397 ++++++++++++++++-- .../search/geo/GeoShapeQueryTests.java | 4 - .../index/query/ShapeQueryBuilder.java | 19 +- .../index/query/ShapeQueryProcessor.java | 4 +- 21 files changed, 1002 insertions(+), 157 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/index/mapper/AbstractSearchableGeometryFieldType.java create mode 100644 server/src/main/java/org/elasticsearch/index/query/VectorGeoPointShapeQueryProcessor.java create mode 100644 server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderGeoPointTests.java create mode 100644 server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderGeoShapeTests.java diff --git a/docs/reference/query-dsl/geo-queries.asciidoc b/docs/reference/query-dsl/geo-queries.asciidoc index b3cc9112576cc..b4eb86763e702 100644 --- a/docs/reference/query-dsl/geo-queries.asciidoc +++ b/docs/reference/query-dsl/geo-queries.asciidoc @@ -18,9 +18,11 @@ Finds documents with geo-points within the specified distance of a central point Find documents with geo-points within the specified polygon. <> query:: -Finds documents with geo-shapes which either intersect, are contained by, or do not intersect with the specified -geo-shape. - +Finds documents with: +* `geo-shapes` which either intersect, are contained by, or do not intersect +with the specified geo-shape +* `geo-points` which intersect the specified +geo-shape include::geo-bounding-box-query.asciidoc[] diff --git a/docs/reference/query-dsl/geo-shape-query.asciidoc b/docs/reference/query-dsl/geo-shape-query.asciidoc index 9706b90d82845..966267d4e3537 100644 --- a/docs/reference/query-dsl/geo-shape-query.asciidoc +++ b/docs/reference/query-dsl/geo-shape-query.asciidoc @@ -4,9 +4,9 @@ Geo-shape ++++ -Filter documents indexed using the `geo_shape` type. +Filter documents indexed using the `geo_shape` or `geo_point` type. -Requires the <>. +Requires the <> or the <>. The `geo_shape` query uses the same grid square representation as the `geo_shape` mapping to find documents that have a shape that intersects @@ -142,7 +142,7 @@ GET /example/_search The <> mapping parameter determines which spatial relation operators may be used at search time. -The following is a complete list of spatial relation operators available: +The following is a complete list of spatial relation operators available when searching a field of type `geo_shape`: * `INTERSECTS` - (default) Return all documents whose `geo_shape` field intersects the query geometry. @@ -153,6 +153,11 @@ is within the query geometry. * `CONTAINS` - Return all documents whose `geo_shape` field contains the query geometry. +When searching a field of type `geo_point` there is a single supported spatial relation operator: + +* `INTERSECTS` - (default) Return all documents whose `geo_point` field +intersects the query geometry. + [float] ==== Ignore Unmapped @@ -162,6 +167,15 @@ querying multiple indexes which might have different mappings. When set to `false` (the default value) the query will throw an exception if the field is not mapped. +==== Shape Types supported for Geo-Point + +When searching a field of type `geo_point` the following shape types are not supported: + +* `POINT` +* `LINE` +* `MULTIPOINT` +* `MULTILINE` + ==== Notes Geo-shape queries on geo-shapes implemented with <> will not be executed if <> is set to false. diff --git a/libs/geo/src/main/java/org/elasticsearch/geometry/LinearRing.java b/libs/geo/src/main/java/org/elasticsearch/geometry/LinearRing.java index c0d0150c3023e..babe02bdf55af 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geometry/LinearRing.java +++ b/libs/geo/src/main/java/org/elasticsearch/geometry/LinearRing.java @@ -64,6 +64,6 @@ public T visit(GeometryVisitor visitor) throws E public String toString() { return "linearring(x=" + Arrays.toString(getX()) + ", y=" + Arrays.toString(getY()) + - (hasZ() ? ", z=" + Arrays.toString(getZ()) : ""); + (hasZ() ? ", z=" + Arrays.toString(getZ()) : "") + ")"; } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/AbstractGeometryFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/AbstractGeometryFieldMapper.java index 006af39b8f59b..d065e8121dc05 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/AbstractGeometryFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/AbstractGeometryFieldMapper.java @@ -26,8 +26,6 @@ import org.elasticsearch.Version; import org.elasticsearch.common.Explicit; import org.elasticsearch.common.ParseField; -import org.elasticsearch.common.geo.ShapeRelation; -import org.elasticsearch.common.geo.SpatialStrategy; import org.elasticsearch.common.geo.builders.ShapeBuilder; import org.elasticsearch.common.geo.builders.ShapeBuilder.Orientation; import org.elasticsearch.common.settings.Settings; @@ -35,7 +33,6 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.support.XContentMapValues; -import org.elasticsearch.geometry.Geometry; import org.elasticsearch.index.mapper.LegacyGeoShapeFieldMapper.DeprecatedParameters; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.index.query.QueryShardException; @@ -91,19 +88,6 @@ public interface Parser { } - /** - * interface representing a query builder that generates a query from the given shape - */ - public interface QueryProcessor { - Query process(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context); - - @Deprecated - default Query process(Geometry shape, String fieldName, SpatialStrategy strategy, ShapeRelation relation, - QueryShardContext context) { - return process(shape, fieldName, relation, context); - } - } - public abstract static class Builder extends FieldMapper.Builder { protected Boolean coerce; @@ -272,14 +256,14 @@ public Mapper.Builder parse(String name, Map node, ParserContext } } - public abstract static class AbstractGeometryFieldType extends MappedFieldType { + public abstract static class AbstractGeometryFieldType extends AbstractSearchableGeometryFieldType { protected Orientation orientation = Defaults.ORIENTATION.value(); protected Indexer geometryIndexer; protected Parser geometryParser; - protected QueryProcessor geometryQueryBuilder; + protected AbstractGeometryFieldType() { setIndexOptions(IndexOptions.DOCS); @@ -339,14 +323,6 @@ public void setGeometryParser(Parser geometryParser) { protected Parser geometryParser() { return geometryParser; } - - public void setGeometryQueryBuilder(QueryProcessor geometryQueryBuilder) { - this.geometryQueryBuilder = geometryQueryBuilder; - } - - public QueryProcessor geometryQueryBuilder() { - return geometryQueryBuilder; - } } protected Explicit coerce; diff --git a/server/src/main/java/org/elasticsearch/index/mapper/AbstractSearchableGeometryFieldType.java b/server/src/main/java/org/elasticsearch/index/mapper/AbstractSearchableGeometryFieldType.java new file mode 100644 index 0000000000000..29154bfc36548 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/mapper/AbstractSearchableGeometryFieldType.java @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.index.mapper; + +import org.apache.lucene.search.Query; +import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.common.geo.SpatialStrategy; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.index.query.QueryShardContext; + +/** + * a base class for geometry types that support shape query builder + */ +public abstract class AbstractSearchableGeometryFieldType extends MappedFieldType { + + protected QueryProcessor geometryQueryBuilder; + + protected AbstractSearchableGeometryFieldType() { + } + + protected AbstractSearchableGeometryFieldType(AbstractSearchableGeometryFieldType ref) { + super(ref); + } + + public void setGeometryQueryBuilder(QueryProcessor geometryQueryBuilder) { + this.geometryQueryBuilder = geometryQueryBuilder; + } + + public QueryProcessor geometryQueryBuilder() { + return geometryQueryBuilder; + } + + /** + * interface representing a query builder that generates a query from the given shape + */ + public interface QueryProcessor { + Query process(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context); + + @Deprecated + default Query process(Geometry shape, String fieldName, SpatialStrategy strategy, ShapeRelation relation, + QueryShardContext context) { + return process(shape, fieldName, relation, context); + } + } +} + diff --git a/server/src/main/java/org/elasticsearch/index/mapper/GeoPointFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/GeoPointFieldMapper.java index 93d8215f21cac..5239331819be3 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/GeoPointFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/GeoPointFieldMapper.java @@ -40,6 +40,7 @@ import org.elasticsearch.index.fielddata.plain.AbstractLatLonPointDVIndexFieldData; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.index.query.QueryShardException; +import org.elasticsearch.index.query.VectorGeoPointShapeQueryProcessor; import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; import org.elasticsearch.search.aggregations.support.ValuesSourceType; @@ -123,6 +124,14 @@ public GeoPointFieldMapper build(BuilderContext context, String simpleName, Mapp ignoreMalformed, ignoreZValue, copyTo); } + @Override + protected void setupFieldType(BuilderContext context) { + super.setupFieldType(context); + + GeoPointFieldType fieldType = (GeoPointFieldType)fieldType(); + fieldType.setGeometryQueryBuilder(new VectorGeoPointShapeQueryProcessor()); + } + @Override public GeoPointFieldMapper build(BuilderContext context) { return build(context, name, fieldType, defaultFieldType, context.indexSettings(), @@ -213,7 +222,7 @@ protected void parseCreateField(ParseContext context, List field throw new UnsupportedOperationException("Parsing is implemented in parse(), this method should NEVER be called"); } - public static class GeoPointFieldType extends MappedFieldType { + public static class GeoPointFieldType extends AbstractSearchableGeometryFieldType { public GeoPointFieldType() { } @@ -253,7 +262,8 @@ public Query existsQuery(QueryShardContext context) { @Override public Query termQuery(Object value, QueryShardContext context) { - throw new QueryShardException(context, "Geo fields do not support exact searching, use dedicated geo queries instead: [" + throw new QueryShardException(context, + "Geo fields do not support exact searching, use dedicated geo queries instead: [" + name() + "]"); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeIndexer.java b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeIndexer.java index 558b4068ffa90..0b7458de58b16 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeIndexer.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeIndexer.java @@ -24,6 +24,7 @@ import org.apache.lucene.index.IndexableField; import org.elasticsearch.common.geo.GeoLineDecomposer; import org.elasticsearch.common.geo.GeoPolygonDecomposer; +import org.elasticsearch.common.geo.GeoShapeType; import org.elasticsearch.geometry.Circle; import org.elasticsearch.geometry.Geometry; import org.elasticsearch.geometry.GeometryCollection; @@ -64,7 +65,7 @@ public Geometry prepareForIndexing(Geometry geometry) { return geometry.visit(new GeometryVisitor<>() { @Override public Geometry visit(Circle circle) { - throw new UnsupportedOperationException("CIRCLE geometry is not supported"); + throw new UnsupportedOperationException(GeoShapeType.CIRCLE + " geometry is not supported"); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/query/AbstractGeometryQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/AbstractGeometryQueryBuilder.java index 3a996bde56e52..d9c3464f1ba26 100644 --- a/server/src/main/java/org/elasticsearch/index/query/AbstractGeometryQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/AbstractGeometryQueryBuilder.java @@ -337,11 +337,9 @@ public boolean ignoreUnmapped() { } /** list of content types this shape query is compatible with */ - protected abstract List validContentTypes(); + protected abstract List validContentTypes(); /** builds the appropriate lucene shape query */ protected abstract Query buildShapeQuery(QueryShardContext context, MappedFieldType fieldType); - /** returns expected content type for this query */ - protected abstract String queryFieldType(); /** writes the xcontent specific to this shape query */ protected abstract void doShapeQueryXContent(XContentBuilder builder, Params params) throws IOException; /** creates a new ShapeQueryBuilder from the provided field name and shape builder */ @@ -365,7 +363,9 @@ protected Query doToQuery(QueryShardContext context) { if (ignoreUnmapped) { return new MatchNoDocsQuery(); } else { - throw new QueryShardException(context, "failed to find " + queryFieldType() + " field [" + fieldName + "]"); + throw new QueryShardException(context, "failed to find " + + String.join(" or ", validContentTypes()) + + " field [" + fieldName + "]"); } } diff --git a/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java index 78c727a90212b..d5d940b353f90 100644 --- a/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java @@ -34,12 +34,14 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.geometry.Geometry; -import org.elasticsearch.index.mapper.AbstractGeometryFieldMapper; +import org.elasticsearch.index.mapper.AbstractSearchableGeometryFieldType; +import org.elasticsearch.index.mapper.GeoPointFieldMapper; import org.elasticsearch.index.mapper.GeoShapeFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.Supplier; @@ -56,6 +58,12 @@ public class GeoShapeQueryBuilder extends AbstractGeometryQueryBuilder validContentTypes = + Collections.unmodifiableList( + Arrays.asList( + GeoShapeFieldMapper.CONTENT_TYPE, + GeoPointFieldMapper.CONTENT_TYPE)); + /** * Creates a new GeoShapeQueryBuilder whose Query will be against the given * field name using the given Shape @@ -168,13 +176,8 @@ public SpatialStrategy strategy() { } @Override - protected List validContentTypes() { - return Arrays.asList(GeoShapeFieldMapper.CONTENT_TYPE); - } - - @Override - public String queryFieldType() { - return GeoShapeFieldMapper.CONTENT_TYPE; + protected List validContentTypes() { + return validContentTypes; } @Override @@ -197,12 +200,15 @@ protected GeoShapeQueryBuilder newShapeQueryBuilder(String fieldName, Supplier { + QueryShardContext context; + MappedFieldType fieldType; + String fieldName; + ShapeRelation relation; + + ShapeVisitor(QueryShardContext context, String fieldName, ShapeRelation relation) { + this.context = context; + this.fieldType = context.fieldMapper(fieldName); + this.fieldName = fieldName; + this.relation = relation; + } + + @Override + public Query visit(Circle circle) { + Query query = LatLonPoint.newDistanceQuery( + fieldName, circle.getLat(), circle.getLon(), circle.getRadiusMeters()); + if (fieldType.hasDocValues()) { + Query dvQuery = LatLonDocValuesField.newSlowDistanceQuery( + fieldName, circle.getLat(), circle.getLon(), circle.getRadiusMeters()); + query = new IndexOrDocValuesQuery(query, dvQuery); + } + return query; + } + + @Override + public Query visit(GeometryCollection collection) { + BooleanQuery.Builder bqb = new BooleanQuery.Builder(); + visit(bqb, collection); + return bqb.build(); + } + + private void visit(BooleanQuery.Builder bqb, GeometryCollection collection) { + BooleanClause.Occur occur = BooleanClause.Occur.FILTER; + for (Geometry shape : collection) { + bqb.add(shape.visit(this), occur); + } + } + + @Override + public Query visit(org.elasticsearch.geometry.Line line) { + throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + + GeoShapeType.LINESTRING + " queries"); + } + + @Override + // don't think this is called directly + public Query visit(LinearRing ring) { + throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + + ShapeType.LINEARRING + " queries"); + } + + @Override + public Query visit(MultiLine multiLine) { + throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + + GeoShapeType.MULTILINESTRING + " queries"); + } + + @Override + public Query visit(MultiPoint multiPoint) { + throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + + GeoShapeType.MULTIPOINT + " queries"); + } + + // helper for visit(MultiPolygon multiPolygon) and visit(Polygon polygon) + private Query visit(ArrayList collector) { + org.apache.lucene.geo.Polygon[] lucenePolygons = + new org.apache.lucene.geo.Polygon[collector.size()]; + for (int i = 0; i < collector.size(); i++) { + lucenePolygons[i] = toLucenePolygon(collector.get(i)); + } + Query query = LatLonPoint.newPolygonQuery(fieldName, lucenePolygons); + if (fieldType.hasDocValues()) { + Query dvQuery = LatLonDocValuesField.newSlowPolygonQuery(fieldName, lucenePolygons); + query = new IndexOrDocValuesQuery(query, dvQuery); + } + return query; + } + + @Override + public Query visit(MultiPolygon multiPolygon) { + ArrayList collector = new ArrayList<>(); + GeoPolygonDecomposer.decomposeMultiPolygon(multiPolygon, true, collector); + return visit(collector); + } + + @Override + public Query visit(Point point) { + // not currently supported + throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + GeoShapeType.POINT + + " queries"); + } + + @Override + public Query visit(Polygon polygon) { + ArrayList collector = new ArrayList<>(); + GeoPolygonDecomposer.decomposePolygon(polygon, true, collector); + return visit(collector); + } + + @Override + public Query visit(Rectangle r) { + Query query = LatLonPoint.newBoxQuery(fieldName, r.getMinY(), r.getMaxY(), r.getMinX(), r.getMaxX()); + if (fieldType.hasDocValues()) { + Query dvQuery = LatLonDocValuesField.newSlowBoxQuery( + fieldName, r.getMinY(), r.getMaxY(), r.getMinX(), r.getMaxX()); + query = new IndexOrDocValuesQuery(query, dvQuery); + } + return query; + } + } +} + diff --git a/server/src/main/java/org/elasticsearch/index/query/VectorGeoShapeQueryProcessor.java b/server/src/main/java/org/elasticsearch/index/query/VectorGeoShapeQueryProcessor.java index 55ef5098bc4ca..4d79437bf239a 100644 --- a/server/src/main/java/org/elasticsearch/index/query/VectorGeoShapeQueryProcessor.java +++ b/server/src/main/java/org/elasticsearch/index/query/VectorGeoShapeQueryProcessor.java @@ -39,14 +39,14 @@ import org.elasticsearch.geometry.MultiPolygon; import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.Rectangle; -import org.elasticsearch.index.mapper.AbstractGeometryFieldMapper; +import org.elasticsearch.index.mapper.AbstractSearchableGeometryFieldType; import org.elasticsearch.index.mapper.GeoShapeFieldMapper; import org.elasticsearch.index.mapper.GeoShapeIndexer; import org.elasticsearch.index.mapper.MappedFieldType; import static org.elasticsearch.index.mapper.GeoShapeIndexer.toLucenePolygon; -public class VectorGeoShapeQueryProcessor implements AbstractGeometryFieldMapper.QueryProcessor { +public class VectorGeoShapeQueryProcessor implements AbstractSearchableGeometryFieldType.QueryProcessor { @Override public Query process(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context) { @@ -59,7 +59,8 @@ public Query process(Geometry shape, String fieldName, ShapeRelation relation, Q return getVectorQueryFromShape(shape, fieldName, relation, context); } - protected Query getVectorQueryFromShape(Geometry queryShape, String fieldName, ShapeRelation relation, QueryShardContext context) { + protected Query getVectorQueryFromShape( + Geometry queryShape, String fieldName, ShapeRelation relation, QueryShardContext context) { GeoShapeIndexer geometryIndexer = new GeoShapeIndexer(true, fieldName); Geometry processedShape = geometryIndexer.prepareForIndexing(queryShape); @@ -85,7 +86,7 @@ private class ShapeVisitor implements GeometryVisitor { @Override public Query visit(Circle circle) { - throw new QueryShardException(context, "Field [" + fieldName + "] found and unknown shape Circle"); + throw new QueryShardException(context, "Field [" + fieldName + "] found an unknown shape Circle"); } @Override @@ -117,7 +118,7 @@ public Query visit(org.elasticsearch.geometry.Line line) { @Override public Query visit(LinearRing ring) { - throw new QueryShardException(context, "Field [" + fieldName + "] found and unsupported shape LinearRing"); + throw new QueryShardException(context, "Field [" + fieldName + "] found an unsupported shape LinearRing"); } @Override diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeometryIndexerTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeometryIndexerTests.java index b00b53ec18271..f5828aad13ee1 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/GeometryIndexerTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/GeometryIndexerTests.java @@ -35,7 +35,6 @@ import org.elasticsearch.geometry.Polygon; import org.elasticsearch.index.mapper.GeoShapeIndexer; import org.elasticsearch.test.ESTestCase; - import java.io.IOException; import java.text.ParseException; import java.util.ArrayList; @@ -52,7 +51,7 @@ public class GeometryIndexerTests extends ESTestCase { public void testCircle() { UnsupportedOperationException ex = expectThrows(UnsupportedOperationException.class, () -> indexer.prepareForIndexing(new Circle(2, 1, 3))); - assertEquals("CIRCLE geometry is not supported", ex.getMessage()); + assertEquals(GeoShapeType.CIRCLE + " geometry is not supported", ex.getMessage()); } public void testCollection() { diff --git a/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderGeoPointTests.java b/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderGeoPointTests.java new file mode 100644 index 0000000000000..e7fb3a0ece67d --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderGeoPointTests.java @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.index.query; + +import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.elasticsearch.test.geo.RandomShapeGenerator; + +public class GeoShapeQueryBuilderGeoPointTests extends GeoShapeQueryBuilderTests { + + protected String fieldName() { + return GEO_POINT_FIELD_NAME; + } + + protected GeoShapeQueryBuilder doCreateTestQueryBuilder(boolean indexedShape) { + RandomShapeGenerator.ShapeType shapeType = RandomShapeGenerator.ShapeType.POLYGON; + ShapeBuilder shape = RandomShapeGenerator.createShapeWithin(random(), null, shapeType); + GeoShapeQueryBuilder builder; + clearShapeFields(); + if (indexedShape == false) { + builder = new GeoShapeQueryBuilder(fieldName(), shape); + } else { + indexedShapeToReturn = shape; + indexedShapeId = randomAlphaOfLengthBetween(3, 20); + builder = new GeoShapeQueryBuilder(fieldName(), indexedShapeId); + if (randomBoolean()) { + indexedShapeIndex = randomAlphaOfLengthBetween(3, 20); + builder.indexedShapeIndex(indexedShapeIndex); + } + if (randomBoolean()) { + indexedShapePath = randomAlphaOfLengthBetween(3, 20); + builder.indexedShapePath(indexedShapePath); + } + if (randomBoolean()) { + indexedShapeRouting = randomAlphaOfLengthBetween(3, 20); + builder.indexedShapeRouting(indexedShapeRouting); + } + } + if (randomBoolean()) { + builder.relation(randomFrom(ShapeRelation.INTERSECTS)); + } + + if (randomBoolean()) { + builder.ignoreUnmapped(randomBoolean()); + } + return builder; + } + +} diff --git a/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderGeoShapeTests.java b/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderGeoShapeTests.java new file mode 100644 index 0000000000000..36b438994c076 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderGeoShapeTests.java @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.index.query; + +import org.elasticsearch.Version; +import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.elasticsearch.test.geo.RandomShapeGenerator; + +public class GeoShapeQueryBuilderGeoShapeTests extends GeoShapeQueryBuilderTests { + + protected String fieldName() { + return GEO_SHAPE_FIELD_NAME; + } + + protected GeoShapeQueryBuilder doCreateTestQueryBuilder(boolean indexedShape) { + RandomShapeGenerator.ShapeType shapeType = randomFrom( + RandomShapeGenerator.ShapeType.POINT, + RandomShapeGenerator.ShapeType.MULTIPOINT, + RandomShapeGenerator.ShapeType.LINESTRING, + RandomShapeGenerator.ShapeType.MULTILINESTRING, + RandomShapeGenerator.ShapeType.POLYGON); + ShapeBuilder shape = RandomShapeGenerator.createShapeWithin(random(), null, shapeType); + GeoShapeQueryBuilder builder; + clearShapeFields(); + if (indexedShape == false) { + builder = new GeoShapeQueryBuilder(fieldName(), shape); + } else { + indexedShapeToReturn = shape; + indexedShapeId = randomAlphaOfLengthBetween(3, 20); + builder = new GeoShapeQueryBuilder(fieldName(), indexedShapeId); + if (randomBoolean()) { + indexedShapeIndex = randomAlphaOfLengthBetween(3, 20); + builder.indexedShapeIndex(indexedShapeIndex); + } + if (randomBoolean()) { + indexedShapePath = randomAlphaOfLengthBetween(3, 20); + builder.indexedShapePath(indexedShapePath); + } + if (randomBoolean()) { + indexedShapeRouting = randomAlphaOfLengthBetween(3, 20); + builder.indexedShapeRouting(indexedShapeRouting); + } + } + if (randomBoolean()) { + QueryShardContext context = createShardContext(); + if (context.indexVersionCreated().onOrAfter(Version.V_7_5_0)) { // CONTAINS is only supported from version 7.5 + if (shapeType == RandomShapeGenerator.ShapeType.LINESTRING || shapeType == RandomShapeGenerator.ShapeType.MULTILINESTRING) { + builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, ShapeRelation.CONTAINS)); + } else { + builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, + ShapeRelation.WITHIN, ShapeRelation.CONTAINS)); + } + } else { + if (shapeType == RandomShapeGenerator.ShapeType.LINESTRING || shapeType == RandomShapeGenerator.ShapeType.MULTILINESTRING) { + builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS)); + } else { + builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, ShapeRelation.WITHIN)); + } + } + } + + if (randomBoolean()) { + builder.ignoreUnmapped(randomBoolean()); + } + return builder; + } +} diff --git a/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderTests.java index 07abc89ee430f..c69006023818f 100644 --- a/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderTests.java @@ -29,7 +29,6 @@ import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.geo.builders.EnvelopeBuilder; import org.elasticsearch.common.geo.builders.ShapeBuilder; import org.elasticsearch.common.io.stream.BytesStreamOutput; @@ -48,13 +47,13 @@ import java.io.IOException; -import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.matchesPattern; -public class GeoShapeQueryBuilderTests extends AbstractQueryTestCase { +public abstract class GeoShapeQueryBuilderTests extends AbstractQueryTestCase { protected static String indexedShapeId; protected static String indexedShapePath; @@ -62,9 +61,7 @@ public class GeoShapeQueryBuilderTests extends AbstractQueryTestCase indexedShapeToReturn; - protected String fieldName() { - return GEO_SHAPE_FIELD_NAME; - } + protected abstract String fieldName(); @Override protected Settings createTestIndexSettings() { @@ -81,54 +78,7 @@ protected GeoShapeQueryBuilder doCreateTestQueryBuilder() { return doCreateTestQueryBuilder(randomBoolean()); } - protected GeoShapeQueryBuilder doCreateTestQueryBuilder(boolean indexedShape) { - RandomShapeGenerator.ShapeType shapeType = - randomFrom(ShapeType.POINT, ShapeType.MULTIPOINT, ShapeType.LINESTRING, ShapeType.MULTILINESTRING, ShapeType.POLYGON); - ShapeBuilder shape = RandomShapeGenerator.createShapeWithin(random(), null, shapeType); - GeoShapeQueryBuilder builder; - clearShapeFields(); - if (indexedShape == false) { - builder = new GeoShapeQueryBuilder(fieldName(), shape); - } else { - indexedShapeToReturn = shape; - indexedShapeId = randomAlphaOfLengthBetween(3, 20); - builder = new GeoShapeQueryBuilder(fieldName(), indexedShapeId); - if (randomBoolean()) { - indexedShapeIndex = randomAlphaOfLengthBetween(3, 20); - builder.indexedShapeIndex(indexedShapeIndex); - } - if (randomBoolean()) { - indexedShapePath = randomAlphaOfLengthBetween(3, 20); - builder.indexedShapePath(indexedShapePath); - } - if (randomBoolean()) { - indexedShapeRouting = randomAlphaOfLengthBetween(3, 20); - builder.indexedShapeRouting(indexedShapeRouting); - } - } - if (randomBoolean()) { - QueryShardContext context = createShardContext(); - if (context.indexVersionCreated().onOrAfter(Version.V_7_5_0)) { // CONTAINS is only supported from version 7.5 - if (shapeType == ShapeType.LINESTRING || shapeType == ShapeType.MULTILINESTRING) { - builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, ShapeRelation.CONTAINS)); - } else { - builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, - ShapeRelation.WITHIN, ShapeRelation.CONTAINS)); - } - } else { - if (shapeType == ShapeType.LINESTRING || shapeType == ShapeType.MULTILINESTRING) { - builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS)); - } else { - builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, ShapeRelation.WITHIN)); - } - } - } - - if (randomBoolean()) { - builder.ignoreUnmapped(randomBoolean()); - } - return builder; - } + abstract GeoShapeQueryBuilder doCreateTestQueryBuilder(boolean indexedShape); @Override protected GetResponse executeGet(GetRequest getRequest) { @@ -276,7 +226,7 @@ public void testIgnoreUnmapped() throws IOException { new GeoShapeQueryBuilder("unmapped", shape.buildGeometry()); failingQueryBuilder.ignoreUnmapped(false); QueryShardException e = expectThrows(QueryShardException.class, () -> failingQueryBuilder.toQuery(createShardContext())); - assertThat(e.getMessage(), containsString("failed to find geo_shape field [unmapped]")); + assertThat(e.getMessage(), matchesPattern("failed to find .*geo_shape.* field \\[unmapped\\]")); } public void testWrongFieldType() throws IOException { @@ -286,7 +236,8 @@ public void testWrongFieldType() throws IOException { new GeoShapeQueryBuilder(STRING_FIELD_NAME, shape) : new GeoShapeQueryBuilder(STRING_FIELD_NAME, shape.buildGeometry()); QueryShardException e = expectThrows(QueryShardException.class, () -> queryBuilder.toQuery(createShardContext())); - assertThat(e.getMessage(), containsString("Field [mapped_string] is not of type [geo_shape] but of type [text]")); + assertThat(e.getMessage(), matchesPattern("Field \\[mapped_string\\] is of unsupported type \\[text\\]." + + " \\[geo_shape\\] query supports the following types \\[.*geo_shape.*\\]")); } public void testSerializationFailsUnlessFetched() throws IOException { diff --git a/server/src/test/java/org/elasticsearch/search/geo/GeoPointShapeQueryTests.java b/server/src/test/java/org/elasticsearch/search/geo/GeoPointShapeQueryTests.java index 51702c6d08c6c..1715194f62079 100644 --- a/server/src/test/java/org/elasticsearch/search/geo/GeoPointShapeQueryTests.java +++ b/server/src/test/java/org/elasticsearch/search/geo/GeoPointShapeQueryTests.java @@ -19,23 +19,158 @@ package org.elasticsearch.search.geo; +import org.elasticsearch.action.search.SearchAction; +import org.elasticsearch.action.search.SearchPhaseExecutionException; +import org.elasticsearch.action.search.SearchRequestBuilder; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.geo.GeoShapeType; +import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.common.geo.builders.CoordinatesBuilder; +import org.elasticsearch.common.geo.builders.LineStringBuilder; +import org.elasticsearch.common.geo.builders.MultiLineStringBuilder; +import org.elasticsearch.common.geo.builders.MultiPointBuilder; +import org.elasticsearch.common.geo.builders.PointBuilder; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.geometry.Line; +import org.elasticsearch.geometry.LinearRing; +import org.elasticsearch.geometry.MultiLine; +import org.elasticsearch.geometry.MultiPoint; +import org.elasticsearch.geometry.Point; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.index.query.GeoShapeQueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; + +import static org.hamcrest.Matchers.containsString; public class GeoPointShapeQueryTests extends GeoQueryTests { @Override protected XContentBuilder createDefaultMapping() throws Exception { XContentBuilder xcb = XContentFactory.jsonBuilder().startObject() - .startObject("properties").startObject("location") + .startObject("properties").startObject(defaultGeoFieldName) .field("type", "geo_point") .endObject().endObject().endObject(); return xcb; } - public void testIndexPointsFilterRectangle() throws Exception { - //super.testIndexPointsFilterRectangle(Strings.toString(createDefaultMapping())); + public void testProcessRelationSupport() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate("test").setMapping(mapping).get(); + ensureGreen(); + + Rectangle rectangle = new Rectangle(-35, -25, -25, -35); + + for (ShapeRelation shapeRelation : ShapeRelation.values()) { + if (!shapeRelation.equals(ShapeRelation.INTERSECTS)) { + SearchPhaseExecutionException e = expectThrows(SearchPhaseExecutionException.class, () -> + client().prepareSearch("test") + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, rectangle) + .relation(shapeRelation)) + .get()); + assertThat(e.getCause().getMessage(), + containsString(shapeRelation + + " query relation not supported for Field [" + defaultGeoFieldName + "]")); + } + } + } + + public void testQueryLine() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate("test").setMapping(mapping).get(); + ensureGreen(); + + Line line = new Line(new double[]{-25, -25}, new double[]{-35, -35}); + + try { + client().prepareSearch("test") + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, line)).get(); + } catch ( + SearchPhaseExecutionException e) { + assertThat(e.getCause().getMessage(), + containsString("does not support " + GeoShapeType.LINESTRING + " queries")); + } + } + + public void testQueryLinearRing() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate("test").setMapping(mapping).get(); + ensureGreen(); + + LinearRing linearRing = new LinearRing(new double[]{-25,-35,-25}, new double[]{-25,-35,-25}); + + try { + // LinearRing extends Line implements Geometry: expose the build process + GeoShapeQueryBuilder queryBuilder = new GeoShapeQueryBuilder(defaultGeoFieldName, linearRing); + SearchRequestBuilder searchRequestBuilder = new SearchRequestBuilder(client(), SearchAction.INSTANCE); + searchRequestBuilder.setQuery(queryBuilder); + searchRequestBuilder.setIndices("test"); + searchRequestBuilder.get(); + } catch ( + SearchPhaseExecutionException e) { + assertThat(e.getCause().getMessage(), + containsString("Field [" + defaultGeoFieldName + "] does not support LINEARRING queries")); + } + } + + public void testQueryMultiLine() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate("test").setMapping(mapping).get(); + ensureGreen(); + + CoordinatesBuilder coords1 = new CoordinatesBuilder() + .coordinate(-35,-35) + .coordinate(-25,-25); + CoordinatesBuilder coords2 = new CoordinatesBuilder() + .coordinate(-15,-15) + .coordinate(-5,-5); + LineStringBuilder lsb1 = new LineStringBuilder(coords1); + LineStringBuilder lsb2 = new LineStringBuilder(coords2); + MultiLineStringBuilder mlb = new MultiLineStringBuilder().linestring(lsb1).linestring(lsb2); + MultiLine multiline = (MultiLine) mlb.buildGeometry(); + + try { + client().prepareSearch("test") + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, multiline)).get(); + } catch (Exception e) { + assertThat(e.getCause().getMessage(), + containsString("does not support " + GeoShapeType.MULTILINESTRING + " queries")); + } + } + + public void testQueryMultiPoint() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate("test").setMapping(mapping).get(); + ensureGreen(); + + MultiPointBuilder mpb = new MultiPointBuilder().coordinate(-35,-25).coordinate(-15,-5); + MultiPoint multiPoint = mpb.buildGeometry(); + + try { + client().prepareSearch("test") + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, multiPoint)).get(); + } catch (Exception e) { + assertThat(e.getCause().getMessage(), + containsString("does not support " + GeoShapeType.MULTIPOINT + " queries")); + } + } + + public void testQueryPoint() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate("test").setMapping(mapping).get(); + ensureGreen(); + + PointBuilder pb = new PointBuilder().coordinate(-35, -25); + Point point = pb.buildGeometry(); + + try { + client().prepareSearch("test") + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, point)).get(); + } catch (Exception e) { + assertThat(e.getCause().getMessage(), + containsString("does not support " + GeoShapeType.POINT + " queries")); + } } } diff --git a/server/src/test/java/org/elasticsearch/search/geo/GeoQueryTests.java b/server/src/test/java/org/elasticsearch/search/geo/GeoQueryTests.java index 6b00f4a24ebb4..8fce489319e1d 100644 --- a/server/src/test/java/org/elasticsearch/search/geo/GeoQueryTests.java +++ b/server/src/test/java/org/elasticsearch/search/geo/GeoQueryTests.java @@ -22,59 +22,76 @@ import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.geo.GeoShapeType; +import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.common.geo.builders.CircleBuilder; +import org.elasticsearch.common.geo.builders.CoordinatesBuilder; import org.elasticsearch.common.geo.builders.EnvelopeBuilder; +import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder; +import org.elasticsearch.common.geo.builders.MultiPolygonBuilder; +import org.elasticsearch.common.geo.builders.PolygonBuilder; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.index.query.GeoShapeQueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.search.SearchHits; import org.elasticsearch.test.ESSingleNodeTestCase; import org.locationtech.jts.geom.Coordinate; import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; -import static org.elasticsearch.index.query.QueryBuilders.geoIntersectionQuery; -import static org.elasticsearch.index.query.QueryBuilders.geoShapeQuery; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; public abstract class GeoQueryTests extends ESSingleNodeTestCase { protected abstract XContentBuilder createDefaultMapping() throws Exception; + static String defaultGeoFieldName = "geo"; + static String defaultIndexName = "test"; + public void testNullShape() throws Exception { String mapping = Strings.toString(createDefaultMapping()); - client().admin().indices().prepareCreate("test").setMapping(mapping).get(); + client().admin().indices().prepareCreate(defaultIndexName).setMapping(mapping).get(); ensureGreen(); - client().prepareIndex("test").setId("aNullshape").setSource("{\"geo\": null}", XContentType.JSON) + client().prepareIndex(defaultIndexName) + .setId("aNullshape") + .setSource("{\"geo\": null}", XContentType.JSON) .setRefreshPolicy(IMMEDIATE).get(); - GetResponse result = client().prepareGet("test", "aNullshape").get(); + GetResponse result = client().prepareGet(defaultIndexName, "aNullshape").get(); assertThat(result.getField("location"), nullValue()); }; - public void testIndexPointsFilterRectangle(String mapping) throws Exception { - client().admin().indices().prepareCreate("test").setMapping(mapping).get(); + public void testIndexPointsFilterRectangle() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate(defaultIndexName).setMapping(mapping).get(); ensureGreen(); - client().prepareIndex("test").setId("1").setSource(jsonBuilder().startObject() - .field("name", "Document 1") - .startObject("geo") - .field("type", "point") - .startArray("coordinates").value(-30).value(-30).endArray() - .endObject() + client().prepareIndex(defaultIndexName).setId("1").setSource(jsonBuilder() + .startObject() + .field("name", "Document 1") + .field(defaultGeoFieldName, "POINT(-30 -30)") .endObject()).setRefreshPolicy(IMMEDIATE).get(); - client().prepareIndex("test").setId("2").setSource(jsonBuilder().startObject() - .field("name", "Document 2") - .startObject("geo") - .field("type", "point") - .startArray("coordinates").value(-45).value(-50).endArray() - .endObject() + client().prepareIndex(defaultIndexName).setId("2").setSource(jsonBuilder() + .startObject() + .field("name", "Document 2") + .field(defaultGeoFieldName, "POINT(-45 -50)") .endObject()).setRefreshPolicy(IMMEDIATE).get(); EnvelopeBuilder shape = new EnvelopeBuilder(new Coordinate(-45, 45), new Coordinate(45, -45)); - - SearchResponse searchResponse = client().prepareSearch("test") - .setQuery(geoIntersectionQuery("geo", shape)) + GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(shape); + Geometry geometry = builder.buildGeometry().get(0); + SearchResponse searchResponse = client().prepareSearch(defaultIndexName) + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, geometry) + .relation(ShapeRelation.INTERSECTS)) .get(); assertSearchResponse(searchResponse); @@ -82,8 +99,9 @@ public void testIndexPointsFilterRectangle(String mapping) throws Exception { assertThat(searchResponse.getHits().getHits().length, equalTo(1)); assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("1")); - searchResponse = client().prepareSearch("test") - .setQuery(geoShapeQuery("geo", shape)) + // default query, without specifying relation (expect intersects) + searchResponse = client().prepareSearch(defaultIndexName) + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, geometry)) .get(); assertSearchResponse(searchResponse); @@ -92,4 +110,335 @@ public void testIndexPointsFilterRectangle(String mapping) throws Exception { assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("1")); } + public void testIndexPointsCircle() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate(defaultIndexName).setMapping(mapping).get(); + ensureGreen(); + + client().prepareIndex(defaultIndexName).setId("1").setSource(jsonBuilder() + .startObject() + .field("name", "Document 1") + .field(defaultGeoFieldName, "POINT(-30 -30)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).setId("2").setSource(jsonBuilder() + .startObject() + .field("name", "Document 2") + .field(defaultGeoFieldName, "POINT(-45 -50)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + CircleBuilder shape = new CircleBuilder().center(new Coordinate(-30, -30)).radius("100m"); + GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(shape); + Geometry geometry = builder.buildGeometry().get(0); + + try { + client().prepareSearch(defaultIndexName) + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, geometry) + .relation(ShapeRelation.INTERSECTS)) + .get(); + } catch ( + Exception e) { + assertThat(e.getCause().getMessage(), + containsString("failed to create query: " + + GeoShapeType.CIRCLE + " geometry is not supported")); + } + } + + public void testIndexPointsPolygon() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate(defaultIndexName).setMapping(mapping).get(); + ensureGreen(); + + client().prepareIndex(defaultIndexName).setId("1").setSource(jsonBuilder() + .startObject() + .field(defaultGeoFieldName, "POINT(-30 -30)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).setId("2").setSource(jsonBuilder() + .startObject() + .field(defaultGeoFieldName, "POINT(-45 -50)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + CoordinatesBuilder cb = new CoordinatesBuilder(); + cb.coordinate(new Coordinate(-35, -35)) + .coordinate(new Coordinate(-35, -25)) + .coordinate(new Coordinate(-25, -25)) + .coordinate(new Coordinate(-25, -35)) + .coordinate(new Coordinate(-35, -35)); + PolygonBuilder shape = new PolygonBuilder(cb); + GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(shape); + Geometry geometry = builder.buildGeometry(); + SearchResponse searchResponse = client().prepareSearch(defaultIndexName) + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, geometry) + .relation(ShapeRelation.INTERSECTS)) + .get(); + + assertSearchResponse(searchResponse); + SearchHits searchHits = searchResponse.getHits(); + assertThat(searchHits.getTotalHits().value, equalTo(1L)); + assertThat(searchHits.getAt(0).getId(), equalTo("1")); + } + + public void testIndexPointsMultiPolygon() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate(defaultIndexName).setMapping(mapping).get(); + ensureGreen(); + + client().prepareIndex(defaultIndexName).setId("1").setSource(jsonBuilder() + .startObject() + .field("name", "Document 1") + .field(defaultGeoFieldName, "POINT(-30 -30)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).setId("2").setSource(jsonBuilder() + .startObject() + .field("name", "Document 2") + .field(defaultGeoFieldName, "POINT(-40 -40)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).setId("3").setSource(jsonBuilder() + .startObject() + .field("name", "Document 3") + .field(defaultGeoFieldName, "POINT(-50 -50)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + CoordinatesBuilder encloseDocument1Cb = new CoordinatesBuilder(); + encloseDocument1Cb.coordinate(new Coordinate(-35, -35)) + .coordinate(new Coordinate(-35, -25)) + .coordinate(new Coordinate(-25, -25)) + .coordinate(new Coordinate(-25, -35)) + .coordinate(new Coordinate(-35, -35)); + PolygonBuilder encloseDocument1Shape = new PolygonBuilder(encloseDocument1Cb); + + CoordinatesBuilder encloseDocument2Cb = new CoordinatesBuilder(); + encloseDocument2Cb.coordinate(new Coordinate(-55, -55)) + .coordinate(new Coordinate(-55, -45)) + .coordinate(new Coordinate(-45, -45)) + .coordinate(new Coordinate(-45, -55)) + .coordinate(new Coordinate(-55, -55)); + PolygonBuilder encloseDocument2Shape = new PolygonBuilder(encloseDocument2Cb); + + MultiPolygonBuilder mp = new MultiPolygonBuilder(); + mp.polygon(encloseDocument1Shape).polygon(encloseDocument2Shape); + + GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(mp); + Geometry geometry = builder.buildGeometry(); + SearchResponse searchResponse = client().prepareSearch(defaultIndexName) + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, geometry) + .relation(ShapeRelation.INTERSECTS)) + .get(); + + assertSearchResponse(searchResponse); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(2L)); + assertThat(searchResponse.getHits().getHits().length, equalTo(2)); + assertThat(searchResponse.getHits().getAt(0).getId(), not(equalTo("2"))); + assertThat(searchResponse.getHits().getAt(1).getId(), not(equalTo("2"))); + } + + public void testIndexPointsRectangle() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate(defaultIndexName).setMapping(mapping).get(); + ensureGreen(); + + client().prepareIndex(defaultIndexName).setId("1").setSource(jsonBuilder() + .startObject() + .field("name", "Document 1") + .field(defaultGeoFieldName, "POINT(-30 -30)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).setId("2").setSource(jsonBuilder() + .startObject() + .field("name", "Document 2") + .field(defaultGeoFieldName, "POINT(-45 -50)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + Rectangle rectangle = new Rectangle(-50, -40, -45, -55); + + SearchResponse searchResponse = client().prepareSearch(defaultIndexName) + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, rectangle) + .relation(ShapeRelation.INTERSECTS)) + .get(); + + assertSearchResponse(searchResponse); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + assertThat(searchResponse.getHits().getHits().length, equalTo(1)); + assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("2")); + } + + public void testIndexPointsIndexedRectangle() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate(defaultIndexName).setMapping(mapping).get(); + ensureGreen(); + + client().prepareIndex(defaultIndexName).setId("point1").setSource(jsonBuilder() + .startObject() + .field(defaultGeoFieldName, "POINT(-30 -30)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).setId("point2").setSource(jsonBuilder() + .startObject() + .field(defaultGeoFieldName, "POINT(-45 -50)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + String indexedShapeIndex = "indexed_query_shapes"; + String indexedShapePath = "shape"; + String queryShapesMapping = Strings.toString(XContentFactory.jsonBuilder().startObject() + .startObject("properties").startObject(indexedShapePath) + .field("type", "geo_shape") + .endObject() + .endObject() + .endObject()); + client().admin().indices().prepareCreate(indexedShapeIndex).setMapping(queryShapesMapping).get(); + ensureGreen(); + + client().prepareIndex(indexedShapeIndex).setId("shape1").setSource(jsonBuilder() + .startObject() + .field(indexedShapePath, "BBOX(-50, -40, -45, -55)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(indexedShapeIndex).setId("shape2").setSource(jsonBuilder() + .startObject() + .field(indexedShapePath, "BBOX(-60, -50, -50, -60)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + SearchResponse searchResponse = client().prepareSearch(defaultIndexName) + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, "shape1") + .relation(ShapeRelation.INTERSECTS) + .indexedShapeIndex(indexedShapeIndex) + .indexedShapePath(indexedShapePath)) + .get(); + + assertSearchResponse(searchResponse); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + assertThat(searchResponse.getHits().getHits().length, equalTo(1)); + assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("point2")); + + searchResponse = client().prepareSearch(defaultIndexName) + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, "shape2") + .relation(ShapeRelation.INTERSECTS) + .indexedShapeIndex(indexedShapeIndex) + .indexedShapePath(indexedShapePath)) + .get(); + assertSearchResponse(searchResponse); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(0L)); + } + + public void testRectangleSpanningDateline() throws Exception { + XContentBuilder mapping = createDefaultMapping(); + client().admin().indices().prepareCreate("test").setMapping(mapping).get(); + ensureGreen(); + + client().prepareIndex(defaultIndexName).setId("1").setSource(jsonBuilder() + .startObject() + .field(defaultGeoFieldName, "POINT(-169 0)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).setId("2").setSource(jsonBuilder() + .startObject() + .field(defaultGeoFieldName, "POINT(-179 0)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).setId("3").setSource(jsonBuilder() + .startObject() + .field(defaultGeoFieldName, "POINT(171 0)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + Rectangle rectangle = new Rectangle( + 169, -178, 1, -1); + + GeoShapeQueryBuilder geoShapeQueryBuilder = QueryBuilders.geoShapeQuery("geo", rectangle); + SearchResponse response = client().prepareSearch("test").setQuery(geoShapeQueryBuilder).get(); + SearchHits searchHits = response.getHits(); + assertEquals(2, searchHits.getTotalHits().value); + assertNotEquals("1", searchHits.getAt(0).getId()); + assertNotEquals("1", searchHits.getAt(1).getId()); + } + + public void testPolygonSpanningDateline() throws Exception { + XContentBuilder mapping = createDefaultMapping(); + client().admin().indices().prepareCreate("test").setMapping(mapping).get(); + ensureGreen(); + + client().prepareIndex(defaultIndexName).setId("1").setSource(jsonBuilder() + .startObject() + .field(defaultGeoFieldName, "POINT(-169 7)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).setId("2").setSource(jsonBuilder() + .startObject() + .field(defaultGeoFieldName, "POINT(-179 7)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).setId("3").setSource(jsonBuilder() + .startObject() + .field(defaultGeoFieldName, "POINT(179 7)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).setId("4").setSource(jsonBuilder() + .startObject() + .field(defaultGeoFieldName, "POINT(171 7)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + PolygonBuilder polygon = new PolygonBuilder(new CoordinatesBuilder() + .coordinate(-177, 10) + .coordinate(177, 10) + .coordinate(177, 5) + .coordinate(-177, 5) + .coordinate(-177, 10)); + + GeoShapeQueryBuilder geoShapeQueryBuilder = QueryBuilders.geoShapeQuery("geo", polygon.buildGeometry()); + geoShapeQueryBuilder.relation(ShapeRelation.INTERSECTS); + SearchResponse response = client().prepareSearch("test").setQuery(geoShapeQueryBuilder).get(); + SearchHits searchHits = response.getHits(); + assertEquals(2, searchHits.getTotalHits().value); + assertNotEquals("1", searchHits.getAt(0).getId()); + assertNotEquals("4", searchHits.getAt(0).getId()); + assertNotEquals("1", searchHits.getAt(1).getId()); + assertNotEquals("4", searchHits.getAt(1).getId()); + } + + public void testMultiPolygonSpanningDateline() throws Exception { + XContentBuilder mapping = createDefaultMapping(); + client().admin().indices().prepareCreate("test").setMapping(mapping).get(); + ensureGreen(); + + client().prepareIndex(defaultIndexName).setId("1").setSource(jsonBuilder() + .startObject() + .field(defaultGeoFieldName, "POINT(-169 7)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).setId("2").setSource(jsonBuilder() + .startObject() + .field(defaultGeoFieldName, "POINT(-179 7)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).setId("3").setSource(jsonBuilder() + .startObject() + .field(defaultGeoFieldName, "POINT(171 7)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + MultiPolygonBuilder multiPolygon = new MultiPolygonBuilder() + .polygon(new PolygonBuilder(new CoordinatesBuilder() + .coordinate(-167, 10) + .coordinate(-171, 10) + .coordinate(171, 5) + .coordinate(-167, 5) + .coordinate(-167, 10))) + .polygon(new PolygonBuilder(new CoordinatesBuilder() + .coordinate(-177, 10) + .coordinate(177, 10) + .coordinate(177, 5) + .coordinate(-177, 5) + .coordinate(-177, 10))); + + GeoShapeQueryBuilder geoShapeQueryBuilder = QueryBuilders.geoShapeQuery( + "geo", + multiPolygon.buildGeometry()); + geoShapeQueryBuilder.relation(ShapeRelation.INTERSECTS); + SearchResponse response = client().prepareSearch("test").setQuery(geoShapeQueryBuilder).get(); + SearchHits searchHits = response.getHits(); + assertEquals(2, searchHits.getTotalHits().value); + assertNotEquals("3", searchHits.getAt(0).getId()); + assertNotEquals("3", searchHits.getAt(1).getId()); + } } diff --git a/server/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java b/server/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java index df2a044159f9f..afb87348a541f 100644 --- a/server/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java +++ b/server/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java @@ -109,10 +109,6 @@ protected XContentBuilder createRandomMapping() throws Exception { return xcb; } - public void testIndexPointsFilterRectangle() throws Exception { - super.testIndexPointsFilterRectangle(Strings.toString(createRandomMapping())); - } - public void testShapeFetchingPath() throws Exception { createIndex("shapes"); String mapping = Strings.toString(createDefaultMapping()); diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilder.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilder.java index 408da2ed69943..a92e827b8f66e 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilder.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilder.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.Supplier; @@ -35,6 +36,9 @@ public class ShapeQueryBuilder extends AbstractGeometryQueryBuilder { public static final String NAME = "shape"; + protected static final List validContentTypes = + Collections.unmodifiableList(Arrays.asList(ShapeFieldMapper.CONTENT_TYPE)); + /** * Creates a new GeoShapeQueryBuilder whose Query will be against the given * field name using the given Shape @@ -100,23 +104,20 @@ protected ShapeQueryBuilder newShapeQueryBuilder(String fieldName, Supplier validContentTypes(){ + return validContentTypes; } @Override @SuppressWarnings({ "rawtypes" }) public Query buildShapeQuery(QueryShardContext context, MappedFieldType fieldType) { - if (fieldType.typeName().equals(ShapeFieldMapper.CONTENT_TYPE) == false) { + List validContentTypes = validContentTypes(); + if (validContentTypes.contains(fieldType.typeName()) == false) { throw new QueryShardException(context, - "Field [" + fieldName + "] is not of type [" + queryFieldType() + "] but of type [" + fieldType.typeName() + "]"); + "Field [" + fieldName + "] is not of type [" + String.join(" or ", validContentTypes()) + + "] but of type [" + fieldType.typeName() + "]"); } final AbstractGeometryFieldMapper.AbstractGeometryFieldType ft = (AbstractGeometryFieldMapper.AbstractGeometryFieldType) fieldType; diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java index 006577bde0266..da9bc32355bac 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java @@ -28,7 +28,7 @@ import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.Polygon; import org.elasticsearch.geometry.Rectangle; -import org.elasticsearch.index.mapper.AbstractGeometryFieldMapper; +import org.elasticsearch.index.mapper.AbstractSearchableGeometryFieldType; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.index.query.QueryShardException; @@ -36,7 +36,7 @@ import static org.elasticsearch.xpack.spatial.index.mapper.ShapeIndexer.toLucenePolygon; -public class ShapeQueryProcessor implements AbstractGeometryFieldMapper.QueryProcessor { +public class ShapeQueryProcessor implements AbstractSearchableGeometryFieldType.QueryProcessor { @Override public Query process(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context) {