diff --git a/docs/reference/mapping/types/point.asciidoc b/docs/reference/mapping/types/point.asciidoc new file mode 100644 index 00000000000..1a78a121b41 --- /dev/null +++ b/docs/reference/mapping/types/point.asciidoc @@ -0,0 +1,99 @@ +[[point]] +[role="xpack"] +[testenv="basic"] +=== Point datatype +++++ +Point +++++ + +The `point` datatype facilitates the indexing of and searching +arbitrary `x, y` pairs that fall in a 2-dimensional planar +coordinate system. + +You can query documents using this type using +<>. + +There are four ways that a point may be specified, as demonstrated below: + +[source,console] +-------------------------------------------------- +PUT my_index +{ + "mappings": { + "properties": { + "location": { + "type": "point" + } + } + } +} + +PUT my_index/_doc/1 +{ + "text": "Point as an object", + "location": { <1> + "x": 41.12, + "y": -71.34 + } +} + +PUT my_index/_doc/2 +{ + "text": "Point as a string", + "location": "41.12,-71.34" <2> +} + + +PUT my_index/_doc/4 +{ + "text": "Point as an array", + "location": [41.12, -71.34] <3> +} + +PUT my_index/_doc/5 +{ + "text": "Point as a WKT POINT primitive", + "location" : "POINT (41.12 -71.34)" <4> +} + +-------------------------------------------------- + +<1> Point expressed as an object, with `x` and `y` keys. +<2> Point expressed as a string with the format: `"x,y"`. +<4> Point expressed as an array with the format: [ `x`, `y`] +<5> Point expressed as a http://docs.opengeospatial.org/is/12-063r5/12-063r5.html[Well-Known Text] +POINT with the format: `"POINT(x y)"` + +The coordinates provided to the indexer are single precision floating point values so +the field guarantees the same accuracy provided by the java virtual machine (typically +`1E-38`). + +[[geo-point-params]] +==== Parameters for `geo_point` fields + +The following parameters are accepted by `point` fields: + +[horizontal] + +<>:: + + If `true`, malformed points are ignored. If `false` (default), + malformed points throw an exception and reject the whole document. + +`ignore_z_value`:: + + If `true` (default) three dimension points will be accepted (stored in source) + but only x and y values will be indexed; the third dimension is + ignored. If `false`, points containing any more than x and y + (two dimensions) values throw an exception and reject the whole document. + +<>:: + + Accepts an point value which is substituted for any explicit `null` values. + Defaults to `null`, which means the field is treated as missing. + +==== Sorting and Retrieving index Shapes + +It is currently not possible to sort shapes or retrieve their fields +directly. The `point` value is only retrievable through the `_source` +field. diff --git a/docs/reference/query-dsl/shape-queries.asciidoc b/docs/reference/query-dsl/shape-queries.asciidoc index 204ebab9cec..2e44069c066 100644 --- a/docs/reference/query-dsl/shape-queries.asciidoc +++ b/docs/reference/query-dsl/shape-queries.asciidoc @@ -3,16 +3,21 @@ [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. +map out virtual worlds, sporting venues, theme parks, and CAD diagrams. + +Elasticsearch supports two types of cartesian data: +<> fields which support x/y pairs, and +<> fields, which support points, lines, circles, polygons, multi-polygons, etc. The queries in this group are: <> query:: -Finds documents with shapes that either intersect, are within, or do not -intersect a specified shape. +Finds documents with: +* `shapes` which either intersect, are contained by, are within or do not intersect +with the specified shape +* `points` which intersect the specified shape include::shape-query.asciidoc[] 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 a93fb75d709..417d8d54f04 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 @@ -14,6 +14,7 @@ import org.elasticsearch.plugins.MapperPlugin; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.plugins.SearchPlugin; +import org.elasticsearch.xpack.spatial.index.mapper.PointFieldMapper; import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper; import org.elasticsearch.xpack.spatial.index.query.ShapeQueryBuilder; import org.elasticsearch.xpack.spatial.ingest.CircleProcessor; @@ -41,6 +42,7 @@ public class SpatialPlugin extends Plugin implements MapperPlugin, SearchPlugin, public Map getMappers() { Map mappers = new LinkedHashMap<>(); mappers.put(ShapeFieldMapper.CONTENT_TYPE, new ShapeFieldMapper.TypeParser()); + mappers.put(PointFieldMapper.CONTENT_TYPE, new PointFieldMapper.TypeParser()); return Collections.unmodifiableMap(mappers); } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianPoint.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianPoint.java new file mode 100644 index 00000000000..e49803bfc1f --- /dev/null +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianPoint.java @@ -0,0 +1,293 @@ +/* + * 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.mapper; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ToXContentFragment; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentSubParser; +import org.elasticsearch.common.xcontent.support.MapXContentParser; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.ShapeType; +import org.elasticsearch.geometry.utils.StandardValidator; +import org.elasticsearch.geometry.utils.WellKnownText; + +import java.io.IOException; +import java.util.Collections; +import java.util.Locale; + +import static org.elasticsearch.index.mapper.GeoPointFieldMapper.Names.IGNORE_Z_VALUE; + +/** + * Represents a point in the cartesian space. + */ +public final class CartesianPoint implements ToXContentFragment { + + private static final ParseField X_FIELD = new ParseField("x"); + private static final ParseField Y_FIELD = new ParseField("y"); + private static final ParseField Z_FIELD = new ParseField("z"); + + private float x; + private float y; + + public CartesianPoint() { + } + + public CartesianPoint(float x, float y) { + this.x = x; + this.y = y; + } + + public CartesianPoint reset(float x, float y) { + this.x = x; + this.y = y; + return this; + } + + public CartesianPoint resetFromString(String value, final boolean ignoreZValue) { + if (value.toLowerCase(Locale.ROOT).contains("point")) { + return resetFromWKT(value, ignoreZValue); + } else { + return resetFromCoordinates(value, ignoreZValue); + } + } + + + public CartesianPoint resetFromCoordinates(String value, final boolean ignoreZValue) { + String[] vals = value.split(","); + if (vals.length > 3 || vals.length < 2) { + throw new ElasticsearchParseException("failed to parse [{}], expected 2 or 3 coordinates " + + "but found: [{}]", vals, vals.length); + } + final float x; + final float y; + try { + x = Float.parseFloat(vals[0].trim()); + if (Float.isFinite(x) == false) { + throw new ElasticsearchParseException("invalid [{}] value [{}]; " + + "must be between -3.4028234663852886E38 and 3.4028234663852886E38", + X_FIELD.getPreferredName(), + x); + } + } catch (NumberFormatException ex) { + throw new ElasticsearchParseException("[{}]] must be a number", X_FIELD.getPreferredName()); + } + try { + y = Float.parseFloat(vals[1].trim()); + if (Float.isFinite(y) == false) { + throw new ElasticsearchParseException("invalid [{}] value [{}]; " + + "must be between -3.4028234663852886E38 and 3.4028234663852886E38", + Y_FIELD.getPreferredName(), + y); + } + } catch (NumberFormatException ex) { + throw new ElasticsearchParseException("[{}]] must be a number", Y_FIELD.getPreferredName()); + } + if (vals.length > 2) { + try { + CartesianPoint.assertZValue(ignoreZValue, Float.parseFloat(vals[2].trim())); + } catch (NumberFormatException ex) { + throw new ElasticsearchParseException("[{}]] must be a number", Y_FIELD.getPreferredName()); + } + } + return reset(x, y); + } + + private CartesianPoint resetFromWKT(String value, boolean ignoreZValue) { + Geometry geometry; + try { + geometry = new WellKnownText(false, new StandardValidator(ignoreZValue)) + .fromWKT(value); + } catch (Exception e) { + throw new ElasticsearchParseException("Invalid WKT format", e); + } + if (geometry.type() != ShapeType.POINT) { + throw new ElasticsearchParseException("[{}] supports only POINT among WKT primitives, " + + "but found {}", PointFieldMapper.CONTENT_TYPE, geometry.type()); + } + org.elasticsearch.geometry.Point point = (org.elasticsearch.geometry.Point) geometry; + return reset((float) point.getX(), (float) point.getY()); + } + + public float getX() { + return this.x; + } + + public float getY() { + return this.y; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CartesianPoint point = (CartesianPoint) o; + + if (Float.compare(point.x, x) != 0) return false; + if (Float.compare(point.y, y) != 0) return false; + + return true; + } + + @Override + public int hashCode() { + int result; + int temp; + temp = x != +0.0f ? Float.floatToIntBits(x) : 0; + result = Integer.hashCode(temp); + temp = y != +0.0f ? Float.floatToIntBits(y) : 0; + result = 31 * result + Integer.hashCode(temp); + return result; + } + + @Override + public String toString() { + return x + ", " + y; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject().field(X_FIELD.getPreferredName(), x).field(Y_FIELD.getPreferredName(), y).endObject(); + } + + public static CartesianPoint parsePoint(XContentParser parser, CartesianPoint point, boolean ignoreZvalue) + throws IOException, ElasticsearchParseException { + float x = Float.NaN; + float y = Float.NaN; + NumberFormatException numberFormatException = null; + + if(parser.currentToken() == XContentParser.Token.START_OBJECT) { + try (XContentSubParser subParser = new XContentSubParser(parser)) { + while (subParser.nextToken() != XContentParser.Token.END_OBJECT) { + if (subParser.currentToken() == XContentParser.Token.FIELD_NAME) { + String field = subParser.currentName(); + if (field.equals(X_FIELD.getPreferredName())) { + subParser.nextToken(); + switch (subParser.currentToken()) { + case VALUE_NUMBER: + case VALUE_STRING: + try { + x = subParser.floatValue(true); + } catch (NumberFormatException e) { + numberFormatException = e; + } + break; + default: + throw new ElasticsearchParseException("[{}] must be a number", + X_FIELD.getPreferredName()); + } + } else if (field.equals(Y_FIELD.getPreferredName())) { + subParser.nextToken(); + switch (subParser.currentToken()) { + case VALUE_NUMBER: + case VALUE_STRING: + try { + y = subParser.floatValue(true); + } catch (NumberFormatException e) { + numberFormatException = e; + } + break; + default: + throw new ElasticsearchParseException("[{}] must be a number", + Y_FIELD.getPreferredName()); + } + } else if (field.equals(Z_FIELD.getPreferredName())) { + subParser.nextToken(); + switch (subParser.currentToken()) { + case VALUE_NUMBER: + case VALUE_STRING: + try { + CartesianPoint.assertZValue(ignoreZvalue, subParser.floatValue(true)); + } catch (NumberFormatException e) { + numberFormatException = e; + } + break; + default: + throw new ElasticsearchParseException("[{}] must be a number", + Z_FIELD.getPreferredName()); + } + } else { + throw new ElasticsearchParseException("field must be either [{}] or [{}]", + X_FIELD.getPreferredName(), + Y_FIELD.getPreferredName()); + } + } else { + throw new ElasticsearchParseException("token [{}] not allowed", subParser.currentToken()); + } + } + } + if (numberFormatException != null) { + throw new ElasticsearchParseException("[{}] and [{}] must be valid float values", numberFormatException, + X_FIELD.getPreferredName(), + Y_FIELD.getPreferredName()); + } else if (Float.isNaN(x)) { + throw new ElasticsearchParseException("field [{}] missing", X_FIELD.getPreferredName()); + } else if (Float.isNaN(y)) { + throw new ElasticsearchParseException("field [{}] missing", Y_FIELD.getPreferredName()); + } else { + return point.reset(x, y); + } + + } else if(parser.currentToken() == XContentParser.Token.START_ARRAY) { + try (XContentSubParser subParser = new XContentSubParser(parser)) { + int element = 0; + while (subParser.nextToken() != XContentParser.Token.END_ARRAY) { + if (subParser.currentToken() == XContentParser.Token.VALUE_NUMBER) { + element++; + if (element == 1) { + x = subParser.floatValue(); + } else if (element == 2) { + y = subParser.floatValue(); + } else { + throw new ElasticsearchParseException("[{}}] field type does not accept > 2 dimensions", + PointFieldMapper.CONTENT_TYPE); + } + } else { + throw new ElasticsearchParseException("numeric value expected"); + } + } + } + return point.reset(x, y); + } else if(parser.currentToken() == XContentParser.Token.VALUE_STRING) { + String val = parser.text(); + return point.resetFromString(val, ignoreZvalue); + } else { + throw new ElasticsearchParseException("point expected"); + } + } + + public static CartesianPoint parsePoint(Object value, boolean ignoreZValue) throws ElasticsearchParseException { + try (XContentParser parser = new MapXContentParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, + Collections.singletonMap("null_value", value), null)) { + parser.nextToken(); // start object + parser.nextToken(); // field name + parser.nextToken(); // field value + return parsePoint(parser, new CartesianPoint(), ignoreZValue); + } catch (IOException ex) { + throw new ElasticsearchParseException("error parsing point", ex); + } + } + + public static double assertZValue(final boolean ignoreZValue, float zValue) { + if (ignoreZValue == false) { + throw new ElasticsearchParseException("Exception parsing coordinates: found Z value [{}] but [{}] " + + "parameter is [{}]", zValue, IGNORE_Z_VALUE, ignoreZValue); + } + if (Float.isFinite(zValue) == false) { + throw new ElasticsearchParseException("invalid [{}] value [{}]; " + + "must be between -3.4028234663852886E38 and 3.4028234663852886E38", + Z_FIELD.getPreferredName(), + zValue); + } + return zValue; + } +} diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapper.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapper.java new file mode 100644 index 00000000000..320e0e019cb --- /dev/null +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapper.java @@ -0,0 +1,352 @@ +/* + * 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.mapper; + +import org.apache.lucene.document.StoredField; +import org.apache.lucene.document.XYDocValuesField; +import org.apache.lucene.document.XYPointField; +import org.apache.lucene.index.IndexOptions; +import org.apache.lucene.index.IndexableField; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.DocValuesFieldExistsQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TermQuery; +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.Explicit; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.index.mapper.AbstractSearchableGeometryFieldType; +import org.elasticsearch.index.mapper.ArrayValueMapperParser; +import org.elasticsearch.index.mapper.FieldMapper; +import org.elasticsearch.index.mapper.FieldNamesFieldMapper; +import org.elasticsearch.index.mapper.GeoPointFieldMapper; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.mapper.Mapper; +import org.elasticsearch.index.mapper.MapperParsingException; +import org.elasticsearch.index.mapper.ParseContext; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.index.query.QueryShardException; +import org.elasticsearch.xpack.spatial.index.query.ShapeQueryPointProcessor; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.index.mapper.TypeParsers.parseField; + + +/** + * Field Mapper for point type. + * + * Uses lucene 8 XYPoint encoding + */ +public class PointFieldMapper extends FieldMapper implements ArrayValueMapperParser { + public static final String CONTENT_TYPE = "point"; + + public static class Names { + public static final ParseField IGNORE_MALFORMED = new ParseField("ignore_malformed"); + public static final ParseField IGNORE_Z_VALUE = new ParseField("ignore_z_value"); + public static final ParseField NULL_VALUE = new ParseField("null_value"); + } + + public static class Defaults { + public static final Explicit IGNORE_MALFORMED = new Explicit<>(false, false); + public static final PointFieldType FIELD_TYPE = new PointFieldType(); + public static final Explicit IGNORE_Z_VALUE = new Explicit<>(true, false); + + static { + FIELD_TYPE.setTokenized(false); + FIELD_TYPE.setHasDocValues(true); + FIELD_TYPE.setDimensions(2, Integer.BYTES); + FIELD_TYPE.freeze(); + } + } + + public static class Builder extends FieldMapper.Builder { + protected Boolean ignoreMalformed; + private Boolean ignoreZValue; + + public Builder(String name) { + super(name, Defaults.FIELD_TYPE, Defaults.FIELD_TYPE); + builder = this; + } + + public Builder ignoreMalformed(boolean ignoreMalformed) { + this.ignoreMalformed = ignoreMalformed; + return builder; + } + + protected Explicit ignoreMalformed(BuilderContext context) { + if (ignoreMalformed != null) { + return new Explicit<>(ignoreMalformed, true); + } + if (context.indexSettings() != null) { + return new Explicit<>(IGNORE_MALFORMED_SETTING.get(context.indexSettings()), false); + } + return PointFieldMapper.Defaults.IGNORE_MALFORMED; + } + + protected Explicit ignoreZValue(BuilderContext context) { + if (ignoreZValue != null) { + return new Explicit<>(ignoreZValue, true); + } + return PointFieldMapper.Defaults.IGNORE_Z_VALUE; + } + + public PointFieldMapper.Builder ignoreZValue(final boolean ignoreZValue) { + this.ignoreZValue = ignoreZValue; + return this; + } + public PointFieldMapper build(BuilderContext context, String simpleName, MappedFieldType fieldType, + MappedFieldType defaultFieldType, Settings indexSettings, + MultiFields multiFields, Explicit ignoreMalformed, + CopyTo copyTo) { + setupFieldType(context); + return new PointFieldMapper(simpleName, fieldType, defaultFieldType, indexSettings, multiFields, + ignoreMalformed, ignoreZValue(context), copyTo); + } + + @Override + public PointFieldType fieldType() { + return (PointFieldType)fieldType; + } + + @Override + public PointFieldMapper build(BuilderContext context) { + return build(context, name, fieldType, defaultFieldType, context.indexSettings(), + multiFieldsBuilder.build(this, context), ignoreMalformed(context), copyTo); + } + + @Override + protected void setupFieldType(BuilderContext context) { + super.setupFieldType(context); + + fieldType().setGeometryQueryBuilder(new ShapeQueryPointProcessor()); + } + } + + public static class TypeParser implements Mapper.TypeParser { + @Override + @SuppressWarnings("rawtypes") + public Mapper.Builder parse(String name, Map node, ParserContext parserContext) + throws MapperParsingException { + Builder builder = new PointFieldMapper.Builder(name); + parseField(builder, name, node, parserContext); + Object nullValue = null; + for (Iterator> iterator = node.entrySet().iterator(); iterator.hasNext();) { + Map.Entry entry = iterator.next(); + String propName = entry.getKey(); + Object propNode = entry.getValue(); + + if (propName.equals(Names.IGNORE_MALFORMED.getPreferredName())) { + builder.ignoreMalformed(XContentMapValues.nodeBooleanValue(propNode, name + "." + Names.IGNORE_MALFORMED)); + iterator.remove(); + } else if (propName.equals(PointFieldMapper.Names.IGNORE_Z_VALUE.getPreferredName())) { + builder.ignoreZValue(XContentMapValues.nodeBooleanValue(propNode, + name + "." + PointFieldMapper.Names.IGNORE_Z_VALUE.getPreferredName())); + iterator.remove(); + } else if (propName.equals(Names.NULL_VALUE.getPreferredName())) { + if (propNode == null) { + throw new MapperParsingException("Property [null_value] cannot be null."); + } + nullValue = propNode; + iterator.remove(); + } + } + + if (nullValue != null) { + boolean ignoreMalformed = builder.ignoreMalformed == null ? + Defaults.IGNORE_MALFORMED.value() : builder.ignoreMalformed; + boolean ignoreZValue = builder.ignoreZValue == null ? + Defaults.IGNORE_Z_VALUE.value() : builder.ignoreZValue; + CartesianPoint point = CartesianPoint.parsePoint(nullValue, ignoreZValue); + if (ignoreMalformed == false) { + if (Float.isFinite(point.getX()) == false) { + throw new IllegalArgumentException("illegal x value [" + point.getX() + "]"); + } + if (Float.isFinite(point.getY()) == false) { + throw new IllegalArgumentException("illegal y value [" + point.getY() + "]"); + } + } + builder.nullValue(point); + } + return builder; + } + } + + protected Explicit ignoreMalformed; + protected Explicit ignoreZValue; + + public PointFieldMapper(String simpleName, MappedFieldType fieldType, MappedFieldType defaultFieldType, + Settings indexSettings, MultiFields multiFields, Explicit ignoreMalformed, + Explicit ignoreZValue, CopyTo copyTo) { + super(simpleName, fieldType, defaultFieldType, indexSettings, multiFields, copyTo); + this.ignoreMalformed = ignoreMalformed; + this.ignoreZValue = ignoreZValue; + } + + @Override + protected void doMerge(Mapper mergeWith) { + super.doMerge(mergeWith); + PointFieldMapper gpfmMergeWith = (PointFieldMapper) mergeWith; + if (gpfmMergeWith.ignoreMalformed.explicit()) { + this.ignoreMalformed = gpfmMergeWith.ignoreMalformed; + } + if (gpfmMergeWith.ignoreZValue.explicit()) { + this.ignoreZValue = gpfmMergeWith.ignoreZValue; + } + } + + @Override + protected String contentType() { + return CONTENT_TYPE; + } + + @Override + protected void parseCreateField(ParseContext context, List fields) throws IOException { + throw new UnsupportedOperationException("Parsing is implemented in parse(), this method should NEVER be called"); + } + + public static class PointFieldType extends AbstractSearchableGeometryFieldType { + public PointFieldType() { + } + + PointFieldType(PointFieldType ref) { + super(ref); + } + + @Override + public String typeName() { + return CONTENT_TYPE; + } + + @Override + public MappedFieldType clone() { + return new PointFieldType(this); + } + + @Override + public Query existsQuery(QueryShardContext context) { + if (hasDocValues()) { + return new DocValuesFieldExistsQuery(name()); + } else { + return new TermQuery(new Term(FieldNamesFieldMapper.NAME, name())); + } + } + + @Override + public Query termQuery(Object value, QueryShardContext context) { + throw new QueryShardException(context, "Spatial fields do not support exact searching, " + + "use dedicated spatial queries instead: [" + name() + "]"); + } + } + + protected void parse(ParseContext context, CartesianPoint point) throws IOException { + + if (fieldType().indexOptions() != IndexOptions.NONE) { + context.doc().add(new XYPointField(fieldType().name(), point.getX(), point.getY())); + } + if (fieldType().stored()) { + context.doc().add(new StoredField(fieldType().name(), point.toString())); + } + if (fieldType.hasDocValues()) { + context.doc().add(new XYDocValuesField(fieldType().name(), point.getX(), point.getY())); + } else if (fieldType().stored() || fieldType().indexOptions() != IndexOptions.NONE) { + List fields = new ArrayList<>(1); + createFieldNamesField(context, fields); + for (IndexableField field : fields) { + context.doc().add(field); + } + } + // if the mapping contains multi-fields then throw an error? + if (multiFields.iterator().hasNext()) { + throw new ElasticsearchParseException("[{}] field type does not accept multi-fields", CONTENT_TYPE); + } + } + + @Override + public void parse(ParseContext context) throws IOException { + context.path().add(simpleName()); + + try { + CartesianPoint sparse = context.parseExternalValue(CartesianPoint.class); + + if (sparse != null) { + parse(context, sparse); + } else { + sparse = new CartesianPoint(); + XContentParser.Token token = context.parser().currentToken(); + if (token == XContentParser.Token.START_ARRAY) { + token = context.parser().nextToken(); + if (token == XContentParser.Token.VALUE_NUMBER) { + float x = context.parser().floatValue(); + context.parser().nextToken(); + float y = context.parser().floatValue(); + token = context.parser().nextToken(); + if (token == XContentParser.Token.VALUE_NUMBER) { + CartesianPoint.assertZValue(ignoreZValue.value(), context.parser().floatValue()); + } else if (token != XContentParser.Token.END_ARRAY) { + throw new ElasticsearchParseException("[{}] field type does not accept > 3 dimensions", CONTENT_TYPE); + } + parse(context, sparse.reset(x, y)); + } else { + while (token != XContentParser.Token.END_ARRAY) { + parsePointIgnoringMalformed(context, sparse); + token = context.parser().nextToken(); + } + } + } else if (token == XContentParser.Token.VALUE_NULL) { + if (fieldType.nullValue() != null) { + parse(context, (CartesianPoint) fieldType.nullValue()); + } + } else { + parsePointIgnoringMalformed(context, sparse); + } + } + } catch (Exception ex) { + throw new MapperParsingException("failed to parse field [{}] of type [{}]", ex, fieldType().name(), fieldType().typeName()); + } + + context.path().remove(); + } + + /** + * Parses point represented as an object or an array, ignores malformed points if needed + */ + private void parsePointIgnoringMalformed(ParseContext context, CartesianPoint sparse) throws IOException { + try { + parse(context, CartesianPoint.parsePoint(context.parser(), sparse, ignoreZValue().value())); + } catch (ElasticsearchParseException e) { + if (ignoreMalformed.value() == false) { + throw e; + } + context.addIgnoredField(fieldType.name()); + } + } + + @Override + protected void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException { + super.doXContentBody(builder, includeDefaults, params); + if (includeDefaults || ignoreMalformed.explicit()) { + builder.field(Names.IGNORE_MALFORMED.getPreferredName(), ignoreMalformed.value()); + } + if (includeDefaults || ignoreZValue.explicit()) { + builder.field(GeoPointFieldMapper.Names.IGNORE_Z_VALUE.getPreferredName(), ignoreZValue.value()); + } + if (includeDefaults || fieldType().nullValue() != null) { + builder.field(Names.NULL_VALUE.getPreferredName(), fieldType().nullValue()); + } + } + + public Explicit ignoreZValue() { + return ignoreZValue; + } +} 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 33a183f5260..bc0771a66bb 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 @@ -17,13 +17,14 @@ import org.elasticsearch.common.logging.DeprecationLogger; 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.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.PointFieldMapper; import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper; import java.io.IOException; @@ -48,7 +49,7 @@ public class ShapeQueryBuilder extends AbstractGeometryQueryBuilder validContentTypes = - Collections.unmodifiableList(Arrays.asList(ShapeFieldMapper.CONTENT_TYPE)); + Collections.unmodifiableList(Arrays.asList(ShapeFieldMapper.CONTENT_TYPE, PointFieldMapper.CONTENT_TYPE)); /** * Creates a new GeoShapeQueryBuilder whose Query will be against the given @@ -138,7 +139,7 @@ public class ShapeQueryBuilder extends AbstractGeometryQueryBuilder { + 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) { + XYCircle xyCircle = ShapeUtils.toLuceneXYCircle(circle); + Query query = XYPointField.newDistanceQuery(fieldName, xyCircle.getX(), xyCircle.getY(), xyCircle.getRadius()); + if (fieldType.hasDocValues()) { + Query dvQuery = XYDocValuesField.newSlowDistanceQuery(fieldName, + xyCircle.getX(), xyCircle.getY(), xyCircle.getRadius()); + 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 " + + ShapeType.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 " + + ShapeType.MULTILINESTRING + " queries"); + } + + @Override + public Query visit(MultiPoint multiPoint) { + throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + + ShapeType.MULTIPOINT + " queries"); + } + + @Override + public Query visit(MultiPolygon multiPolygon) { + org.apache.lucene.geo.XYPolygon[] lucenePolygons = + new org.apache.lucene.geo.XYPolygon[multiPolygon.size()]; + for (int i = 0; i < multiPolygon.size(); i++) { + lucenePolygons[i] = ShapeUtils.toLuceneXYPolygon(multiPolygon.get(i)); + } + Query query = XYPointField.newPolygonQuery(fieldName, lucenePolygons); + if (fieldType.hasDocValues()) { + Query dvQuery = XYDocValuesField.newSlowPolygonQuery(fieldName, lucenePolygons); + query = new IndexOrDocValuesQuery(query, dvQuery); + } + return query; + } + + @Override + public Query visit(Point point) { + // not currently supported + throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + ShapeType.POINT + + " queries"); + } + + @Override + public Query visit(Polygon polygon) { + org.apache.lucene.geo.XYPolygon lucenePolygon = ShapeUtils.toLuceneXYPolygon(polygon); + Query query = XYPointField.newPolygonQuery(fieldName, lucenePolygon); + if (fieldType.hasDocValues()) { + Query dvQuery = XYDocValuesField.newSlowPolygonQuery(fieldName, lucenePolygon); + query = new IndexOrDocValuesQuery(query, dvQuery); + } + return query; + } + + @Override + public Query visit(Rectangle r) { + XYRectangle xyRectangle = ShapeUtils.toLuceneXYRectangle(r); + Query query = XYPointField.newBoxQuery(fieldName, xyRectangle.minX, xyRectangle.maxX, xyRectangle.minY, xyRectangle.maxY); + if (fieldType.hasDocValues()) { + Query dvQuery = XYDocValuesField.newSlowBoxQuery( + fieldName, xyRectangle.minX, xyRectangle.maxX, xyRectangle.minY, xyRectangle.maxY); + query = new IndexOrDocValuesQuery(query, dvQuery); + } + return query; + } + } +} diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianFieldMapperTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianFieldMapperTests.java new file mode 100644 index 00000000000..0f31f481dd3 --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianFieldMapperTests.java @@ -0,0 +1,175 @@ +/* + * 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.mapper; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.compress.CompressedXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.mapper.DocumentMapper; +import org.elasticsearch.index.mapper.DocumentMapperParser; +import org.elasticsearch.index.mapper.MapperParsingException; +import org.elasticsearch.index.mapper.ParsedDocument; +import org.elasticsearch.index.mapper.SourceToParse; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.elasticsearch.test.InternalSettingsPlugin; +import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; +import org.elasticsearch.xpack.spatial.SpatialPlugin; + +import java.io.IOException; +import java.util.Collection; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +/** Base class for testing cartesian field mappers */ +public abstract class CartesianFieldMapperTests extends ESSingleNodeTestCase { + + private static final String FIELD_NAME = "location"; + + @Override + protected Collection> getPlugins() { + return pluginList(InternalSettingsPlugin.class, SpatialPlugin.class, LocalStateCompositeXPackPlugin.class); + } + + protected abstract XContentBuilder createDefaultMapping(String fieldName, + boolean ignored_malformed, + boolean ignoreZValue) throws IOException; + + + public void testWKT() throws IOException { + String mapping = Strings.toString(createDefaultMapping(FIELD_NAME, randomBoolean(), randomBoolean())); + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser() + .parse("type", new CompressedXContent(mapping)); + + ParsedDocument doc = defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .field(FIELD_NAME, "POINT (2000.1 305.6)") + .endObject()), + XContentType.JSON)); + + assertThat(doc.rootDoc().getField(FIELD_NAME), notNullValue()); + } + + public void testEmptyName() throws IOException { + String mapping = Strings.toString(createDefaultMapping("", randomBoolean(), randomBoolean())); + + DocumentMapperParser parser = createIndex("test").mapperService().documentMapperParser(); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> parser.parse("type", new CompressedXContent(mapping)) + ); + assertThat(e.getMessage(), containsString("name cannot be empty string")); + } + + public void testInvalidPointValuesIgnored() throws IOException { + String mapping = Strings.toString(createDefaultMapping(FIELD_NAME, true, randomBoolean())); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser() + .parse("type", new CompressedXContent(mapping)); + + assertThat(defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field(FIELD_NAME, "1234.333").endObject() + ), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field("lat", "-").field("x", 1.3).endObject() + ), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field("lat", 1.3).field("y", "-").endObject() + ), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field(FIELD_NAME, "-,1.3").endObject() + ), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field(FIELD_NAME, "1.3,-").endObject() + ), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field("x", "NaN").field("y", "NaN").endObject() + ), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field("lat", 12).field("y", "NaN").endObject() + ), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field("x", "NaN").field("y", 10).endObject() + ), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field(FIELD_NAME, "NaN,NaN").endObject() + ), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field(FIELD_NAME, "10,NaN").endObject() + ), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field(FIELD_NAME, "NaN,12").endObject() + ), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().startObject(FIELD_NAME).nullField("y").field("x", 1).endObject().endObject() + ), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().startObject(FIELD_NAME).nullField("x").nullField("y").endObject().endObject() + ), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue()); + } + + public void testZValue() throws IOException { + String mapping = Strings.toString(createDefaultMapping(FIELD_NAME, false, true)); + DocumentMapper defaultMapper = createIndex("test1").mapperService().documentMapperParser() + .parse("type", new CompressedXContent(mapping)); + + ParsedDocument doc = defaultMapper.parse(new SourceToParse("test1","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .field(FIELD_NAME, "POINT (2000.1 305.6 34567.33)") + .endObject()), + XContentType.JSON)); + + assertThat(doc.rootDoc().getField(FIELD_NAME), notNullValue()); + + mapping = Strings.toString(createDefaultMapping(FIELD_NAME, false, false)); + DocumentMapper defaultMapper2 = createIndex("test2").mapperService().documentMapperParser() + .parse("type", new CompressedXContent(mapping)); + + MapperParsingException e = expectThrows(MapperParsingException.class, + () -> defaultMapper2.parse(new SourceToParse("test2","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .field(FIELD_NAME, "POINT (2000.1 305.6 34567.33)") + .endObject()), + XContentType.JSON)) + ); + assertThat(e.getMessage(), containsString("failed to parse field [" + FIELD_NAME + "] of type")); + assertThat(e.getRootCause().getMessage(), + containsString("found Z value [34567.33] but [ignore_z_value] parameter is [false]")); + } +} diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapperTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapperTests.java new file mode 100644 index 00000000000..cd04672e650 --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapperTests.java @@ -0,0 +1,297 @@ +/* + * 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.mapper; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.compress.CompressedXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.mapper.DocumentMapper; +import org.elasticsearch.index.mapper.Mapper; +import org.elasticsearch.index.mapper.ParsedDocument; +import org.elasticsearch.index.mapper.SourceToParse; +import org.hamcrest.CoreMatchers; + +import java.io.IOException; + +import static org.elasticsearch.index.mapper.GeoPointFieldMapper.Names.IGNORE_Z_VALUE; +import static org.elasticsearch.xpack.spatial.index.mapper.PointFieldMapper.Names.NULL_VALUE; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; + +public class PointFieldMapperTests extends CartesianFieldMapperTests { + + @Override + protected XContentBuilder createDefaultMapping(String fieldName, + boolean ignored_malformed, + boolean ignoreZValue) throws IOException { + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject(fieldName).field("type", "point"); + if (ignored_malformed || randomBoolean()) { + xContentBuilder.field(PointFieldMapper.Names.IGNORE_MALFORMED.getPreferredName(), ignored_malformed); + } + if (ignoreZValue == false || randomBoolean()) { + xContentBuilder.field(PointFieldMapper.Names.IGNORE_Z_VALUE.getPreferredName(), ignoreZValue); + } + return xContentBuilder.endObject().endObject().endObject().endObject(); + } + + public void testValuesStored() throws Exception { + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("point").field("type", "point"); + String mapping = Strings.toString(xContentBuilder.field("store", true).endObject().endObject().endObject().endObject()); + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser() + .parse("type", new CompressedXContent(mapping)); + + ParsedDocument doc = defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .startObject("point").field("x", 2000.1).field("y", 305.6).endObject() + .endObject()), + XContentType.JSON)); + + assertThat(doc.rootDoc().getField("point"), notNullValue()); + } + + public void testArrayValues() throws Exception { + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("point").field("type", "point").field("doc_values", false); + String mapping = Strings.toString(xContentBuilder.field("store", true).endObject().endObject().endObject().endObject()); + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser() + .parse("type", new CompressedXContent(mapping)); + + ParsedDocument doc = defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .startArray("point") + .startObject().field("x", 1.2).field("y", 1.3).endObject() + .startObject().field("x", 1.4).field("y", 1.5).endObject() + .endArray() + .endObject()), + XContentType.JSON)); + + // doc values are enabled by default, but in this test we disable them; we should only have 2 points + assertThat(doc.rootDoc().getFields("point"), notNullValue()); + assertThat(doc.rootDoc().getFields("point").length, equalTo(4)); + } + + public void testLatLonInOneValue() throws Exception { + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("point").field("type", "point"); + String mapping = Strings.toString(xContentBuilder.endObject().endObject().endObject().endObject()); + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser() + .parse("type", new CompressedXContent(mapping)); + + ParsedDocument doc = defaultMapper.parse(new SourceToParse("test", "type","1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .field("point", "1.2,1.3") + .endObject()), + XContentType.JSON)); + + assertThat(doc.rootDoc().getField("point"), notNullValue()); + } + + public void testInOneValueStored() throws Exception { + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("point").field("type", "point"); + String mapping = Strings.toString(xContentBuilder.field("store", true).endObject().endObject().endObject().endObject()); + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser() + .parse("type", new CompressedXContent(mapping)); + + ParsedDocument doc = defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .field("point", "1.2,1.3") + .endObject()), + XContentType.JSON)); + assertThat(doc.rootDoc().getField("point"), notNullValue()); + } + + public void testLatLonInOneValueArray() throws Exception { + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("point").field("type", "point").field("doc_values", false); + String mapping = Strings.toString(xContentBuilder.field("store", true).endObject().endObject().endObject().endObject()); + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser() + .parse("type", new CompressedXContent(mapping)); + + ParsedDocument doc = defaultMapper.parse(new SourceToParse("test", "type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .startArray("point") + .value("1.2,1.3") + .value("1.4,1.5") + .endArray() + .endObject()), + XContentType.JSON)); + + // doc values are enabled by default, but in this test we disable them; we should only have 2 points + assertThat(doc.rootDoc().getFields("point"), notNullValue()); + assertThat(doc.rootDoc().getFields("point").length, equalTo(4)); + } + + public void testArray() throws Exception { + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("point").field("type", "point"); + String mapping = Strings.toString(xContentBuilder.endObject().endObject().endObject().endObject()); + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser() + .parse("type", new CompressedXContent(mapping)); + + ParsedDocument doc = defaultMapper.parse(new SourceToParse("test", "type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .startArray("point").value(1.3).value(1.2).endArray() + .endObject()), + XContentType.JSON)); + + assertThat(doc.rootDoc().getField("point"), notNullValue()); + } + + public void testArrayDynamic() throws Exception { + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type") + .startArray("dynamic_templates").startObject().startObject("point").field("match", "point*") + .startObject("mapping").field("type", "point"); + String mapping = Strings.toString(xContentBuilder.endObject().endObject().endObject().endArray().endObject().endObject()); + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser() + .parse("type", new CompressedXContent(mapping)); + + ParsedDocument doc = defaultMapper.parse(new SourceToParse("test", "type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .startArray("point").value(1.3).value(1.2).endArray() + .endObject()), + XContentType.JSON)); + + assertThat(doc.rootDoc().getField("point"), notNullValue()); + } + + public void testArrayStored() throws Exception { + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("point").field("type", "point"); + String mapping = Strings.toString(xContentBuilder.field("store", true).endObject().endObject().endObject().endObject()); + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser() + .parse("type", new CompressedXContent(mapping)); + + ParsedDocument doc = defaultMapper.parse(new SourceToParse("test", "type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .startArray("point").value(1.3).value(1.2).endArray() + .endObject()), + XContentType.JSON)); + + assertThat(doc.rootDoc().getField("point"), notNullValue()); + assertThat(doc.rootDoc().getFields("point").length, equalTo(3)); + } + + public void testArrayArrayStored() throws Exception { + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("point").field("type", "point"); + String mapping = Strings.toString(xContentBuilder.field("store", true) + .field("doc_values", false).endObject().endObject() + .endObject().endObject()); + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser() + .parse("type", new CompressedXContent(mapping)); + + ParsedDocument doc = defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .startArray("point") + .startArray().value(1.3).value(1.2).endArray() + .startArray().value(1.5).value(1.4).endArray() + .endArray() + .endObject()), + XContentType.JSON)); + + assertThat(doc.rootDoc().getFields("point"), notNullValue()); + assertThat(doc.rootDoc().getFields("point").length, CoreMatchers.equalTo(4)); + } + + public void testNullValue() throws Exception { + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("location") + .field("type", "point") + .field(NULL_VALUE.getPreferredName(), "1,2") + .endObject().endObject() + .endObject().endObject()); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser() + .parse("type", new CompressedXContent(mapping)); + Mapper fieldMapper = defaultMapper.mappers().getMapper("location"); + assertThat(fieldMapper, instanceOf(PointFieldMapper.class)); + + Object nullValue = ((PointFieldMapper) fieldMapper).fieldType().nullValue(); + assertThat(nullValue, equalTo(new CartesianPoint(1, 2))); + + ParsedDocument doc = defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .nullField("location") + .endObject()), + XContentType.JSON)); + + assertThat(doc.rootDoc().getField("location"), notNullValue()); + BytesRef defaultValue = doc.rootDoc().getField("location").binaryValue(); + + doc = defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .field("location", "1, 2") + .endObject()), + XContentType.JSON)); + // Shouldn't matter if we specify the value explicitly or use null value + assertThat(defaultValue, equalTo(doc.rootDoc().getField("location").binaryValue())); + + doc = defaultMapper.parse(new SourceToParse("test","type", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .field("location", "3, 4") + .endObject()), + XContentType.JSON)); + // Shouldn't matter if we specify the value explicitly or use null value + assertThat(defaultValue, not(equalTo(doc.rootDoc().getField("location").binaryValue()))); + } + + /** + * Test that accept_z_value parameter correctly parses + */ + public void testIgnoreZValue() throws IOException { + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1") + .startObject("properties").startObject("location") + .field("type", "point") + .field(IGNORE_Z_VALUE.getPreferredName(), "true") + .endObject().endObject() + .endObject().endObject()); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser() + .parse("type1", new CompressedXContent(mapping)); + Mapper fieldMapper = defaultMapper.mappers().getMapper("location"); + assertThat(fieldMapper, instanceOf(PointFieldMapper.class)); + + boolean ignoreZValue = ((PointFieldMapper)fieldMapper).ignoreZValue().value(); + assertThat(ignoreZValue, equalTo(true)); + + // explicit false accept_z_value test + mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1") + .startObject("properties").startObject("location") + .field("type", "point") + .field(IGNORE_Z_VALUE.getPreferredName(), "false") + .endObject().endObject() + .endObject().endObject()); + + defaultMapper = createIndex("test2").mapperService().documentMapperParser() + .parse("type1", new CompressedXContent(mapping)); + fieldMapper = defaultMapper.mappers().getMapper("location"); + assertThat(fieldMapper, instanceOf(PointFieldMapper.class)); + + ignoreZValue = ((PointFieldMapper)fieldMapper).ignoreZValue().value(); + assertThat(ignoreZValue, equalTo(false)); + } +} diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldTypeTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldTypeTests.java new file mode 100644 index 00000000000..45956d5ddb2 --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldTypeTests.java @@ -0,0 +1,16 @@ +/* + * 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.mapper; + +import org.elasticsearch.index.mapper.FieldTypeTestCase; +import org.elasticsearch.index.mapper.MappedFieldType; + +public class PointFieldTypeTests extends FieldTypeTestCase { + @Override + protected MappedFieldType createDefaultFieldType() { + return new PointFieldMapper.PointFieldType(); + } +} diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/ShapeFieldMapperTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/ShapeFieldMapperTests.java index 809f9d62139..3b118160e8c 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/ShapeFieldMapperTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/ShapeFieldMapperTests.java @@ -18,7 +18,6 @@ import org.elasticsearch.index.mapper.DocumentMapperParser; import org.elasticsearch.index.mapper.Mapper; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.test.InternalSettingsPlugin; import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; import org.elasticsearch.xpack.spatial.SpatialPlugin; @@ -28,17 +27,31 @@ import java.util.Collection; import java.util.Collections; import static org.elasticsearch.index.mapper.GeoPointFieldMapper.Names.IGNORE_Z_VALUE; -import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; /** testing for {@link org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper} */ -public class ShapeFieldMapperTests extends ESSingleNodeTestCase { +public class ShapeFieldMapperTests extends CartesianFieldMapperTests { @Override protected Collection> getPlugins() { return pluginList(InternalSettingsPlugin.class, SpatialPlugin.class, LocalStateCompositeXPackPlugin.class); } + @Override + protected XContentBuilder createDefaultMapping(String fieldName, + boolean ignored_malformed, + boolean ignoreZValue) throws IOException { + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject(fieldName).field("type", "shape"); + if (ignored_malformed || randomBoolean()) { + xContentBuilder.field("ignore_malformed", ignored_malformed); + } + if (ignoreZValue == false || randomBoolean()) { + xContentBuilder.field(PointFieldMapper.Names.IGNORE_Z_VALUE.getPreferredName(), ignoreZValue); + } + return xContentBuilder.endObject().endObject().endObject().endObject(); + } + public void testDefaultConfiguration() throws IOException { String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1") .startObject("properties").startObject("location") @@ -249,21 +262,6 @@ public class ShapeFieldMapperTests extends ESSingleNodeTestCase { assertThat(shapeFieldMapper.fieldType().orientation(), equalTo(ShapeBuilder.Orientation.CW)); } - public void testEmptyName() throws Exception { - // after 5.x - String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1") - .startObject("properties").startObject("") - .field("type", "shape") - .endObject().endObject() - .endObject().endObject()); - DocumentMapperParser parser = createIndex("test").mapperService().documentMapperParser(); - - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, - () -> parser.parse("type1", new CompressedXContent(mapping)) - ); - assertThat(e.getMessage(), containsString("name cannot be empty string")); - } - public void testSerializeDefaults() throws Exception { DocumentMapperParser parser = createIndex("test").mapperService().documentMapperParser(); { diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderOverPointTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderOverPointTests.java new file mode 100644 index 00000000000..b608d11483a --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderOverPointTests.java @@ -0,0 +1,49 @@ +/* + * 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.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.compress.CompressedXContent; +import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.ShapeType; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.xpack.spatial.util.ShapeTestUtils; + +import java.io.IOException; + + +public class ShapeQueryBuilderOverPointTests extends ShapeQueryBuilderTests { + + @Override + protected void initializeAdditionalMappings(MapperService mapperService) throws IOException { + mapperService.merge(docType, new CompressedXContent(Strings.toString(PutMappingRequest.buildFromSimplifiedDef(docType, + fieldName(), "type=point"))), MapperService.MergeReason.MAPPING_UPDATE); + } + + @Override + protected ShapeRelation getShapeRelation(ShapeType type) { + return ShapeRelation.INTERSECTS; + } + + @Override + protected Geometry getGeometry() { + if (randomBoolean()) { + if (randomBoolean()) { + return ShapeTestUtils.randomMultiPolygon(false); + } else { + return ShapeTestUtils.randomPolygon(false); + } + } else if (randomBoolean()) { + // it should be a circle + return ShapeTestUtils.randomPolygon(false); + } else { + return ShapeTestUtils.randomRectangle(); + } + } +} diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderOverShapeTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderOverShapeTests.java new file mode 100644 index 00000000000..71ad0e9bab8 --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderOverShapeTests.java @@ -0,0 +1,52 @@ +/* + * 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.elasticsearch.Version; +import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.compress.CompressedXContent; +import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.ShapeType; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.xpack.spatial.util.ShapeTestUtils; + +import java.io.IOException; + +public class ShapeQueryBuilderOverShapeTests extends ShapeQueryBuilderTests { + + @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); + } + + @Override + protected ShapeRelation getShapeRelation(ShapeType type) { + QueryShardContext context = createShardContext(); + if (context.indexVersionCreated().onOrAfter(Version.V_7_5_0)) { // CONTAINS is only supported from version 7.5 + if (type == ShapeType.LINESTRING || type == ShapeType.MULTILINESTRING) { + return randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, ShapeRelation.CONTAINS); + } else { + return randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, + ShapeRelation.WITHIN, ShapeRelation.CONTAINS); + } + } else { + if (type == ShapeType.LINESTRING || type == ShapeType.MULTILINESTRING) { + return randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS); + } else { + return randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, ShapeRelation.WITHIN); + } + } + } + + @Override + protected Geometry getGeometry() { + return ShapeTestUtils.randomGeometry(false); + } +} diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderTests.java index d5e97c0becc..48f44a59498 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderTests.java @@ -10,13 +10,10 @@ import org.apache.lucene.search.ConstantScoreQuery; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.Version; -import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.geo.GeoJson; import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.io.stream.BytesStreamOutput; @@ -36,7 +33,6 @@ import org.elasticsearch.index.query.Rewriteable; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.AbstractQueryTestCase; import org.elasticsearch.xpack.spatial.SpatialPlugin; -import org.elasticsearch.xpack.spatial.util.ShapeTestUtils; import org.junit.After; import java.io.IOException; @@ -49,11 +45,11 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.equalTo; -public class ShapeQueryBuilderTests extends AbstractQueryTestCase { +public abstract class ShapeQueryBuilderTests extends AbstractQueryTestCase { protected static final String SHAPE_FIELD_NAME = "mapped_shape"; - private static String docType = "_doc"; + protected static String docType = "_doc"; protected static String indexedShapeId; protected static String indexedShapeType; @@ -62,17 +58,15 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase> 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; } @@ -83,7 +77,7 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase new ShapeQueryBuilder(null, shape)); assertEquals("fieldName is required", e.getMessage()); } @@ -168,7 +148,7 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase builder.relation(null)); assertEquals("No Shape Relation defined", e.getMessage()); @@ -223,7 +203,7 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase failingQueryBuilder.toQuery(createShardContext())); - assertThat(e.getMessage(), containsString("failed to find shape field [unmapped]")); + assertThat(e.getMessage(), containsString("failed to find shape or point field [unmapped]")); } public void testWrongFieldType() { - Geometry shape = ShapeTestUtils.randomGeometry(false); + Geometry shape = getGeometry(); final ShapeQueryBuilder queryBuilder = new ShapeQueryBuilder(TEXT_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]")); + assertThat(e.getMessage(), containsString("Field [mapped_string] is not of type [shape or point] but of type [text]")); } public void testSerializationFailsUnlessFetched() throws IOException { diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryOverPointTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryOverPointTests.java new file mode 100644 index 00000000000..cddbc7b8dd7 --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryOverPointTests.java @@ -0,0 +1,160 @@ +/* + * 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.search.SearchAction; +import org.elasticsearch.action.search.SearchPhaseExecutionException; +import org.elasticsearch.action.search.SearchRequestBuilder; +import org.elasticsearch.common.Strings; +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.common.xcontent.XContentType; +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.geometry.ShapeType; +import org.elasticsearch.xpack.spatial.index.query.ShapeQueryBuilder; +import org.hamcrest.CoreMatchers; + +public class ShapeQueryOverPointTests extends ShapeQueryTests { + @Override + protected XContentBuilder createDefaultMapping() throws Exception { + XContentBuilder xcb = XContentFactory.jsonBuilder().startObject() + .startObject("properties").startObject(defaultFieldName) + .field("type", "point") + .endObject().endObject().endObject(); + + return xcb; + } + + public void testProcessRelationSupport() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate("test").addMapping(defaultFieldType, mapping, XContentType.JSON).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(new ShapeQueryBuilder(defaultFieldName, rectangle) + .relation(shapeRelation)) + .get()); + assertThat(e.getCause().getMessage(), + CoreMatchers.containsString(shapeRelation + + " query relation not supported for Field [" + defaultFieldName + "]")); + } + } + } + + public void testQueryLine() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate("test").addMapping(defaultFieldType, mapping, XContentType.JSON).get(); + ensureGreen(); + + Line line = new Line(new double[]{-25, -25}, new double[]{-35, -35}); + + try { + client().prepareSearch("test") + .setQuery(new ShapeQueryBuilder(defaultFieldName, line)).get(); + } catch ( + SearchPhaseExecutionException e) { + assertThat(e.getCause().getMessage(), + CoreMatchers.containsString("does not support " + ShapeType.LINESTRING + " queries")); + } + } + + public void testQueryLinearRing() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate("test").addMapping(defaultFieldType, mapping, XContentType.JSON).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 + ShapeQueryBuilder queryBuilder = new ShapeQueryBuilder(defaultFieldName, linearRing); + SearchRequestBuilder searchRequestBuilder = new SearchRequestBuilder(client(), SearchAction.INSTANCE); + searchRequestBuilder.setQuery(queryBuilder); + searchRequestBuilder.setIndices("test"); + searchRequestBuilder.get(); + } catch ( + SearchPhaseExecutionException e) { + assertThat(e.getCause().getMessage(), + CoreMatchers.containsString("Field [" + defaultFieldName + "] does not support LINEARRING queries")); + } + } + + public void testQueryMultiLine() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate("test").addMapping(defaultFieldType, mapping, XContentType.JSON).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(new ShapeQueryBuilder(defaultFieldName, multiline)).get(); + } catch (Exception e) { + assertThat(e.getCause().getMessage(), + CoreMatchers.containsString("does not support " + ShapeType.MULTILINESTRING + " queries")); + } + } + + public void testQueryMultiPoint() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate("test").addMapping(defaultFieldType, mapping, XContentType.JSON).get(); + ensureGreen(); + + MultiPointBuilder mpb = new MultiPointBuilder().coordinate(-35,-25).coordinate(-15,-5); + MultiPoint multiPoint = mpb.buildGeometry(); + + try { + client().prepareSearch("test") + .setQuery(new ShapeQueryBuilder(defaultFieldName, multiPoint)).get(); + } catch (Exception e) { + assertThat(e.getCause().getMessage(), + CoreMatchers.containsString("does not support " + ShapeType.MULTIPOINT + " queries")); + } + } + + public void testQueryPoint() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate("test").addMapping(defaultFieldType, mapping, XContentType.JSON).get(); + ensureGreen(); + + PointBuilder pb = new PointBuilder().coordinate(-35, -25); + Point point = pb.buildGeometry(); + + try { + client().prepareSearch("test") + .setQuery(new ShapeQueryBuilder(defaultFieldName, point)).get(); + } catch (Exception e) { + assertThat(e.getCause().getMessage(), + CoreMatchers.containsString("does not support " + ShapeType.POINT + " queries")); + } + } + +} diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryOverShapeTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryOverShapeTests.java new file mode 100644 index 00000000000..82c0a41448c --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryOverShapeTests.java @@ -0,0 +1,349 @@ +/* + * 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.index.IndexRequest; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.common.geo.GeoJson; +import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.common.geo.builders.EnvelopeBuilder; +import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder; +import org.elasticsearch.common.geo.builders.MultiPointBuilder; +import org.elasticsearch.common.geo.builders.PointBuilder; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.ShapeType; +import org.elasticsearch.index.query.ExistsQueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; +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.io.IOException; +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 ShapeQueryOverShapeTests extends ShapeQueryTests { + + private static String INDEX = "test"; + private static String IGNORE_MALFORMED_INDEX = INDEX + "_ignore_malformed"; + private static String FIELD = "shape"; + private static Geometry queryGeometry = null; + + private int numDocs; + + @Override + protected XContentBuilder createDefaultMapping() throws Exception { + XContentBuilder xcb = XContentFactory.jsonBuilder().startObject() + .startObject("properties").startObject(defaultFieldName) + .field("type", "shape") + .endObject().endObject().endObject(); + + return xcb; + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + // create test index + assertAcked(client().admin().indices().prepareCreate(INDEX) + .addMapping(defaultFieldType, FIELD, "type=shape", "alias", "type=alias,path=" + FIELD).get()); + // create index that ignores malformed geometry + assertAcked(client().admin().indices().prepareCreate(IGNORE_MALFORMED_INDEX) + .addMapping(defaultFieldType,FIELD, "type=shape,ignore_malformed=true", "_source", "enabled=false").get()); + ensureGreen(); + + // index random shapes + numDocs = randomIntBetween(25, 50); + // reset query geometry to make sure we pick one from the indexed shapes + queryGeometry = null; + 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, defaultFieldType).setSource(geoJson).setRefreshPolicy(IMMEDIATE).get(); + client().prepareIndex(IGNORE_MALFORMED_INDEX, defaultFieldType).setRefreshPolicy(IMMEDIATE).setSource(geoJson).get(); + } catch (Exception e) { + // sometimes GeoTestUtil will create invalid geometry; catch and continue: + if (queryGeometry == geometry) { + // reset query geometry as it didn't get indexed + queryGeometry = null; + } + --i; + continue; + } + } + } + + public void testIndexedShapeReferenceSourceDisabled() throws Exception { + EnvelopeBuilder shape = new EnvelopeBuilder(new Coordinate(-45, 45), new Coordinate(45, -45)); + + client().prepareIndex(IGNORE_MALFORMED_INDEX, defaultFieldType).setId("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(defaultFieldType,"location", "type=shape").get(); + + String location = "\"location\" : {\"type\":\"polygon\", \"coordinates\":[[[-10,-10],[10,-10],[10,10],[-10,10],[-10,-10]]]}"; + + client().prepareIndex(indexName, defaultFieldType).setId("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, defaultFieldType).setId("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, LocalStateCompositeXPackPlugin.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, defaultFieldType).setId("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, defaultFieldType).setId("aNullshape").setSource("{\"" + FIELD + "\": null}", XContentType.JSON) + .setRefreshPolicy(IMMEDIATE).get(); + client().prepareIndex(IGNORE_MALFORMED_INDEX, defaultFieldType).setId("aNullshape").setSource("{\"" + FIELD + "\": null}", + XContentType.JSON).setRefreshPolicy(IMMEDIATE).get(); + GetResponse result = client().prepareGet(INDEX, defaultFieldType, "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); + } + + public void testContainsShapeQuery() { + + client().admin().indices().prepareCreate("test_contains").addMapping(defaultFieldType,"location", "type=shape") + .execute().actionGet(); + + String doc = "{\"location\" : {\"type\":\"envelope\", \"coordinates\":[ [-100.0, 100.0], [100.0, -100.0]]}}"; + client().prepareIndex("test_contains", defaultFieldType).setId("1") + .setSource(doc, XContentType.JSON).setRefreshPolicy(IMMEDIATE).get(); + + // index the mbr of the collection + EnvelopeBuilder queryShape = new EnvelopeBuilder(new Coordinate(-50, 50), new Coordinate(50, -50)); + ShapeQueryBuilder queryBuilder = + new ShapeQueryBuilder("location", queryShape.buildGeometry()).relation(ShapeRelation.CONTAINS); + SearchResponse response = client().prepareSearch("test_contains").setQuery(queryBuilder).get(); + assertSearchResponse(response); + + assertThat(response.getHits().getTotalHits().value, equalTo(1L)); + } + + public void testGeometryCollectionRelations() throws IOException { + XContentBuilder mapping = XContentFactory.jsonBuilder().startObject() + .startObject("properties") + .startObject("geometry").field("type", "shape").endObject() + .endObject() + .endObject(); + + createIndex("test_collections", Settings.builder().put("index.number_of_shards", 1).build() + , defaultFieldType, mapping); + + EnvelopeBuilder envelopeBuilder = new EnvelopeBuilder(new Coordinate(-10, 10), new Coordinate(10, -10)); + + client().index(new IndexRequest("test_collections") + .source(jsonBuilder().startObject().field("geometry", envelopeBuilder).endObject()) + .setRefreshPolicy(IMMEDIATE)).actionGet(); + + { + // A geometry collection that is fully within the indexed shape + GeometryCollectionBuilder builder = new GeometryCollectionBuilder(); + builder.shape(new PointBuilder(1, 2)); + builder.shape(new PointBuilder(-2, -1)); + SearchResponse response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS)) + .get(); + assertEquals(1, response.getHits().getTotalHits().value); + response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS)) + .get(); + assertEquals(1, response.getHits().getTotalHits().value); + response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT)) + .get(); + assertEquals(0, response.getHits().getTotalHits().value); + } + { + // A geometry collection (as multi point) that is partially within the indexed shape + MultiPointBuilder builder = new MultiPointBuilder(); + builder.coordinate(1, 2); + builder.coordinate(20, 30); + SearchResponse response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS)) + .get(); + assertEquals(0, response.getHits().getTotalHits().value); + response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS)) + .get(); + assertEquals(1, response.getHits().getTotalHits().value); + response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT)) + .get(); + assertEquals(0, response.getHits().getTotalHits().value); + } + { + // A geometry collection that is disjoint with the indexed shape + GeometryCollectionBuilder builder = new GeometryCollectionBuilder(); + MultiPointBuilder innerBuilder = new MultiPointBuilder(); + innerBuilder.coordinate(-20, -30); + innerBuilder.coordinate(20, 30); + builder.shape(innerBuilder); + SearchResponse response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS)) + .get(); + assertEquals(0, response.getHits().getTotalHits().value); + response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS)) + .get(); + assertEquals(0, response.getHits().getTotalHits().value); + response = client().prepareSearch("test_collections") + .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT)) + .get(); + assertEquals(1, response.getHits().getTotalHits().value); + } + } +} 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 index 2b81d96eb30..9d2f11f0f0e 100644 --- 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 @@ -8,336 +8,322 @@ package org.elasticsearch.xpack.spatial.search; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.search.SearchResponse; -import org.elasticsearch.common.geo.GeoJson; +import org.elasticsearch.common.Strings; 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.MultiPointBuilder; +import org.elasticsearch.common.geo.builders.MultiPolygonBuilder; import org.elasticsearch.common.geo.builders.PointBuilder; -import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.geo.builders.PolygonBuilder; import org.elasticsearch.common.unit.DistanceUnit; 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.ShapeType; -import org.elasticsearch.index.query.ExistsQueryBuilder; -import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.geometry.Rectangle; import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.search.SearchHits; import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; 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.io.IOException; + 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.not; 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); - // reset query geometry to make sure we pick one from the indexed shapes - queryGeometry = null; - 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: - if (queryGeometry == geometry) { - // reset query geometry as it didn't get indexed - queryGeometry = null; - } - --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); - } +public abstract class ShapeQueryTests extends ESSingleNodeTestCase { @Override protected Collection> getPlugins() { return pluginList(SpatialPlugin.class, LocalStateCompositeXPackPlugin.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); - } + protected abstract XContentBuilder createDefaultMapping() throws Exception; - /** - * 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" + - "}"; + static String defaultFieldName = "xy"; + static String defaultIndexName = "test-points"; + static String defaultFieldType = "doc"; - client().prepareIndex(INDEX, FIELD_TYPE, "0").setSource(source, XContentType.JSON).setRouting("ABC").get(); - client().admin().indices().prepareRefresh(INDEX).get(); + public void testNullShape() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate(defaultIndexName) + .addMapping(defaultFieldType, mapping, XContentType.JSON).get(); + ensureGreen(); - 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) + client().prepareIndex(defaultIndexName, defaultFieldType, "aNullshape") + .setSource("{\"geo\": 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()); - } + GetResponse result = client().prepareGet(defaultIndexName, defaultFieldType, "aNullshape").get(); + assertThat(result.getField("location"), nullValue()); + }; - public void testExistsQuery() { - ExistsQueryBuilder eqb = QueryBuilders.existsQuery(FIELD); - SearchResponse result = client().prepareSearch(INDEX).setQuery(eqb).get(); - assertSearchResponse(result); - assertHitCount(result, numDocs); - } + public void testIndexPointsFilterRectangle() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate(defaultIndexName) + .addMapping(defaultFieldType, mapping, XContentType.JSON).get(); + ensureGreen(); - public void testFieldAlias() { - SearchResponse response = client().prepareSearch(INDEX) - .setQuery(new ShapeQueryBuilder("alias", queryGeometry).relation(ShapeRelation.INTERSECTS)) + client().prepareIndex(defaultIndexName, defaultFieldType).setId("1").setSource(jsonBuilder() + .startObject() + .field("name", "Document 1") + .field(defaultFieldName, "POINT(-30 -30)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName, defaultFieldType).setId("2").setSource(jsonBuilder() + .startObject() + .field("name", "Document 2") + .field(defaultFieldName, "POINT(-45 -50)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + EnvelopeBuilder shape = new EnvelopeBuilder(new Coordinate(-45, 45), new Coordinate(45, -45)); + GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(shape); + Geometry geometry = builder.buildGeometry().get(0); + SearchResponse searchResponse = client().prepareSearch(defaultIndexName) + .setQuery(new ShapeQueryBuilder(defaultFieldName, geometry) + .relation(ShapeRelation.INTERSECTS)) .get(); - assertTrue(response.getHits().getTotalHits().value > 0); + + assertSearchResponse(searchResponse); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + assertThat(searchResponse.getHits().getHits().length, equalTo(1)); + assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("1")); + + // default query, without specifying relation (expect intersects) + searchResponse = client().prepareSearch(defaultIndexName) + .setQuery(new ShapeQueryBuilder(defaultFieldName, geometry)) + .get(); + + assertSearchResponse(searchResponse); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + assertThat(searchResponse.getHits().getHits().length, equalTo(1)); + assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("1")); } - public void testContainsShapeQuery() { + public void testIndexPointsCircle() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate(defaultIndexName) + .addMapping(defaultFieldType, mapping, XContentType.JSON).get(); + ensureGreen(); - client().admin().indices().prepareCreate("test_contains").addMapping("type", "location", "type=shape") - .execute().actionGet(); + client().prepareIndex(defaultIndexName, defaultFieldType).setId("1").setSource(jsonBuilder() + .startObject() + .field("name", "Document 1") + .field(defaultFieldName, "POINT(-30 -30)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); - String doc = "{\"location\" : {\"type\":\"envelope\", \"coordinates\":[ [-100.0, 100.0], [100.0, -100.0]]}}"; - client().prepareIndex("test_contains", "type").setId("1").setSource(doc, XContentType.JSON).setRefreshPolicy(IMMEDIATE).get(); + client().prepareIndex(defaultIndexName, defaultFieldType).setId("2").setSource(jsonBuilder() + .startObject() + .field("name", "Document 2") + .field(defaultFieldName, "POINT(-45 -50)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); - // index the mbr of the collection - EnvelopeBuilder queryShape = new EnvelopeBuilder(new Coordinate(-50, 50), new Coordinate(50, -50)); - ShapeQueryBuilder queryBuilder = new ShapeQueryBuilder("location", queryShape.buildGeometry()).relation(ShapeRelation.CONTAINS); - SearchResponse response = client().prepareSearch("test_contains").setQuery(queryBuilder).get(); - assertSearchResponse(response); + CircleBuilder shape = new CircleBuilder().center(new Coordinate(-30, -30)).radius("1"); + GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(shape); + Geometry geometry = builder.buildGeometry().get(0); - assertThat(response.getHits().getTotalHits().value, equalTo(1L)); + SearchResponse searchResponse = client().prepareSearch(defaultIndexName) + .setQuery(new ShapeQueryBuilder(defaultFieldName, geometry) + .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("1")); } - public void testGeometryCollectionRelations() throws IOException { - XContentBuilder mapping = XContentFactory.jsonBuilder().startObject() - .startObject("doc") - .startObject("properties") - .startObject("geometry").field("type", "shape").endObject() + public void testIndexPointsPolygon() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate(defaultIndexName) + .addMapping(defaultFieldType, mapping, XContentType.JSON).get(); + ensureGreen(); + + client().prepareIndex(defaultIndexName, defaultFieldType).setId("1").setSource(jsonBuilder() + .startObject() + .field(defaultFieldName, "POINT(-30 -30)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName, defaultFieldType).setId("2").setSource(jsonBuilder() + .startObject() + .field(defaultFieldName, "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(new ShapeQueryBuilder(defaultFieldName, 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) + .addMapping(defaultFieldType, mapping, XContentType.JSON).get(); + ensureGreen(); + + client().prepareIndex(defaultIndexName, defaultFieldType).setId("1").setSource(jsonBuilder() + .startObject() + .field("name", "Document 1") + .field(defaultFieldName, "POINT(-30 -30)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName, defaultFieldType).setId("2").setSource(jsonBuilder() + .startObject() + .field("name", "Document 2") + .field(defaultFieldName, "POINT(-40 -40)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName, defaultFieldType).setId("3").setSource(jsonBuilder() + .startObject() + .field("name", "Document 3") + .field(defaultFieldName, "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(new ShapeQueryBuilder(defaultFieldName, 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) + .addMapping(defaultFieldType, mapping, XContentType.JSON).get(); + ensureGreen(); + + client().prepareIndex(defaultIndexName, defaultFieldType).setId("1").setSource(jsonBuilder() + .startObject() + .field("name", "Document 1") + .field(defaultFieldName, "POINT(-30 -30)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName, defaultFieldType).setId("2").setSource(jsonBuilder() + .startObject() + .field("name", "Document 2") + .field(defaultFieldName, "POINT(-45 -50)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + Rectangle rectangle = new Rectangle(-50, -40, -45, -55); + + SearchResponse searchResponse = client().prepareSearch(defaultIndexName) + .setQuery(new ShapeQueryBuilder(defaultFieldName, 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) + .addMapping(defaultFieldType, mapping, XContentType.JSON).get(); + ensureGreen(); + + client().prepareIndex(defaultIndexName, defaultFieldType).setId("point1").setSource(jsonBuilder() + .startObject() + .field(defaultFieldName, "POINT(-30 -30)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName, defaultFieldType).setId("point2").setSource(jsonBuilder() + .startObject() + .field(defaultFieldName, "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", "shape") .endObject() .endObject() - .endObject(); + .endObject()); + client().admin().indices().prepareCreate(indexedShapeIndex) + .addMapping(defaultFieldType, queryShapesMapping, XContentType.JSON); + ensureGreen(); - createIndex("test_collections", Settings.builder().put("index.number_of_shards", 1).build(), "doc", mapping); + client().prepareIndex(indexedShapeIndex, defaultFieldType).setId("shape1").setSource(jsonBuilder() + .startObject() + .field(indexedShapePath, "BBOX(-50, -40, -45, -55)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); - EnvelopeBuilder envelopeBuilder = new EnvelopeBuilder(new Coordinate(-10, 10), new Coordinate(10, -10)); + client().prepareIndex(indexedShapeIndex, defaultFieldType).setId("shape2").setSource(jsonBuilder() + .startObject() + .field(indexedShapePath, "BBOX(-60, -50, -50, -60)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); - client().index(new IndexRequest("test_collections") - .source(jsonBuilder().startObject().field("geometry", envelopeBuilder).endObject()) - .setRefreshPolicy(IMMEDIATE)).actionGet(); + SearchResponse searchResponse = client().prepareSearch(defaultIndexName) + .setQuery(new ShapeQueryBuilder(defaultFieldName, "shape1") + .relation(ShapeRelation.INTERSECTS) + .indexedShapeIndex(indexedShapeIndex) + .indexedShapePath(indexedShapePath)) + .get(); - { - // A geometry collection that is fully within the indexed shape - GeometryCollectionBuilder builder = new GeometryCollectionBuilder(); - builder.shape(new PointBuilder(1, 2)); - builder.shape(new PointBuilder(-2, -1)); - SearchResponse response = client().prepareSearch("test_collections") - .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS)) - .get(); - assertEquals(1, response.getHits().getTotalHits().value); - response = client().prepareSearch("test_collections") - .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS)) - .get(); - assertEquals(1, response.getHits().getTotalHits().value); - response = client().prepareSearch("test_collections") - .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT)) - .get(); - assertEquals(0, response.getHits().getTotalHits().value); - } - { - // A geometry collection (as multi point) that is partially within the indexed shape - MultiPointBuilder builder = new MultiPointBuilder(); - builder.coordinate(1, 2); - builder.coordinate(20, 30); - SearchResponse response = client().prepareSearch("test_collections") - .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS)) - .get(); - assertEquals(0, response.getHits().getTotalHits().value); - response = client().prepareSearch("test_collections") - .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS)) - .get(); - assertEquals(1, response.getHits().getTotalHits().value); - response = client().prepareSearch("test_collections") - .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT)) - .get(); - assertEquals(0, response.getHits().getTotalHits().value); - } - { - // A geometry collection that is disjoint with the indexed shape - GeometryCollectionBuilder builder = new GeometryCollectionBuilder(); - MultiPointBuilder innerBuilder = new MultiPointBuilder(); - innerBuilder.coordinate(-20, -30); - innerBuilder.coordinate(20, 30); - builder.shape(innerBuilder); - SearchResponse response = client().prepareSearch("test_collections") - .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS)) - .get(); - assertEquals(0, response.getHits().getTotalHits().value); - response = client().prepareSearch("test_collections") - .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS)) - .get(); - assertEquals(0, response.getHits().getTotalHits().value); - response = client().prepareSearch("test_collections") - .setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT)) - .get(); - assertEquals(1, response.getHits().getTotalHits().value); - } + 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(new ShapeQueryBuilder(defaultFieldName, "shape2") + .relation(ShapeRelation.INTERSECTS) + .indexedShapeIndex(indexedShapeIndex) + .indexedShapePath(indexedShapePath)) + .get(); + assertSearchResponse(searchResponse); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(0L)); } public void testDistanceQuery() throws Exception {