diff --git a/docs/reference/mapping/types/shape.asciidoc b/docs/reference/mapping/types/shape.asciidoc index 10695be364a50..7580f756bc162 100644 --- a/docs/reference/mapping/types/shape.asciidoc +++ b/docs/reference/mapping/types/shape.asciidoc @@ -11,6 +11,9 @@ with arbitrary `x, y` cartesian shapes such as rectangles and polygons. It can b used to index and query geometries whose coordinates fall in a 2-dimensional planar coordinate system. +You can query documents using this type using +<>. + [[shape-mapping-options]] [float] ==== Mapping Options diff --git a/docs/reference/query-dsl.asciidoc b/docs/reference/query-dsl.asciidoc index 1a279101531c2..58ebe3190a352 100644 --- a/docs/reference/query-dsl.asciidoc +++ b/docs/reference/query-dsl.asciidoc @@ -35,6 +35,8 @@ include::query-dsl/full-text-queries.asciidoc[] include::query-dsl/geo-queries.asciidoc[] +include::query-dsl/shape-queries.asciidoc[] + include::query-dsl/joining-queries.asciidoc[] include::query-dsl/match-all-query.asciidoc[] diff --git a/docs/reference/query-dsl/shape-queries.asciidoc b/docs/reference/query-dsl/shape-queries.asciidoc new file mode 100644 index 0000000000000..204ebab9cecef --- /dev/null +++ b/docs/reference/query-dsl/shape-queries.asciidoc @@ -0,0 +1,18 @@ +[[shape-queries]] +[role="xpack"] +[testenv="basic"] +== Shape queries + +Like <> Elasticsearch supports the ability to index +arbitrary two dimension (non Geospatial) geometries making it possible to +map out virtual worlds, sporting venues, theme parks, and CAD diagrams. The +<> field type supports points, lines, polygons, multi-polygons, +envelope, etc. + +The queries in this group are: + +<> query:: +Finds documents with shapes that either intersect, are within, or do not +intersect a specified shape. + +include::shape-query.asciidoc[] diff --git a/docs/reference/query-dsl/shape-query.asciidoc b/docs/reference/query-dsl/shape-query.asciidoc new file mode 100644 index 0000000000000..d90730780159c --- /dev/null +++ b/docs/reference/query-dsl/shape-query.asciidoc @@ -0,0 +1,149 @@ +[[query-dsl-shape-query]] +[role="xpack"] +[testenv="basic"] +=== Shape query +++++ +Shape +++++ + +Queries documents that contain fields indexed using the `shape` type. + +Requires the <>. + +The query supports two ways of defining the target shape, either by +providing a whole shape definition, or by referencing the name, or id, of a shape +pre-indexed in another index. Both formats are defined below with +examples. + +==== Inline Shape Definition + +Similar to the `geo_shape` query, the `shape` query uses +http://www.geojson.org[GeoJSON] or +https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry[Well Known Text] +(WKT) to represent shapes. + +Given the following index: + +[source,js] +-------------------------------------------------- +PUT /example +{ + "mappings": { + "properties": { + "geometry": { + "type": "shape" + } + } + } +} + +POST /example/_doc?refresh +{ + "name": "Lucky Landing", + "location": { + "type": "point", + "coordinates": [1355.400544, 5255.530286] + } +} +-------------------------------------------------- +// CONSOLE +// TESTSETUP + +The following query will find the point using the Elasticsearch's +`envelope` GeoJSON extension: + +[source,js] +-------------------------------------------------- +GET /example/_search +{ + "query":{ + "shape": { + "geometry": { + "shape": { + "type": "envelope", + "coordinates" : [[1355.0, 5355.0], [1400.0, 5200.0]] + }, + "relation": "within" + } + } + } +} +-------------------------------------------------- +// CONSOLE + +==== Pre-Indexed Shape + +The Query also supports using a shape which has already been indexed in +another index. This is particularly useful for when +you have a pre-defined list of shapes which are useful to your +application and you want to reference this using a logical name (for +example 'New Zealand') rather than having to provide their coordinates +each time. In this situation it is only necessary to provide: + +* `id` - The ID of the document that containing the pre-indexed shape. +* `index` - Name of the index where the pre-indexed shape is. Defaults +to 'shapes'. +* `path` - The field specified as path containing the pre-indexed shape. +Defaults to 'shape'. +* `routing` - The routing of the shape document if required. + +The following is an example of using the Filter with a pre-indexed +shape: + +[source,js] +-------------------------------------------------- +PUT /shapes +{ + "mappings": { + "properties": { + "geometry": { + "type": "shape" + } + } + } +} + +PUT /shapes/_doc/footprint +{ + "geometry": { + "type": "envelope", + "coordinates" : [[1355.0, 5355.0], [1400.0, 5200.0]] + } +} + +GET /example/_search +{ + "query": { + "shape": { + "geometry": { + "indexed_shape": { + "index": "shapes", + "id": "footprint", + "path": "geometry" + } + } + } + } +} +-------------------------------------------------- +// CONSOLE + +==== Spatial Relations + +The following is a complete list of spatial relation operators available: + +* `INTERSECTS` - (default) Return all documents whose `geo_shape` field +intersects the query geometry. +* `DISJOINT` - Return all documents whose `geo_shape` field +has nothing in common with the query geometry. +* `WITHIN` - Return all documents whose `geo_shape` field +is within the query geometry. + +[float] +==== Ignore Unmapped + +When set to `true` the `ignore_unmapped` option will ignore an unmapped field +and will not match any documents for this query. This can be useful when +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. 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 2edbae206506e..b61291d68b8c9 100644 --- a/server/src/main/java/org/elasticsearch/index/query/AbstractGeometryQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/AbstractGeometryQueryBuilder.java @@ -544,7 +544,7 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws } /** local class that encapsulates xcontent parsed shape parameters */ - protected abstract static class ParsedShapeQueryParams { + protected abstract static class ParsedGeometryQueryParams { public String fieldName; public ShapeRelation relation; public ShapeBuilder shape; @@ -562,7 +562,7 @@ protected abstract static class ParsedShapeQueryParams { protected abstract boolean parseXContentField(XContentParser parser) throws IOException; } - public static ParsedShapeQueryParams parsedParamsFromXContent(XContentParser parser, ParsedShapeQueryParams params) + public static ParsedGeometryQueryParams parsedParamsFromXContent(XContentParser parser, ParsedGeometryQueryParams params) throws IOException { String fieldName = null; XContentParser.Token token; 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 7f54f8d261a0b..5e41565f8fb50 100644 --- a/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java @@ -237,7 +237,7 @@ protected GeoShapeQueryBuilder doRewrite(QueryRewriteContext queryRewriteContext return builder; } - private static class ParsedGeoShapeQueryParams extends ParsedShapeQueryParams { + private static class ParsedGeoShapeQueryParams extends ParsedGeometryQueryParams { SpatialStrategy strategy; @Override diff --git a/test/framework/src/main/java/org/elasticsearch/geo/GeometryTestUtils.java b/test/framework/src/main/java/org/elasticsearch/geo/GeometryTestUtils.java index 468d3bc0412f7..863a5938d1d27 100644 --- a/test/framework/src/main/java/org/elasticsearch/geo/GeometryTestUtils.java +++ b/test/framework/src/main/java/org/elasticsearch/geo/GeometryTestUtils.java @@ -61,13 +61,15 @@ public static Circle randomCircle(boolean hasAlt) { } public static Line randomLine(boolean hasAlts) { - int size = ESTestCase.randomIntBetween(2, 10); + // we use nextPolygon because it guarantees no duplicate points + org.apache.lucene.geo.Polygon lucenePolygon = GeoTestUtil.nextPolygon(); + int size = lucenePolygon.numPoints() - 1; double[] lats = new double[size]; double[] lons = new double[size]; double[] alts = hasAlts ? new double[size] : null; for (int i = 0; i < size; i++) { - lats[i] = randomLat(); - lons[i] = randomLon(); + lats[i] = lucenePolygon.getPolyLat(i); + lons[i] = lucenePolygon.getPolyLon(i); if (hasAlts) { alts[i] = randomAlt(); } @@ -96,11 +98,12 @@ public static Polygon randomPolygon(boolean hasAlt) { org.apache.lucene.geo.Polygon[] luceneHoles = lucenePolygon.getHoles(); List holes = new ArrayList<>(); for (int i = 0; i < lucenePolygon.numHoles(); i++) { - holes.add(linearRing(luceneHoles[i], hasAlt)); + org.apache.lucene.geo.Polygon poly = luceneHoles[i]; + holes.add(linearRing(poly.getPolyLats(), poly.getPolyLons(), hasAlt)); } - return new Polygon(linearRing(lucenePolygon, hasAlt), holes); + return new Polygon(linearRing(lucenePolygon.getPolyLats(), lucenePolygon.getPolyLons(), hasAlt), holes); } - return new Polygon(linearRing(lucenePolygon, hasAlt)); + return new Polygon(linearRing(lucenePolygon.getPolyLats(), lucenePolygon.getPolyLons(), hasAlt)); } @@ -113,12 +116,11 @@ private static double[] randomAltRing(int size) { return alts; } - private static LinearRing linearRing(org.apache.lucene.geo.Polygon polygon, boolean generateAlts) { + public static LinearRing linearRing(double[] lats, double[] lons, boolean generateAlts) { if (generateAlts) { - return new LinearRing(polygon.getPolyLats(), polygon.getPolyLons(), randomAltRing(polygon.numPoints())); - } else { - return new LinearRing(polygon.getPolyLats(), polygon.getPolyLons()); + return new LinearRing(lats, lons, randomAltRing(lats.length)); } + return new LinearRing(lats, lons); } public static Rectangle randomRectangle() { @@ -170,7 +172,7 @@ public static Geometry randomGeometry(boolean hasAlt) { return randomGeometry(0, hasAlt); } - private static Geometry randomGeometry(int level, boolean hasAlt) { + protected static Geometry randomGeometry(int level, boolean hasAlt) { @SuppressWarnings("unchecked") Function geometry = ESTestCase.randomFrom( GeometryTestUtils::randomCircle, GeometryTestUtils::randomLine, diff --git a/x-pack/plugin/spatial/build.gradle b/x-pack/plugin/spatial/build.gradle index 068ddd2b97069..91bc2015195f7 100644 --- a/x-pack/plugin/spatial/build.gradle +++ b/x-pack/plugin/spatial/build.gradle @@ -17,6 +17,11 @@ dependencies { } } +licenseHeaders { + // This class was sourced from apache lucene's sandbox module tests + excludes << 'org/apache/lucene/geo/XShapeTestUtil.java' +} + // xpack modules are installed in real clusters as the meta plugin, so // installing them as individual plugins for integ tests doesn't make sense, // so we disable integ tests diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialPlugin.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialPlugin.java index 39babd68dcd3f..b91a6f335ed47 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialPlugin.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialPlugin.java @@ -12,9 +12,11 @@ import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.MapperPlugin; import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.SearchPlugin; import org.elasticsearch.xpack.core.action.XPackInfoFeatureAction; import org.elasticsearch.xpack.core.action.XPackUsageFeatureAction; import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper; +import org.elasticsearch.xpack.spatial.index.query.ShapeQueryBuilder; import java.util.Arrays; import java.util.Collections; @@ -22,7 +24,9 @@ import java.util.List; import java.util.Map; -public class SpatialPlugin extends Plugin implements ActionPlugin, MapperPlugin { +import static java.util.Collections.singletonList; + +public class SpatialPlugin extends Plugin implements ActionPlugin, MapperPlugin, SearchPlugin { public SpatialPlugin(Settings settings) { } @@ -40,4 +44,9 @@ public Map getMappers() { mappers.put(ShapeFieldMapper.CONTENT_TYPE, new ShapeFieldMapper.TypeParser()); return Collections.unmodifiableMap(mappers); } + + @Override + public List> getQueries() { + return singletonList(new QuerySpec<>(ShapeQueryBuilder.NAME, ShapeQueryBuilder::new, ShapeQueryBuilder::fromXContent)); + } } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialUsageTransportAction.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialUsageTransportAction.java index 7c00f2c0cb2dd..a2873a2bcf938 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialUsageTransportAction.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialUsageTransportAction.java @@ -40,8 +40,7 @@ public SpatialUsageTransportAction(TransportService transportService, ClusterSer @Override protected void masterOperation(Task task, XPackUsageRequest request, ClusterState state, ActionListener listener) { - SpatialFeatureSetUsage usage = - new SpatialFeatureSetUsage(licenseState.isSpatialAllowed(), true); + SpatialFeatureSetUsage usage = new SpatialFeatureSetUsage(licenseState.isSpatialAllowed(), true); listener.onResponse(new XPackUsageFeatureResponse(usage)); } } 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 new file mode 100644 index 0000000000000..2721cc78cef53 --- /dev/null +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilder.java @@ -0,0 +1,213 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.spatial.index.query; + +import org.apache.logging.log4j.LogManager; +import org.apache.lucene.search.Query; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.elasticsearch.common.geo.parsers.ShapeParser; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.logging.DeprecationLogger; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.geo.geometry.Geometry; +import org.elasticsearch.index.mapper.AbstractGeometryFieldMapper; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.query.AbstractGeometryQueryBuilder; +import org.elasticsearch.index.query.GeoShapeQueryBuilder; +import org.elasticsearch.index.query.QueryRewriteContext; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.index.query.QueryShardException; +import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Derived {@link AbstractGeometryQueryBuilder} that builds a {@code x, y} Shape Query + * + * GeoJson and WKT shape definitions are supported + */ +public class ShapeQueryBuilder extends AbstractGeometryQueryBuilder { + public static final String NAME = "shape"; + + private static final DeprecationLogger deprecationLogger = new DeprecationLogger( + LogManager.getLogger(GeoShapeQueryBuilder.class)); + + static final String TYPES_DEPRECATION_MESSAGE = "[types removal] Types are deprecated in [geo_shape] queries. " + + "The type should no longer be specified in the [indexed_shape] section."; + + /** + * Creates a new GeoShapeQueryBuilder whose Query will be against the given + * field name using the given Shape + * + * @param fieldName + * Name of the field that will be queried + * @param shape + * Shape used in the Query + * @deprecated use {@link #ShapeQueryBuilder(String, Geometry)} instead + */ + @Deprecated + @SuppressWarnings({ "rawtypes" }) + protected ShapeQueryBuilder(String fieldName, ShapeBuilder shape) { + super(fieldName, shape); + } + + /** + * Creates a new GeoShapeQueryBuilder whose Query will be against the given + * field name using the given Shape + * + * @param fieldName + * Name of the field that will be queried + * @param shape + * Shape used in the Query + */ + public ShapeQueryBuilder(String fieldName, Geometry shape) { + super(fieldName, shape); + } + + protected ShapeQueryBuilder(String fieldName, Supplier shapeSupplier, String indexedShapeId, + @Nullable String indexedShapeType) { + super(fieldName, shapeSupplier, indexedShapeId, indexedShapeType); + } + + /** + * Creates a new GeoShapeQueryBuilder whose Query will be against the given + * field name and will use the Shape found with the given ID + * + * @param fieldName + * Name of the field that will be filtered + * @param indexedShapeId + * ID of the indexed Shape that will be used in the Query + */ + public ShapeQueryBuilder(String fieldName, String indexedShapeId) { + super(fieldName, indexedShapeId); + } + + @Deprecated + protected ShapeQueryBuilder(String fieldName, String indexedShapeId, String indexedShapeType) { + super(fieldName, (Geometry) null, indexedShapeId, indexedShapeType); + } + + public ShapeQueryBuilder(StreamInput in) throws IOException { + super(in); + } + + @Override + protected void doWriteTo(StreamOutput out) throws IOException { + super.doWriteTo(out); + } + + @Override + protected ShapeQueryBuilder newShapeQueryBuilder(String fieldName, Geometry shape) { + return new ShapeQueryBuilder(fieldName, shape); + } + + @Override + protected ShapeQueryBuilder newShapeQueryBuilder(String fieldName, Supplier shapeSupplier, String indexedShapeId, + String indexedShapeType) { + return new ShapeQueryBuilder(fieldName, shapeSupplier, indexedShapeId, indexedShapeType); + } + + @Override + public String queryFieldType() { + return ShapeFieldMapper.CONTENT_TYPE; + } + + @Override + @SuppressWarnings({ "rawtypes" }) + protected List validContentTypes() { + return Arrays.asList(ShapeFieldMapper.CONTENT_TYPE); + } + + @Override + @SuppressWarnings({ "rawtypes" }) + public Query buildShapeQuery(QueryShardContext context, MappedFieldType fieldType) { + if (fieldType.typeName().equals(ShapeFieldMapper.CONTENT_TYPE) == false) { + throw new QueryShardException(context, + "Field [" + fieldName + "] is not of type [" + queryFieldType() + "] but of type [" + fieldType.typeName() + "]"); + } + + final AbstractGeometryFieldMapper.AbstractGeometryFieldType ft = (AbstractGeometryFieldMapper.AbstractGeometryFieldType) fieldType; + return ft.geometryQueryBuilder().process(shape, ft.name(), relation, context); + } + + @Override + public void doShapeQueryXContent(XContentBuilder builder, Params params) throws IOException { + // noop + } + + @Override + protected ShapeQueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException { + return (ShapeQueryBuilder)super.doRewrite(queryRewriteContext); + } + + @Override + protected boolean doEquals(ShapeQueryBuilder other) { + return super.doEquals((AbstractGeometryQueryBuilder)other); + } + + @Override + protected int doHashCode() { + return Objects.hash(super.doHashCode()); + } + + @Override + public String getWriteableName() { + return NAME; + } + + private static class ParsedShapeQueryParams extends ParsedGeometryQueryParams { + @Override + protected boolean parseXContentField(XContentParser parser) throws IOException { + if (SHAPE_FIELD.match(parser.currentName(), parser.getDeprecationHandler())) { + this.shape = ShapeParser.parse(parser); + return true; + } + return false; + } + } + + public static ShapeQueryBuilder fromXContent(XContentParser parser) throws IOException { + ParsedShapeQueryParams pgsqb = (ParsedShapeQueryParams)AbstractGeometryQueryBuilder.parsedParamsFromXContent(parser, + new ParsedShapeQueryParams()); + + ShapeQueryBuilder builder; + if (pgsqb.type != null) { + deprecationLogger.deprecatedAndMaybeLog( + "geo_share_query_with_types", TYPES_DEPRECATION_MESSAGE); + } + + if (pgsqb.shape != null) { + builder = new ShapeQueryBuilder(pgsqb.fieldName, pgsqb.shape); + } else { + builder = new ShapeQueryBuilder(pgsqb.fieldName, pgsqb.id, pgsqb.type); + } + if (pgsqb.index != null) { + builder.indexedShapeIndex(pgsqb.index); + } + if (pgsqb.shapePath != null) { + builder.indexedShapePath(pgsqb.shapePath); + } + if (pgsqb.shapeRouting != null) { + builder.indexedShapeRouting(pgsqb.shapeRouting); + } + if (pgsqb.relation != null) { + builder.relation(pgsqb.relation); + } + if (pgsqb.queryName != null) { + builder.queryName(pgsqb.queryName); + } + builder.boost(pgsqb.boost); + builder.ignoreUnmapped(pgsqb.ignoreUnmapped); + return builder; + } +} diff --git a/x-pack/plugin/spatial/src/test/java/org/apache/lucene/geo/XShapeTestUtil.java b/x-pack/plugin/spatial/src/test/java/org/apache/lucene/geo/XShapeTestUtil.java new file mode 100644 index 0000000000000..e42e7bf9e03b3 --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/apache/lucene/geo/XShapeTestUtil.java @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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.apache.lucene.geo; + +import java.util.ArrayList; +import java.util.Random; + +import com.carrotsearch.randomizedtesting.RandomizedContext; +import com.carrotsearch.randomizedtesting.generators.BiasedNumbers; +import org.apache.lucene.util.SloppyMath; +import org.apache.lucene.util.TestUtil; + +/** generates random cartesian geometry; heavy reuse of {@link GeoTestUtil} */ +public class XShapeTestUtil { + + /** returns next pseudorandom polygon */ + public static XYPolygon nextPolygon() { + if (random().nextBoolean()) { + return surpriseMePolygon(); + } else if (random().nextInt(10) == 1) { + // this poly is slow to create ... only do it 10% of the time: + while (true) { + int gons = TestUtil.nextInt(random(), 4, 500); + // So the poly can cover at most 50% of the earth's surface: + double radius = random().nextDouble() * 0.5 * Float.MAX_VALUE + 1.0; + try { + return createRegularPolygon(nextDouble(), nextDouble(), radius, gons); + } catch (IllegalArgumentException iae) { + // we tried to cross dateline or pole ... try again + } + } + } + + XYRectangle box = nextBoxInternal(); + if (random().nextBoolean()) { + // box + return boxPolygon(box); + } else { + // triangle + return trianglePolygon(box); + } + } + + private static XYPolygon trianglePolygon(XYRectangle box) { + final float[] polyX = new float[4]; + final float[] polyY = new float[4]; + polyX[0] = (float)box.minX; + polyY[0] = (float)box.minY; + polyX[1] = (float)box.minX; + polyY[1] = (float)box.minY; + polyX[2] = (float)box.minX; + polyY[2] = (float)box.minY; + polyX[3] = (float)box.minX; + polyY[3] = (float)box.minY; + return new XYPolygon(polyX, polyY); + } + + public static XYRectangle nextBox() { + return nextBoxInternal(); + } + + private static XYRectangle nextBoxInternal() { + // prevent lines instead of boxes + double x0 = nextDouble(); + double x1 = nextDouble(); + while (x0 == x1) { + x1 = nextDouble(); + } + // prevent lines instead of boxes + double y0 = nextDouble(); + double y1 = nextDouble(); + while (y0 == y1) { + y1 = nextDouble(); + } + + if (x1 < x0) { + double x = x0; + x0 = x1; + x1 = x; + } + + if (y1 < y0) { + double y = y0; + y0 = y1; + y1 = y; + } + + return new XYRectangle(x0, x1, y0, y1); + } + + private static XYPolygon boxPolygon(XYRectangle box) { + final float[] polyX = new float[5]; + final float[] polyY = new float[5]; + polyX[0] = (float)box.minX; + polyY[0] = (float)box.minY; + polyX[1] = (float)box.minX; + polyY[1] = (float)box.minY; + polyX[2] = (float)box.minX; + polyY[2] = (float)box.minY; + polyX[3] = (float)box.minX; + polyY[3] = (float)box.minY; + polyX[4] = (float)box.minX; + polyY[4] = (float)box.minY; + return new XYPolygon(polyX, polyY); + } + + private static XYPolygon surpriseMePolygon() { + // repeat until we get a poly that doesn't cross dateline: + while (true) { + //System.out.println("\nPOLY ITER"); + double centerX = nextDouble(); + double centerY = nextDouble(); + double radius = 0.1 + 20 * random().nextDouble(); + double radiusDelta = random().nextDouble(); + + ArrayList xList = new ArrayList<>(); + ArrayList yList = new ArrayList<>(); + double angle = 0.0; + while (true) { + angle += random().nextDouble()*40.0; + //System.out.println(" angle " + angle); + if (angle > 360) { + break; + } + double len = radius * (1.0 - radiusDelta + radiusDelta * random().nextDouble()); + double maxX = StrictMath.min(StrictMath.abs(Float.MAX_VALUE - centerX), StrictMath.abs(-Float.MAX_VALUE - centerX)); + double maxY = StrictMath.min(StrictMath.abs(Float.MAX_VALUE - centerY), StrictMath.abs(-Float.MAX_VALUE - centerY)); + + len = StrictMath.min(len, StrictMath.min(maxX, maxY)); + + //System.out.println(" len=" + len); + float x = (float)(centerX + len * Math.cos(SloppyMath.toRadians(angle))); + float y = (float)(centerY + len * Math.sin(SloppyMath.toRadians(angle))); + + xList.add(x); + yList.add(y); + + //System.out.println(" lat=" + lats.get(lats.size()-1) + " lon=" + lons.get(lons.size()-1)); + } + + // close it + xList.add(xList.get(0)); + yList.add(yList.get(0)); + + float[] xArray = new float[xList.size()]; + float[] yArray = new float[yList.size()]; + for(int i=0;i { + + protected static final String SHAPE_FIELD_NAME = "mapped_shape"; + + private static String docType = "_doc"; + + protected static String indexedShapeId; + protected static String indexedShapeType; + protected static String indexedShapePath; + protected static String indexedShapeIndex; + protected static String indexedShapeRouting; + protected static Geometry indexedShapeToReturn; + + @Override + protected Collection> getPlugins() { + return Collections.singleton(SpatialPlugin.class); + } + + @Override + protected void initializeAdditionalMappings(MapperService mapperService) throws IOException { + mapperService.merge(docType, new CompressedXContent(Strings.toString(PutMappingRequest.buildFromSimplifiedDef(docType, + fieldName(), "type=shape"))), MapperService.MergeReason.MAPPING_UPDATE); + } + + protected String fieldName() { + return SHAPE_FIELD_NAME; + } + + @Override + protected ShapeQueryBuilder doCreateTestQueryBuilder() { + return doCreateTestQueryBuilder(randomBoolean()); + } + + protected ShapeQueryBuilder doCreateTestQueryBuilder(boolean indexedShape) { + Geometry shape; + // multipoint queries not (yet) supported + do { + shape = ShapeTestUtils.randomGeometry(false); + } while (shape.type() == ShapeType.MULTIPOINT || shape.type() == ShapeType.GEOMETRYCOLLECTION); + + ShapeQueryBuilder builder; + clearShapeFields(); + if (indexedShape == false) { + builder = new ShapeQueryBuilder(fieldName(), shape); + } else { + indexedShapeToReturn = shape; + indexedShapeId = randomAlphaOfLengthBetween(3, 20); + indexedShapeType = randomBoolean() ? randomAlphaOfLengthBetween(3, 20) : null; + builder = new ShapeQueryBuilder(fieldName(), indexedShapeId, indexedShapeType); + 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 (shape.type() == ShapeType.LINESTRING || shape.type() == ShapeType.MULTILINESTRING) { + builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS)); + } else { + // XYShape does not support CONTAINS: + builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, ShapeRelation.WITHIN)); + } + + if (randomBoolean()) { + builder.ignoreUnmapped(randomBoolean()); + } + return builder; + } + + @After + public void clearShapeFields() { + indexedShapeToReturn = null; + indexedShapeId = null; + indexedShapeType = null; + indexedShapePath = null; + indexedShapeIndex = null; + indexedShapeRouting = null; + } + + @Override + protected void doAssertLuceneQuery(ShapeQueryBuilder queryBuilder, Query query, SearchContext context) throws IOException { + // Logic for doToQuery is complex and is hard to test here. Need to rely + // on Integration tests to determine if created query is correct + // TODO improve ShapeQueryBuilder.doToQuery() method to make it + // easier to test here + assertThat(query, anyOf(instanceOf(BooleanQuery.class), instanceOf(ConstantScoreQuery.class))); + } + + public void testNoFieldName() { + Geometry shape = ShapeTestUtils.randomGeometry(false); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new ShapeQueryBuilder(null, shape)); + assertEquals("fieldName is required", e.getMessage()); + } + + public void testNoShape() { + expectThrows(IllegalArgumentException.class, () -> new ShapeQueryBuilder(fieldName(), (Geometry) null)); + } + + public void testNoIndexedShape() { + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> new ShapeQueryBuilder(fieldName(), null, "type")); + assertEquals("either shape or indexedShapeId is required", e.getMessage()); + } + + public void testNoRelation() { + Geometry shape = ShapeTestUtils.randomGeometry(false); + ShapeQueryBuilder builder = new ShapeQueryBuilder(fieldName(), shape); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> builder.relation(null)); + assertEquals("No Shape Relation defined", e.getMessage()); + } + + public void testFromJson() throws IOException { + String json = + "{\n" + + " \"shape\" : {\n" + + " \"geometry\" : {\n" + + " \"shape\" : {\n" + + " \"type\" : \"envelope\",\n" + + " \"coordinates\" : [ [ 1300.0, 5300.0 ], [ 1400.0, 5200.0 ] ]\n" + + " },\n" + + " \"relation\" : \"intersects\"\n" + + " },\n" + + " \"ignore_unmapped\" : false,\n" + + " \"boost\" : 42.0\n" + + " }\n" + + "}"; + ShapeQueryBuilder parsed = (ShapeQueryBuilder) parseQuery(json); + checkGeneratedJson(json, parsed); + assertEquals(json, 42.0, parsed.boost(), 0.0001); + } + + @Override + public void testMustRewrite() { + ShapeQueryBuilder query = doCreateTestQueryBuilder(true); + + UnsupportedOperationException e = expectThrows(UnsupportedOperationException.class, () -> query.toQuery(createShardContext())); + assertEquals("query must be rewritten first", e.getMessage()); + QueryBuilder rewrite = rewriteAndFetch(query, createShardContext()); + ShapeQueryBuilder geoShapeQueryBuilder = new ShapeQueryBuilder(fieldName(), indexedShapeToReturn); + geoShapeQueryBuilder.relation(query.relation()); + assertEquals(geoShapeQueryBuilder, rewrite); + } + + public void testMultipleRewrite() { + ShapeQueryBuilder shape = doCreateTestQueryBuilder(true); + QueryBuilder builder = new BoolQueryBuilder() + .should(shape) + .should(shape); + + builder = rewriteAndFetch(builder, createShardContext()); + + ShapeQueryBuilder expectedShape = new ShapeQueryBuilder(fieldName(), indexedShapeToReturn); + expectedShape.relation(shape.relation()); + QueryBuilder expected = new BoolQueryBuilder() + .should(expectedShape) + .should(expectedShape); + assertEquals(expected, builder); + } + + public void testIgnoreUnmapped() throws IOException { + Geometry shape = ShapeTestUtils.randomGeometry(false); + final ShapeQueryBuilder queryBuilder = new ShapeQueryBuilder("unmapped", shape); + queryBuilder.ignoreUnmapped(true); + Query query = queryBuilder.toQuery(createShardContext()); + assertThat(query, notNullValue()); + assertThat(query, instanceOf(MatchNoDocsQuery.class)); + + final ShapeQueryBuilder failingQueryBuilder = new ShapeQueryBuilder("unmapped", shape); + failingQueryBuilder.ignoreUnmapped(false); + QueryShardException e = expectThrows(QueryShardException.class, () -> failingQueryBuilder.toQuery(createShardContext())); + assertThat(e.getMessage(), containsString("failed to find shape field [unmapped]")); + } + + public void testWrongFieldType() { + Geometry shape = ShapeTestUtils.randomGeometry(false); + final ShapeQueryBuilder queryBuilder = new ShapeQueryBuilder(STRING_FIELD_NAME, shape); + QueryShardException e = expectThrows(QueryShardException.class, () -> queryBuilder.toQuery(createShardContext())); + assertThat(e.getMessage(), containsString("Field [mapped_string] is not of type [shape] but of type [text]")); + } + + public void testSerializationFailsUnlessFetched() throws IOException { + QueryBuilder builder = doCreateTestQueryBuilder(true); + QueryBuilder queryBuilder = Rewriteable.rewrite(builder, createShardContext()); + IllegalStateException ise = expectThrows(IllegalStateException.class, () -> queryBuilder.writeTo(new BytesStreamOutput(10))); + assertEquals(ise.getMessage(), "supplier must be null, can't serialize suppliers, missing a rewriteAndFetch?"); + builder = rewriteAndFetch(builder, createShardContext()); + builder.writeTo(new BytesStreamOutput(10)); + } + + @Override + protected QueryBuilder parseQuery(XContentParser parser) throws IOException { + QueryBuilder query = super.parseQuery(parser); + assertThat(query, instanceOf(ShapeQueryBuilder.class)); + + ShapeQueryBuilder shapeQuery = (ShapeQueryBuilder) query; + if (shapeQuery.indexedShapeType() != null) { + assertWarnings(ShapeQueryBuilder.TYPES_DEPRECATION_MESSAGE); + } + return query; + } + + @Override + protected GetResponse executeGet(GetRequest getRequest) { + String indexedType = indexedShapeType != null ? indexedShapeType : MapperService.SINGLE_MAPPING_NAME; + + assertThat(indexedShapeToReturn, notNullValue()); + assertThat(indexedShapeId, notNullValue()); + assertThat(getRequest.id(), equalTo(indexedShapeId)); + assertThat(getRequest.type(), equalTo(indexedType)); + assertThat(getRequest.routing(), equalTo(indexedShapeRouting)); + String expectedShapeIndex = indexedShapeIndex == null ? ShapeQueryBuilder.DEFAULT_SHAPE_INDEX_NAME : indexedShapeIndex; + assertThat(getRequest.index(), equalTo(expectedShapeIndex)); + String expectedShapePath = indexedShapePath == null ? ShapeQueryBuilder.DEFAULT_SHAPE_FIELD_NAME : indexedShapePath; + + String json; + try { + XContentBuilder builder = XContentFactory.jsonBuilder().prettyPrint(); + builder.startObject(); + builder.field(expectedShapePath, new ToXContentObject() { + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return GeoJson.toXContent(indexedShapeToReturn, builder, null); + } + }); + builder.field(randomAlphaOfLengthBetween(10, 20), "something"); + builder.endObject(); + json = Strings.toString(builder); + } catch (IOException ex) { + throw new ElasticsearchException("boom", ex); + } + return new GetResponse(new GetResult(indexedShapeIndex, indexedType, indexedShapeId, 0, 1, 0, true, new BytesArray(json), + null, null)); + } +} diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryTests.java new file mode 100644 index 0000000000000..c8181da8fc12d --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryTests.java @@ -0,0 +1,236 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.spatial.search; + +import org.elasticsearch.action.get.GetResponse; +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.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.geo.geometry.Geometry; +import org.elasticsearch.geo.geometry.ShapeType; +import org.elasticsearch.index.query.ExistsQueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.elasticsearch.xpack.core.XPackPlugin; +import org.elasticsearch.xpack.spatial.SpatialPlugin; +import org.elasticsearch.xpack.spatial.index.query.ShapeQueryBuilder; +import org.elasticsearch.xpack.spatial.util.ShapeTestUtils; +import org.locationtech.jts.geom.Coordinate; + +import java.util.Collection; +import java.util.Locale; + +import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount; +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.nullValue; + +public class ShapeQueryTests extends ESSingleNodeTestCase { + + private static String INDEX = "test"; + private static String IGNORE_MALFORMED_INDEX = INDEX + "_ignore_malformed"; + private static String FIELD_TYPE = "geometry"; + private static String FIELD = "shape"; + private static Geometry queryGeometry = null; + + private int numDocs; + + @Override + public void setUp() throws Exception { + super.setUp(); + + // create test index + assertAcked(client().admin().indices().prepareCreate(INDEX) + .addMapping(FIELD_TYPE, FIELD, "type=shape", "alias", "type=alias,path=" + FIELD).get()); + // create index that ignores malformed geometry + assertAcked(client().admin().indices().prepareCreate(IGNORE_MALFORMED_INDEX) + .addMapping(FIELD_TYPE, FIELD, "type=shape,ignore_malformed=true", "_source", "enabled=false").get()); + ensureGreen(); + + // index random shapes + numDocs = randomIntBetween(25, 50); + Geometry geometry; + for (int i = 0; i < numDocs; ++i) { + geometry = ShapeTestUtils.randomGeometry(false); + if (geometry.type() == ShapeType.CIRCLE) continue; + if (queryGeometry == null && geometry.type() != ShapeType.MULTIPOINT) { + queryGeometry = geometry; + } + XContentBuilder geoJson = GeoJson.toXContent(geometry, XContentFactory.jsonBuilder() + .startObject().field(FIELD), null).endObject(); + + try { + client().prepareIndex(INDEX, FIELD_TYPE).setSource(geoJson).setRefreshPolicy(IMMEDIATE).get(); + client().prepareIndex(IGNORE_MALFORMED_INDEX, FIELD_TYPE).setRefreshPolicy(IMMEDIATE).setSource(geoJson).get(); + } catch (Exception e) { + // sometimes GeoTestUtil will create invalid geometry; catch and continue: + --i; + continue; + } + } + } + + public void testIndexedShapeReferenceSourceDisabled() throws Exception { + EnvelopeBuilder shape = new EnvelopeBuilder(new Coordinate(-45, 45), new Coordinate(45, -45)); + + client().prepareIndex(IGNORE_MALFORMED_INDEX, FIELD_TYPE, "Big_Rectangle").setSource(jsonBuilder().startObject() + .field(FIELD, shape).endObject()).setRefreshPolicy(IMMEDIATE).get(); + + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> client().prepareSearch(IGNORE_MALFORMED_INDEX) + .setQuery(new ShapeQueryBuilder(FIELD, "Big_Rectangle").indexedShapeIndex(IGNORE_MALFORMED_INDEX)).get()); + assertThat(e.getMessage(), containsString("source disabled")); + } + + public void testShapeFetchingPath() throws Exception { + String indexName = "shapes_index"; + String searchIndex = "search_index"; + createIndex(indexName); + client().admin().indices().prepareCreate(searchIndex).addMapping("type", "location", "type=shape").get(); + + String location = "\"location\" : {\"type\":\"polygon\", \"coordinates\":[[[-10,-10],[10,-10],[10,10],[-10,10],[-10,-10]]]}"; + + client().prepareIndex(indexName, "type", "1") + .setSource( + String.format( + Locale.ROOT, "{ %s, \"1\" : { %s, \"2\" : { %s, \"3\" : { %s } }} }", location, location, location, location + ), XContentType.JSON) + .setRefreshPolicy(IMMEDIATE).get(); + client().prepareIndex(searchIndex, "type", "1") + .setSource(jsonBuilder().startObject().startObject("location") + .field("type", "polygon") + .startArray("coordinates").startArray() + .startArray().value(-20).value(-20).endArray() + .startArray().value(20).value(-20).endArray() + .startArray().value(20).value(20).endArray() + .startArray().value(-20).value(20).endArray() + .startArray().value(-20).value(-20).endArray() + .endArray().endArray() + .endObject().endObject()).setRefreshPolicy(IMMEDIATE).get(); + + ShapeQueryBuilder filter = new ShapeQueryBuilder("location", "1").relation(ShapeRelation.INTERSECTS) + .indexedShapeIndex(indexName) + .indexedShapePath("location"); + SearchResponse result = client().prepareSearch(searchIndex).setQuery(QueryBuilders.matchAllQuery()) + .setPostFilter(filter).get(); + assertSearchResponse(result); + assertHitCount(result, 1); + filter = new ShapeQueryBuilder("location", "1").relation(ShapeRelation.INTERSECTS) + .indexedShapeIndex(indexName) + .indexedShapePath("1.location"); + result = client().prepareSearch(searchIndex).setQuery(QueryBuilders.matchAllQuery()) + .setPostFilter(filter).get(); + assertSearchResponse(result); + assertHitCount(result, 1); + filter = new ShapeQueryBuilder("location", "1").relation(ShapeRelation.INTERSECTS) + .indexedShapeIndex(indexName) + .indexedShapePath("1.2.location"); + result = client().prepareSearch(searchIndex).setQuery(QueryBuilders.matchAllQuery()) + .setPostFilter(filter).get(); + assertSearchResponse(result); + assertHitCount(result, 1); + filter = new ShapeQueryBuilder("location", "1").relation(ShapeRelation.INTERSECTS) + .indexedShapeIndex(indexName) + .indexedShapePath("1.2.3.location"); + result = client().prepareSearch(searchIndex).setQuery(QueryBuilders.matchAllQuery()) + .setPostFilter(filter).get(); + assertSearchResponse(result); + assertHitCount(result, 1); + + // now test the query variant + ShapeQueryBuilder query = new ShapeQueryBuilder("location", "1") + .indexedShapeIndex(indexName) + .indexedShapePath("location"); + result = client().prepareSearch(searchIndex).setQuery(query).get(); + assertSearchResponse(result); + assertHitCount(result, 1); + query = new ShapeQueryBuilder("location", "1") + .indexedShapeIndex(indexName) + .indexedShapePath("1.location"); + result = client().prepareSearch(searchIndex).setQuery(query).get(); + assertSearchResponse(result); + assertHitCount(result, 1); + query = new ShapeQueryBuilder("location", "1") + .indexedShapeIndex(indexName) + .indexedShapePath("1.2.location"); + result = client().prepareSearch(searchIndex).setQuery(query).get(); + assertSearchResponse(result); + assertHitCount(result, 1); + query = new ShapeQueryBuilder("location", "1") + .indexedShapeIndex(indexName) + .indexedShapePath("1.2.3.location"); + result = client().prepareSearch(searchIndex).setQuery(query).get(); + assertSearchResponse(result); + assertHitCount(result, 1); + } + + @Override + protected Collection> getPlugins() { + return pluginList(SpatialPlugin.class, XPackPlugin.class); + } + + /** + * Test that ignore_malformed on GeoShapeFieldMapper does not fail the entire document + */ + public void testIgnoreMalformed() { + assertHitCount(client().prepareSearch(IGNORE_MALFORMED_INDEX).setQuery(matchAllQuery()).get(), numDocs); + } + + /** + * Test that the indexed shape routing can be provided if it is required + */ + public void testIndexShapeRouting() { + String source = "{\n" + + " \"shape\" : {\n" + + " \"type\" : \"bbox\",\n" + + " \"coordinates\" : [[" + -Float.MAX_VALUE + "," + Float.MAX_VALUE + "], [" + Float.MAX_VALUE + ", " + -Float.MAX_VALUE + + "]]\n" + + " }\n" + + "}"; + + client().prepareIndex(INDEX, FIELD_TYPE, "0").setSource(source, XContentType.JSON).setRouting("ABC").get(); + client().admin().indices().prepareRefresh(INDEX).get(); + + SearchResponse searchResponse = client().prepareSearch(INDEX).setQuery( + new ShapeQueryBuilder(FIELD, "0").indexedShapeIndex(INDEX).indexedShapeRouting("ABC") + ).get(); + + assertThat(searchResponse.getHits().getTotalHits().value, equalTo((long)numDocs+1)); + } + + public void testNullShape() { + // index a null shape + client().prepareIndex(INDEX, FIELD_TYPE, "aNullshape").setSource("{\"" + FIELD + "\": null}", XContentType.JSON) + .setRefreshPolicy(IMMEDIATE).get(); + client().prepareIndex(IGNORE_MALFORMED_INDEX, FIELD_TYPE, "aNullshape").setSource("{\"" + FIELD + "\": null}", + XContentType.JSON).setRefreshPolicy(IMMEDIATE).get(); + GetResponse result = client().prepareGet(INDEX, FIELD_TYPE, "aNullshape").get(); + assertThat(result.getField(FIELD), nullValue()); + } + + public void testExistsQuery() { + ExistsQueryBuilder eqb = QueryBuilders.existsQuery(FIELD); + SearchResponse result = client().prepareSearch(INDEX).setQuery(eqb).get(); + assertSearchResponse(result); + assertHitCount(result, numDocs); + } + + public void testFieldAlias() { + SearchResponse response = client().prepareSearch(INDEX) + .setQuery(new ShapeQueryBuilder("alias", queryGeometry).relation(ShapeRelation.INTERSECTS)) + .get(); + assertTrue(response.getHits().getTotalHits().value > 0); + } +} diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/util/ShapeTestUtils.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/util/ShapeTestUtils.java new file mode 100644 index 0000000000000..63f28af31d65e --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/util/ShapeTestUtils.java @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.spatial.util; + +import org.apache.lucene.geo.XShapeTestUtil; +import org.apache.lucene.geo.XYPolygon; +import org.elasticsearch.geo.GeometryTestUtils; +import org.elasticsearch.geo.geometry.Geometry; +import org.elasticsearch.geo.geometry.GeometryCollection; +import org.elasticsearch.geo.geometry.Line; +import org.elasticsearch.geo.geometry.LinearRing; +import org.elasticsearch.geo.geometry.MultiLine; +import org.elasticsearch.geo.geometry.MultiPoint; +import org.elasticsearch.geo.geometry.MultiPolygon; +import org.elasticsearch.geo.geometry.Point; +import org.elasticsearch.geo.geometry.Polygon; +import org.elasticsearch.geo.geometry.Rectangle; +import org.elasticsearch.test.ESTestCase; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import static org.elasticsearch.geo.GeometryTestUtils.linearRing; +import static org.elasticsearch.geo.GeometryTestUtils.randomAlt; + +/** generates random cartesian shapes */ +public class ShapeTestUtils { + + public static double randomValue() { + return XShapeTestUtil.nextDouble(); + } + + public static Point randomPoint() { + return randomPoint(ESTestCase.randomBoolean()); + } + + public static Point randomPoint(boolean hasAlt) { + if (hasAlt) { + return new Point(randomValue(), randomValue(), randomAlt()); + } + return new Point(randomValue(), randomValue()); + } + + public static Line randomLine(boolean hasAlts) { + // we use nextPolygon because it guarantees no duplicate points + XYPolygon lucenePolygon = XShapeTestUtil.nextPolygon(); + int size = lucenePolygon.numPoints() - 1; + double[] x = new double[size]; + double[] y = new double[size]; + double[] alts = hasAlts ? new double[size] : null; + for (int i = 0; i < size; i++) { + x[i] = lucenePolygon.getPolyX(i); + y[i] = lucenePolygon.getPolyY(i); + if (hasAlts) { + alts[i] = randomAlt(); + } + } + if (hasAlts) { + return new Line(x, y, alts); + } + return new Line(x, y); + } + + public static Polygon randomPolygon(boolean hasAlt) { + XYPolygon lucenePolygon = XShapeTestUtil.nextPolygon(); + if (lucenePolygon.numHoles() > 0) { + XYPolygon[] luceneHoles = lucenePolygon.getHoles(); + List holes = new ArrayList<>(); + for (int i = 0; i < lucenePolygon.numHoles(); i++) { + XYPolygon poly = luceneHoles[i]; + holes.add(linearRing(poly.getPolyY(), poly.getPolyX(), hasAlt)); + } + return new Polygon(linearRing(lucenePolygon.getPolyY(), lucenePolygon.getPolyX(), hasAlt), holes); + } + return new Polygon(linearRing(lucenePolygon.getPolyY(), lucenePolygon.getPolyX(), hasAlt)); + } + + public static Rectangle randomRectangle() { + org.apache.lucene.geo.XYRectangle rectangle = XShapeTestUtil.nextBox(); + return new Rectangle(rectangle.minY, rectangle.maxY, rectangle.minX, rectangle.maxX); + } + + public static MultiPoint randomMultiPoint(boolean hasAlt) { + int size = ESTestCase.randomIntBetween(3, 10); + List points = new ArrayList<>(); + for (int i = 0; i < size; i++) { + points.add(randomPoint(hasAlt)); + } + return new MultiPoint(points); + } + + public static MultiLine randomMultiLine(boolean hasAlt) { + int size = ESTestCase.randomIntBetween(3, 10); + List lines = new ArrayList<>(); + for (int i = 0; i < size; i++) { + lines.add(randomLine(hasAlt)); + } + return new MultiLine(lines); + } + + public static MultiPolygon randomMultiPolygon(boolean hasAlt) { + int size = ESTestCase.randomIntBetween(3, 10); + List polygons = new ArrayList<>(); + for (int i = 0; i < size; i++) { + polygons.add(randomPolygon(hasAlt)); + } + return new MultiPolygon(polygons); + } + + public static GeometryCollection randomGeometryCollection(boolean hasAlt) { + return randomGeometryCollection(0, hasAlt); + } + + private static GeometryCollection randomGeometryCollection(int level, boolean hasAlt) { + int size = ESTestCase.randomIntBetween(1, 10); + List shapes = new ArrayList<>(); + for (int i = 0; i < size; i++) { + shapes.add(randomGeometry(level, hasAlt)); + } + return new GeometryCollection<>(shapes); + } + + public static Geometry randomGeometry(boolean hasAlt) { + return randomGeometry(0, hasAlt); + } + + protected static Geometry randomGeometry(int level, boolean hasAlt) { + @SuppressWarnings("unchecked") Function geometry = ESTestCase.randomFrom( + ShapeTestUtils::randomLine, + ShapeTestUtils::randomPoint, + ShapeTestUtils::randomPolygon, + ShapeTestUtils::randomMultiLine, + ShapeTestUtils::randomMultiPoint, + ShapeTestUtils::randomMultiPolygon, + hasAlt ? ShapeTestUtils::randomPoint : (b) -> randomRectangle(), + level < 3 ? (b) -> randomGeometryCollection(level + 1, b) : GeometryTestUtils::randomPoint // don't build too deep + ); + return geometry.apply(hasAlt); + } +}