From 916a52b5c9a4aea7402c984b912f41875cfedc7b Mon Sep 17 00:00:00 2001 From: iverase Date: Thu, 19 Mar 2020 15:50:21 +0100 Subject: [PATCH 01/11] Add new point field. --- docs/reference/mapping/types/point.asciidoc | 92 ++++ .../query-dsl/shape-queries.asciidoc | 15 +- .../mapper/AbstractGeometryFieldMapper.java | 4 +- .../xpack/spatial/SpatialPlugin.java | 2 + .../spatial/index/mapper/CartesianPoint.java | 252 +++++++++ .../index/mapper/PointFieldMapper.java | 336 ++++++++++++ .../index/query/ShapeQueryBuilder.java | 9 +- .../index/query/ShapeQueryPointProcessor.java | 174 ++++++ .../index/mapper/PointFieldMapperTests.java | 380 +++++++++++++ .../index/mapper/PointFieldTypeTests.java | 29 + .../index/mapper/ShapeFieldMapperTests.java | 1 - .../ShapeQueryBuilderOverPointTests.java | 49 ++ .../ShapeQueryBuilderOverShapeTests.java | 52 ++ .../index/query/ShapeQueryBuilderTests.java | 48 +- .../search/ShapeQueryOverPointTests.java | 154 ++++++ .../search/ShapeQueryOverShapeTests.java | 348 ++++++++++++ .../xpack/spatial/search/ShapeQueryTests.java | 519 +++++++++--------- 17 files changed, 2148 insertions(+), 316 deletions(-) create mode 100644 docs/reference/mapping/types/point.asciidoc create mode 100644 x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianPoint.java create mode 100644 x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapper.java create mode 100644 x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryPointProcessor.java create mode 100644 x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapperTests.java create mode 100644 x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldTypeTests.java create mode 100644 x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderOverPointTests.java create mode 100644 x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderOverShapeTests.java create mode 100644 x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryOverPointTests.java create mode 100644 x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryOverShapeTests.java diff --git a/docs/reference/mapping/types/point.asciidoc b/docs/reference/mapping/types/point.asciidoc new file mode 100644 index 0000000000000..721a83a3360ef --- /dev/null +++ b/docs/reference/mapping/types/point.asciidoc @@ -0,0 +1,92 @@ +[[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. + +<>:: + + 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 204ebab9cecef..13f60fc4b378c 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/server/src/main/java/org/elasticsearch/index/mapper/AbstractGeometryFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/AbstractGeometryFieldMapper.java index d065e8121dc05..5c522d9bb890e 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/AbstractGeometryFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/AbstractGeometryFieldMapper.java @@ -89,7 +89,7 @@ public interface Parser { } public abstract static class Builder - extends FieldMapper.Builder { + extends FieldMapper.Builder { protected Boolean coerce; protected Boolean ignoreMalformed; protected Boolean ignoreZValue; @@ -187,7 +187,7 @@ protected void setupFieldType(BuilderContext context) { public static class TypeParser implements Mapper.TypeParser { protected boolean parseXContentParameters(String name, Map.Entry entry, Map params) - throws MapperParsingException { + throws MapperParsingException { if (DeprecatedParameters.parse(name, entry.getKey(), entry.getValue(), (DeprecatedParameters)params.get(DEPRECATED_PARAMETERS_KEY))) { return true; 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 b15d3e620d203..0ea259742ba4d 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 @@ -17,6 +17,7 @@ import org.elasticsearch.plugins.SearchPlugin; import org.elasticsearch.xpack.core.action.XPackInfoFeatureAction; import org.elasticsearch.xpack.core.action.XPackUsageFeatureAction; +import org.elasticsearch.xpack.spatial.index.mapper.PointFieldMapper; import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper; import org.elasticsearch.xpack.spatial.index.query.ShapeQueryBuilder; import org.elasticsearch.xpack.spatial.ingest.CircleProcessor; @@ -45,6 +46,7 @@ public SpatialPlugin(Settings settings) { 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 0000000000000..fb7d57ad9a969 --- /dev/null +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianPoint.java @@ -0,0 +1,252 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.xpack.spatial.index.mapper; + +import org.elasticsearch.ElasticsearchParseException; +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; + +/** + * Represents a point in the cartesian space. + */ +public final class CartesianPoint implements ToXContentFragment { + + 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) { + if (value.toLowerCase(Locale.ROOT).contains("point")) { + return resetFromWKT(value); + } else { + return resetFromCoordinates(value); + } + } + + + public CartesianPoint resetFromCoordinates(String value) { + String[] vals = value.split(","); + if (vals.length != 2) { + throw new ElasticsearchParseException("failed to parse [{}], expected 2 coordinates " + + "but found: [{}]", vals.length); + } + final float x; + final float y; + try { + x = Float.parseFloat(vals[0].trim()); + if (Float.isFinite(x) == false) { + throw new ElasticsearchParseException("invalid x value [{}]; " + + "must be between -3.4028234663852886E38 and 3.4028234663852886E38", x); + } + } catch (NumberFormatException ex) { + throw new ElasticsearchParseException("x must be a number"); + } + try { + y = Float.parseFloat(vals[1].trim()); + if (Float.isFinite(y) == false) { + throw new ElasticsearchParseException("invalid y value [{}]; " + + "must be between -3.4028234663852886E38 and 3.4028234663852886E38", y); + } + } catch (NumberFormatException ex) { + throw new ElasticsearchParseException("y must be a number"); + } + return reset(x, y); + } + + private CartesianPoint resetFromWKT(String value) { + Geometry geometry; + try { + geometry = new WellKnownText(false, new StandardValidator(true)) + .fromWKT(value); + } catch (Exception e) { + throw new ElasticsearchParseException("Invalid WKT format", e); + } + if (geometry.type() != ShapeType.POINT) { + throw new ElasticsearchParseException("[geo_point] supports only POINT among WKT primitives, " + + "but found " + 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", x).field("y", y).endObject(); + } + + public static CartesianPoint parsePoint(XContentParser parser, CartesianPoint point) + 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 ("x".equals(field)) { + 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("x must be a number"); + } + } else if ("y".equals(field)) { + 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("y must be a number"); + } + } else { + throw new ElasticsearchParseException("field must be either [{}] or [{}]", "x", "y"); + } + } else { + throw new ElasticsearchParseException("token [{}] not allowed", subParser.currentToken()); + } + } + } + if (numberFormatException != null) { + throw new ElasticsearchParseException("[{}] and [{}] must be valid float values", numberFormatException, "x", "y"); + } else if (Float.isNaN(x)) { + throw new ElasticsearchParseException("field [{}] missing", x); + } else if (Float.isNaN(y)) { + throw new ElasticsearchParseException("field [{}] missing", y); + } 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("[point] field type does not accept > 2 dimensions"); + } + } 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); + } else { + throw new ElasticsearchParseException("point expected"); + } + } + + public static CartesianPoint parsePoint(Object value) 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()); + } catch (IOException ex) { + throw new ElasticsearchParseException("error parsing point", ex); + } + } +} 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 0000000000000..dcce52f036239 --- /dev/null +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapper.java @@ -0,0 +1,336 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.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.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.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.search.aggregations.support.CoreValuesSourceType; +import org.elasticsearch.search.aggregations.support.ValuesSourceType; +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 String IGNORE_MALFORMED = "ignore_malformed"; + public static final String NULL_VALUE = "null_value"; + } + + public static class Defaults { + public static final Explicit IGNORE_MALFORMED = new Explicit<>(false, false); + public static final PointFieldType FIELD_TYPE = new PointFieldType(); + + 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; + + 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; + } + + 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, 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)) { + builder.ignoreMalformed(XContentMapValues.nodeBooleanValue(propNode, name + "." + Names.IGNORE_MALFORMED)); + iterator.remove(); + } else if (propName.equals(Names.NULL_VALUE)) { + 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; + CartesianPoint point = CartesianPoint.parsePoint(nullValue); + 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; + + public PointFieldMapper(String simpleName, MappedFieldType fieldType, MappedFieldType defaultFieldType, + Settings indexSettings, MultiFields multiFields, Explicit ignoreMalformed, + CopyTo copyTo) { + super(simpleName, fieldType, defaultFieldType, indexSettings, multiFields, copyTo); + this.ignoreMalformed = ignoreMalformed; + } + + @Override + protected void doMerge(Mapper mergeWith) { + super.doMerge(mergeWith); + PointFieldMapper gpfmMergeWith = (PointFieldMapper) mergeWith; + if (gpfmMergeWith.ignoreMalformed.explicit()) { + this.ignoreMalformed = gpfmMergeWith.ignoreMalformed; + } + } + + @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 ValuesSourceType getValuesSourceType() { + return CoreValuesSourceType.GEOPOINT; + } + + @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, "Geo fields do not support exact searching, use dedicated geo 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.END_ARRAY) { + throw new ElasticsearchParseException("[{}] field type does not accept > 2 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 geopoints if needed + */ + private void parsePointIgnoringMalformed(ParseContext context, CartesianPoint sparse) throws IOException { + try { + parse(context, CartesianPoint.parsePoint(context.parser(), sparse)); + } 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, ignoreMalformed.value()); + } + + if (includeDefaults || fieldType().nullValue() != null) { + builder.field(Names.NULL_VALUE, fieldType().nullValue()); + } + } +} 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 1989b4ab18019..480e4b2544501 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 @@ -14,12 +14,13 @@ 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.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; @@ -38,7 +39,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 @@ -121,8 +122,8 @@ public Query buildShapeQuery(QueryShardContext context, MappedFieldType fieldTyp + "] but of type [" + fieldType.typeName() + "]"); } - final AbstractGeometryFieldMapper.AbstractGeometryFieldType ft = (AbstractGeometryFieldMapper.AbstractGeometryFieldType) fieldType; - return new ConstantScoreQuery(ft.geometryQueryBuilder().process(shape, ft.name(), relation, context)); + final AbstractSearchableGeometryFieldType ft = (AbstractSearchableGeometryFieldType) fieldType; + return new ConstantScoreQuery(ft.geometryQueryBuilder().process(shape, ft.name(), relation, context)); } @Override diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryPointProcessor.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryPointProcessor.java new file mode 100644 index 0000000000000..3cca94b3a3fd4 --- /dev/null +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryPointProcessor.java @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.spatial.index.query; + +import org.apache.lucene.document.XYDocValuesField; +import org.apache.lucene.document.XYPointField; +import org.apache.lucene.geo.XYCircle; +import org.apache.lucene.geo.XYRectangle; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.IndexOrDocValuesQuery; +import org.apache.lucene.search.Query; +import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.geometry.Circle; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.GeometryCollection; +import org.elasticsearch.geometry.GeometryVisitor; +import org.elasticsearch.geometry.LinearRing; +import org.elasticsearch.geometry.MultiLine; +import org.elasticsearch.geometry.MultiPoint; +import org.elasticsearch.geometry.MultiPolygon; +import org.elasticsearch.geometry.Point; +import org.elasticsearch.geometry.Polygon; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.geometry.ShapeType; +import org.elasticsearch.index.mapper.AbstractSearchableGeometryFieldType; +import org.elasticsearch.index.mapper.MappedFieldType; +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.ShapeUtils; + + +public class ShapeQueryPointProcessor implements AbstractSearchableGeometryFieldType.QueryProcessor { + + @Override + public Query process(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context) { + validateIsPointFieldType(fieldName, context); + // geo points only support intersects + if (relation != ShapeRelation.INTERSECTS) { + throw new QueryShardException(context, + relation+ " query relation not supported for Field [" + fieldName + "]."); + } + // wrap geoQuery as a ConstantScoreQuery + return getVectorQueryFromShape(shape, fieldName, relation, context); + } + + private void validateIsPointFieldType(String fieldName, QueryShardContext context) { + MappedFieldType fieldType = context.fieldMapper(fieldName); + if (fieldType instanceof PointFieldMapper.PointFieldType == false) { + throw new QueryShardException(context, "Expected " + PointFieldMapper.CONTENT_TYPE + + " field type for Field [" + fieldName + "] but found " + fieldType.typeName()); + } + } + + protected Query getVectorQueryFromShape( + Geometry queryShape, String fieldName, ShapeRelation relation, QueryShardContext context) { + ShapeVisitor shapeVisitor = new ShapeVisitor(context, fieldName, relation); + return queryShape.visit(shapeVisitor); + } + + private class ShapeVisitor implements GeometryVisitor { + 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/PointFieldMapperTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapperTests.java new file mode 100644 index 0000000000000..a38c42bbf0ea0 --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapperTests.java @@ -0,0 +1,380 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.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.DocumentMapperParser; +import org.elasticsearch.index.mapper.Mapper; +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 org.hamcrest.CoreMatchers; + +import java.util.Collection; + +import static org.elasticsearch.xpack.spatial.index.mapper.PointFieldMapper.Names.NULL_VALUE; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class PointFieldMapperTests extends ESSingleNodeTestCase { + + @Override + protected Collection> getPlugins() { + return pluginList(InternalSettingsPlugin.class, SpatialPlugin.class, LocalStateCompositeXPackPlugin.class); + } + + + public void testWKT() 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", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .field("point", "POINT (2000.1 305.6)") + .endObject()), + XContentType.JSON)); + + assertThat(doc.rootDoc().getField("point"), notNullValue()); + } + + 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", "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", "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", "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", "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", "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", "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", "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", "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", "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 testEmptyName() throws Exception { + // after 5.x + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("").field("type", "point").endObject().endObject() + .endObject().endObject()); + + 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 testNullValue() throws Exception { + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("location") + .field("type", "point") + .field(NULL_VALUE, "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", "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", "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", "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()))); + } + + public void testInvalidPointValuesIgnored() throws Exception { + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties") + .startObject("location") + .field("type", "point") + .field("ignore_malformed", "true") + .endObject() + .endObject().endObject().endObject()); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser() + .parse("type", new CompressedXContent(mapping)); + + assertThat(defaultMapper.parse(new SourceToParse("test", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field("location", "1234.333").endObject() + ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field("lat", "-").field("x", 1.3).endObject() + ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field("lat", 1.3).field("y", "-").endObject() + ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field("location", "-,1.3").endObject() + ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field("location", "1.3,-").endObject() + ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field("x", "NaN").field("y", "NaN").endObject() + ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field("lat", 12).field("y", "NaN").endObject() + ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field("x", "NaN").field("y", 10).endObject() + ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field("location", "NaN,NaN").endObject() + ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field("location", "10,NaN").endObject() + ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().field("location", "NaN,12").endObject() + ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().startObject("location").nullField("y").field("x", 1).endObject().endObject() + ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); + + assertThat(defaultMapper.parse(new SourceToParse("test", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().startObject("location").nullField("x").nullField("y").endObject().endObject() + ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); + } +} 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 0000000000000..5398f5063dd34 --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldTypeTests.java @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.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 809f9d621395e..1e97aaa1a9fa1 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 @@ -250,7 +250,6 @@ public void testShapeMapperMerge() throws Exception { } public void testEmptyName() throws Exception { - // after 5.x String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1") .startObject("properties").startObject("") .field("type", "shape") 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 0000000000000..6b582574b4bfb --- /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.simpleMapping( + 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 0000000000000..13e9244575c43 --- /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.simpleMapping( + 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 a12bbcd5232bb..908469769a309 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.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; @@ -27,7 +24,6 @@ import org.elasticsearch.geometry.Geometry; import org.elasticsearch.geometry.ShapeType; import org.elasticsearch.index.get.GetResult; -import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryShardContext; @@ -36,7 +32,6 @@ 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 +44,11 @@ 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 indexedShapePath; @@ -61,16 +56,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.simpleMapping( - fieldName(), "type=shape"))), MapperService.MergeReason.MAPPING_UPDATE); - } protected String fieldName() { return SHAPE_FIELD_NAME; @@ -82,7 +76,7 @@ protected ShapeQueryBuilder doCreateTestQueryBuilder() { } protected ShapeQueryBuilder doCreateTestQueryBuilder(boolean indexedShape) { - Geometry shape = ShapeTestUtils.randomGeometry(false); + Geometry shape = getGeometry(); ShapeQueryBuilder builder; clearShapeFields(); @@ -107,21 +101,7 @@ protected ShapeQueryBuilder doCreateTestQueryBuilder(boolean indexedShape) { } if (randomBoolean()) { - QueryShardContext context = createShardContext(); - if (context.indexVersionCreated().onOrAfter(Version.V_7_5_0)) { // CONTAINS is only supported from version 7.5 - if (shape.type() == ShapeType.LINESTRING || shape.type() == ShapeType.MULTILINESTRING) { - builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, ShapeRelation.CONTAINS)); - } else { - builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, - ShapeRelation.WITHIN, ShapeRelation.CONTAINS)); - } - } else { - if (shape.type() == ShapeType.LINESTRING || shape.type() == ShapeType.MULTILINESTRING) { - builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS)); - } else { - builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, ShapeRelation.WITHIN)); - } - } + builder.relation(getShapeRelation(shape.type())); } if (randomBoolean()) { @@ -149,7 +129,7 @@ protected void doAssertLuceneQuery(ShapeQueryBuilder queryBuilder, Query query, } public void testNoFieldName() { - Geometry shape = ShapeTestUtils.randomGeometry(false); + Geometry shape = getGeometry(); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new ShapeQueryBuilder(null, shape)); assertEquals("fieldName is required", e.getMessage()); } @@ -165,7 +145,7 @@ public void testNoIndexedShape() { } public void testNoRelation() { - Geometry shape = ShapeTestUtils.randomGeometry(false); + Geometry shape = getGeometry(); ShapeQueryBuilder builder = new ShapeQueryBuilder(fieldName(), shape); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> builder.relation(null)); assertEquals("No Shape Relation defined", e.getMessage()); @@ -220,7 +200,7 @@ public void testMultipleRewrite() { } public void testIgnoreUnmapped() throws IOException { - Geometry shape = ShapeTestUtils.randomGeometry(false); + Geometry shape = getGeometry(); final ShapeQueryBuilder queryBuilder = new ShapeQueryBuilder("unmapped", shape); queryBuilder.ignoreUnmapped(true); Query query = queryBuilder.toQuery(createShardContext()); @@ -230,14 +210,14 @@ public void testIgnoreUnmapped() throws IOException { final ShapeQueryBuilder failingQueryBuilder = new ShapeQueryBuilder("unmapped", shape); failingQueryBuilder.ignoreUnmapped(false); QueryShardException e = expectThrows(QueryShardException.class, () -> failingQueryBuilder.toQuery(createShardContext())); - assertThat(e.getMessage(), containsString("failed to find shape field [unmapped]")); + 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(STRING_FIELD_NAME, shape); QueryShardException e = expectThrows(QueryShardException.class, () -> queryBuilder.toQuery(createShardContext())); - assertThat(e.getMessage(), containsString("Field [mapped_string] is not of type [shape] but of type [text]")); + 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 0000000000000..7efb3d514e31a --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryOverPointTests.java @@ -0,0 +1,154 @@ +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.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").setMapping(mapping).get(); + ensureGreen(); + + Rectangle rectangle = new Rectangle(-35, -25, -25, -35); + + for (ShapeRelation shapeRelation : ShapeRelation.values()) { + if (!shapeRelation.equals(ShapeRelation.INTERSECTS)) { + SearchPhaseExecutionException e = expectThrows(SearchPhaseExecutionException.class, () -> + client().prepareSearch("test") + .setQuery(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").setMapping(mapping).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").setMapping(mapping).get(); + ensureGreen(); + + LinearRing linearRing = new LinearRing(new double[]{-25,-35,-25}, new double[]{-25,-35,-25}); + + try { + // LinearRing extends Line implements Geometry: expose the build process + 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").setMapping(mapping).get(); + ensureGreen(); + + CoordinatesBuilder coords1 = new CoordinatesBuilder() + .coordinate(-35,-35) + .coordinate(-25,-25); + CoordinatesBuilder coords2 = new CoordinatesBuilder() + .coordinate(-15,-15) + .coordinate(-5,-5); + LineStringBuilder lsb1 = new LineStringBuilder(coords1); + LineStringBuilder lsb2 = new LineStringBuilder(coords2); + MultiLineStringBuilder mlb = new MultiLineStringBuilder().linestring(lsb1).linestring(lsb2); + MultiLine multiline = (MultiLine) mlb.buildGeometry(); + + try { + client().prepareSearch("test") + .setQuery(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").setMapping(mapping).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").setMapping(mapping).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 0000000000000..0cfe66eab4ec9 --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryOverShapeTests.java @@ -0,0 +1,348 @@ +/* + * 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) + .setMapping(FIELD, "type=shape", "alias", "type=alias,path=" + FIELD).get()); + // create index that ignores malformed geometry + assertAcked(client().admin().indices().prepareCreate(IGNORE_MALFORMED_INDEX) + .setMapping(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).setSource(geoJson).setRefreshPolicy(IMMEDIATE).get(); + client().prepareIndex(IGNORE_MALFORMED_INDEX).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).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).setMapping("location", "type=shape").get(); + + String location = "\"location\" : {\"type\":\"polygon\", \"coordinates\":[[[-10,-10],[10,-10],[10,10],[-10,10],[-10,-10]]]}"; + + client().prepareIndex(indexName).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).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).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).setId("aNullshape").setSource("{\"" + FIELD + "\": null}", XContentType.JSON) + .setRefreshPolicy(IMMEDIATE).get(); + client().prepareIndex(IGNORE_MALFORMED_INDEX).setId("aNullshape").setSource("{\"" + FIELD + "\": null}", + XContentType.JSON).setRefreshPolicy(IMMEDIATE).get(); + GetResponse result = client().prepareGet(INDEX, "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").setMapping("location", "type=shape") + .execute().actionGet(); + + String doc = "{\"location\" : {\"type\":\"envelope\", \"coordinates\":[ [-100.0, 100.0], [100.0, -100.0]]}}"; + client().prepareIndex("test_contains").setId("1").setSource(doc, XContentType.JSON).setRefreshPolicy(IMMEDIATE).get(); + + // index the mbr of the collection + EnvelopeBuilder queryShape = new EnvelopeBuilder(new Coordinate(-50, 50), new Coordinate(50, -50)); + ShapeQueryBuilder queryBuilder = new ShapeQueryBuilder("location", queryShape.buildGeometry()).relation(ShapeRelation.CONTAINS); + SearchResponse response = client().prepareSearch("test_contains").setQuery(queryBuilder).get(); + assertSearchResponse(response); + + assertThat(response.getHits().getTotalHits().value, equalTo(1L)); + } + + public void testGeometryCollectionRelations() throws IOException { + XContentBuilder mapping = XContentFactory.jsonBuilder().startObject() + .startObject("_doc") + .startObject("properties") + .startObject("geometry").field("type", "shape").endObject() + .endObject() + .endObject() + .endObject(); + + createIndex("test_collections", Settings.builder().put("index.number_of_shards", 1).build(), 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 98e149f1a8362..1accbb68bba12 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,335 +8,314 @@ 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 { +public abstract class ShapeQueryTests extends ESSingleNodeTestCase { - private static String INDEX = "test"; - private static String IGNORE_MALFORMED_INDEX = INDEX + "_ignore_malformed"; - private static String FIELD = "shape"; - private static Geometry queryGeometry = null; + @Override + protected Collection> getPlugins() { + return pluginList(SpatialPlugin.class, LocalStateCompositeXPackPlugin.class); + } - private int numDocs; + protected abstract XContentBuilder createDefaultMapping() throws Exception; - @Override - public void setUp() throws Exception { - super.setUp(); - - // create test index - assertAcked(client().admin().indices().prepareCreate(INDEX) - .setMapping(FIELD, "type=shape", "alias", "type=alias,path=" + FIELD).get()); - // create index that ignores malformed geometry - assertAcked(client().admin().indices().prepareCreate(IGNORE_MALFORMED_INDEX) - .setMapping(FIELD, "type=shape,ignore_malformed=true", "_source", "enabled=false").get()); + static String defaultFieldName = "xy"; + static String defaultIndexName = "test-points"; + + public void testNullShape() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate(defaultIndexName).setMapping(mapping).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).setSource(geoJson).setRefreshPolicy(IMMEDIATE).get(); - client().prepareIndex(IGNORE_MALFORMED_INDEX).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; - } - } - } + client().prepareIndex(defaultIndexName) + .setId("aNullshape") + .setSource("{\"geo\": null}", XContentType.JSON) + .setRefreshPolicy(IMMEDIATE).get(); + GetResponse result = client().prepareGet(defaultIndexName, "aNullshape").get(); + assertThat(result.getField("location"), nullValue()); + }; - public void testIndexedShapeReferenceSourceDisabled() throws Exception { - EnvelopeBuilder shape = new EnvelopeBuilder(new Coordinate(-45, 45), new Coordinate(45, -45)); + public void testIndexPointsFilterRectangle() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate(defaultIndexName).setMapping(mapping).get(); + ensureGreen(); - client().prepareIndex(IGNORE_MALFORMED_INDEX).setId("Big_Rectangle").setSource(jsonBuilder().startObject() - .field(FIELD, shape).endObject()).setRefreshPolicy(IMMEDIATE).get(); + client().prepareIndex(defaultIndexName).setId("1").setSource(jsonBuilder() + .startObject() + .field("name", "Document 1") + .field(defaultFieldName, "POINT(-30 -30)") + .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")); - } + client().prepareIndex(defaultIndexName).setId("2").setSource(jsonBuilder() + .startObject() + .field("name", "Document 2") + .field(defaultFieldName, "POINT(-45 -50)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); - public void testShapeFetchingPath() throws Exception { - String indexName = "shapes_index"; - String searchIndex = "search_index"; - createIndex(indexName); - client().admin().indices().prepareCreate(searchIndex).setMapping("location", "type=shape").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(); - String location = "\"location\" : {\"type\":\"polygon\", \"coordinates\":[[[-10,-10],[10,-10],[10,10],[-10,10],[-10,-10]]]}"; + assertSearchResponse(searchResponse); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + assertThat(searchResponse.getHits().getHits().length, equalTo(1)); + assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("1")); - client().prepareIndex(indexName).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).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); - } + // default query, without specifying relation (expect intersects) + searchResponse = client().prepareSearch(defaultIndexName) + .setQuery(new ShapeQueryBuilder(defaultFieldName, geometry)) + .get(); - @Override - protected Collection> getPlugins() { - return pluginList(SpatialPlugin.class, LocalStateCompositeXPackPlugin.class); + assertSearchResponse(searchResponse); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + assertThat(searchResponse.getHits().getHits().length, equalTo(1)); + assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("1")); } - /** - * 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); - } + public void testIndexPointsCircle() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate(defaultIndexName).setMapping(mapping).get(); + ensureGreen(); - /** - * 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).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)); - } + client().prepareIndex(defaultIndexName).setId("1").setSource(jsonBuilder() + .startObject() + .field("name", "Document 1") + .field(defaultFieldName, "POINT(-30 -30)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).setId("2").setSource(jsonBuilder() + .startObject() + .field("name", "Document 2") + .field(defaultFieldName, "POINT(-45 -50)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + CircleBuilder shape = new CircleBuilder().center(new Coordinate(-30, -30)).radius("1"); + 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(); - public void testNullShape() { - // index a null shape - client().prepareIndex(INDEX).setId("aNullshape").setSource("{\"" + FIELD + "\": null}", XContentType.JSON) - .setRefreshPolicy(IMMEDIATE).get(); - client().prepareIndex(IGNORE_MALFORMED_INDEX).setId("aNullshape").setSource("{\"" + FIELD + "\": null}", - XContentType.JSON).setRefreshPolicy(IMMEDIATE).get(); - GetResponse result = client().prepareGet(INDEX, "aNullshape").get(); - assertThat(result.getField(FIELD), nullValue()); + 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 testExistsQuery() { - ExistsQueryBuilder eqb = QueryBuilders.existsQuery(FIELD); - SearchResponse result = client().prepareSearch(INDEX).setQuery(eqb).get(); - assertSearchResponse(result); - assertHitCount(result, numDocs); + public void testIndexPointsPolygon() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate(defaultIndexName).setMapping(mapping).get(); + ensureGreen(); + + client().prepareIndex(defaultIndexName).setId("1").setSource(jsonBuilder() + .startObject() + .field(defaultFieldName, "POINT(-30 -30)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).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 testFieldAlias() { - SearchResponse response = client().prepareSearch(INDEX) - .setQuery(new ShapeQueryBuilder("alias", queryGeometry).relation(ShapeRelation.INTERSECTS)) + public void testIndexPointsMultiPolygon() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate(defaultIndexName).setMapping(mapping).get(); + ensureGreen(); + + client().prepareIndex(defaultIndexName).setId("1").setSource(jsonBuilder() + .startObject() + .field("name", "Document 1") + .field(defaultFieldName, "POINT(-30 -30)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).setId("2").setSource(jsonBuilder() + .startObject() + .field("name", "Document 2") + .field(defaultFieldName, "POINT(-40 -40)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).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(); - assertTrue(response.getHits().getTotalHits().value > 0); + + 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 testContainsShapeQuery() { + public void testIndexPointsRectangle() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate(defaultIndexName).setMapping(mapping).get(); + ensureGreen(); - client().admin().indices().prepareCreate("test_contains").setMapping("location", "type=shape") - .execute().actionGet(); + client().prepareIndex(defaultIndexName).setId("1").setSource(jsonBuilder() + .startObject() + .field("name", "Document 1") + .field(defaultFieldName, "POINT(-30 -30)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).setId("2").setSource(jsonBuilder() + .startObject() + .field("name", "Document 2") + .field(defaultFieldName, "POINT(-45 -50)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); - String doc = "{\"location\" : {\"type\":\"envelope\", \"coordinates\":[ [-100.0, 100.0], [100.0, -100.0]]}}"; - client().prepareIndex("test_contains").setId("1").setSource(doc, XContentType.JSON).setRefreshPolicy(IMMEDIATE).get(); + Rectangle rectangle = new Rectangle(-50, -40, -45, -55); - // 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); + SearchResponse searchResponse = client().prepareSearch(defaultIndexName) + .setQuery(new ShapeQueryBuilder(defaultFieldName, rectangle) + .relation(ShapeRelation.INTERSECTS)) + .get(); - assertThat(response.getHits().getTotalHits().value, equalTo(1L)); + 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 testGeometryCollectionRelations() throws IOException { - XContentBuilder mapping = XContentFactory.jsonBuilder().startObject() - .startObject("_doc") - .startObject("properties") - .startObject("geometry").field("type", "shape").endObject() + public void testIndexPointsIndexedRectangle() throws Exception { + String mapping = Strings.toString(createDefaultMapping()); + client().admin().indices().prepareCreate(defaultIndexName).setMapping(mapping).get(); + ensureGreen(); + + client().prepareIndex(defaultIndexName).setId("point1").setSource(jsonBuilder() + .startObject() + .field(defaultFieldName, "POINT(-30 -30)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(defaultIndexName).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).setMapping(queryShapesMapping).get(); + ensureGreen(); - createIndex("test_collections", Settings.builder().put("index.number_of_shards", 1).build(), mapping); + client().prepareIndex(indexedShapeIndex).setId("shape1").setSource(jsonBuilder() + .startObject() + .field(indexedShapePath, "BBOX(-50, -40, -45, -55)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + client().prepareIndex(indexedShapeIndex).setId("shape2").setSource(jsonBuilder() + .startObject() + .field(indexedShapePath, "BBOX(-60, -50, -50, -60)") + .endObject()).setRefreshPolicy(IMMEDIATE).get(); + + SearchResponse searchResponse = client().prepareSearch(defaultIndexName) + .setQuery(new ShapeQueryBuilder(defaultFieldName, "shape1") + .relation(ShapeRelation.INTERSECTS) + .indexedShapeIndex(indexedShapeIndex) + .indexedShapePath(indexedShapePath)) + .get(); - EnvelopeBuilder envelopeBuilder = new EnvelopeBuilder(new Coordinate(-10, 10), new Coordinate(10, -10)); + assertSearchResponse(searchResponse); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + assertThat(searchResponse.getHits().getHits().length, equalTo(1)); + assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("point2")); - 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); - } + 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 { From bfb8d4a06c4f367e844eade6b9132a3aa7a8e6c2 Mon Sep 17 00:00:00 2001 From: iverase Date: Thu, 19 Mar 2020 15:56:56 +0100 Subject: [PATCH 02/11] remove spaces --- .../index/mapper/AbstractGeometryFieldMapper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/AbstractGeometryFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/AbstractGeometryFieldMapper.java index 5c522d9bb890e..d065e8121dc05 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/AbstractGeometryFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/AbstractGeometryFieldMapper.java @@ -89,7 +89,7 @@ public interface Parser { } public abstract static class Builder - extends FieldMapper.Builder { + extends FieldMapper.Builder { protected Boolean coerce; protected Boolean ignoreMalformed; protected Boolean ignoreZValue; @@ -187,7 +187,7 @@ protected void setupFieldType(BuilderContext context) { public static class TypeParser implements Mapper.TypeParser { protected boolean parseXContentParameters(String name, Map.Entry entry, Map params) - throws MapperParsingException { + throws MapperParsingException { if (DeprecatedParameters.parse(name, entry.getKey(), entry.getValue(), (DeprecatedParameters)params.get(DEPRECATED_PARAMETERS_KEY))) { return true; From be7c682200e4b773f2bfabe82769579d1718dd85 Mon Sep 17 00:00:00 2001 From: iverase Date: Thu, 19 Mar 2020 16:11:30 +0100 Subject: [PATCH 03/11] license headers --- .../spatial/index/mapper/CartesianPoint.java | 19 +++---------------- .../index/mapper/PointFieldMapper.java | 19 +++---------------- .../index/mapper/PointFieldMapperTests.java | 19 +++---------------- .../index/mapper/PointFieldTypeTests.java | 19 +++---------------- .../search/ShapeQueryOverPointTests.java | 5 +++++ 5 files changed, 17 insertions(+), 64 deletions(-) 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 index fb7d57ad9a969..23ef296ba87a1 100644 --- 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 @@ -1,20 +1,7 @@ /* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * 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; 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 index dcce52f036239..4621b9631a0d5 100644 --- 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 @@ -1,20 +1,7 @@ /* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * 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; 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 index a38c42bbf0ea0..6a1bb81488350 100644 --- 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 @@ -1,20 +1,7 @@ /* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * 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; 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 index 5398f5063dd34..45956d5ddb271 100644 --- 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 @@ -1,20 +1,7 @@ /* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * 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; 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 index 7efb3d514e31a..a417106239fca 100644 --- 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 @@ -1,3 +1,8 @@ +/* + * 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; From d4d2e0a5a386311fcad8c65016b799f499da343a Mon Sep 17 00:00:00 2001 From: iverase Date: Thu, 19 Mar 2020 16:15:23 +0100 Subject: [PATCH 04/11] typo --- docs/reference/query-dsl/shape-queries.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/query-dsl/shape-queries.asciidoc b/docs/reference/query-dsl/shape-queries.asciidoc index 13f60fc4b378c..2e44069c06661 100644 --- a/docs/reference/query-dsl/shape-queries.asciidoc +++ b/docs/reference/query-dsl/shape-queries.asciidoc @@ -10,7 +10,7 @@ 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. +<> fields, which support points, lines, circles, polygons, multi-polygons, etc. The queries in this group are: From fded7b6ebfa864efc2a20c447214ccf2fcd1dfc6 Mon Sep 17 00:00:00 2001 From: iverase Date: Wed, 25 Mar 2020 11:38:16 +0100 Subject: [PATCH 05/11] address review comments --- .../xpack/spatial/index/mapper/PointFieldMapper.java | 7 +------ .../spatial/index/query/ShapeQueryPointProcessor.java | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) 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 index 4621b9631a0d5..a540200a554dc 100644 --- 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 @@ -207,11 +207,6 @@ public MappedFieldType clone() { return new PointFieldType(this); } - @Override - public ValuesSourceType getValuesSourceType() { - return CoreValuesSourceType.GEOPOINT; - } - @Override public Query existsQuery(QueryShardContext context) { if (hasDocValues()) { @@ -296,7 +291,7 @@ public void parse(ParseContext context) throws IOException { } /** - * Parses point represented as an object or an array, ignores malformed geopoints if needed + * Parses point represented as an object or an array, ignores malformed points if needed */ private void parsePointIgnoringMalformed(ParseContext context, CartesianPoint sparse) throws IOException { try { diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryPointProcessor.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryPointProcessor.java index 3cca94b3a3fd4..b4b774a86dc77 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryPointProcessor.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryPointProcessor.java @@ -39,12 +39,12 @@ public class ShapeQueryPointProcessor implements AbstractSearchableGeometryField @Override public Query process(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context) { validateIsPointFieldType(fieldName, context); - // geo points only support intersects + // only the intersects relation is supported for indexed cartesian point types if (relation != ShapeRelation.INTERSECTS) { throw new QueryShardException(context, relation+ " query relation not supported for Field [" + fieldName + "]."); } - // wrap geoQuery as a ConstantScoreQuery + // wrap XYPoint query as a ConstantScoreQuery return getVectorQueryFromShape(shape, fieldName, relation, context); } From e4d8113a1d4de3c10f21b0f5edd72363739acd08 Mon Sep 17 00:00:00 2001 From: iverase Date: Wed, 25 Mar 2020 11:38:38 +0100 Subject: [PATCH 06/11] unused imports --- .../xpack/spatial/index/mapper/PointFieldMapper.java | 2 -- 1 file changed, 2 deletions(-) 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 index a540200a554dc..4283fab92b6cf 100644 --- 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 @@ -30,8 +30,6 @@ import org.elasticsearch.index.mapper.ParseContext; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.index.query.QueryShardException; -import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; -import org.elasticsearch.search.aggregations.support.ValuesSourceType; import org.elasticsearch.xpack.spatial.index.query.ShapeQueryPointProcessor; import java.io.IOException; From e09634c7f16b5e2c77bca7ab39aec02f2869d9ad Mon Sep 17 00:00:00 2001 From: iverase Date: Mon, 30 Mar 2020 10:06:37 +0200 Subject: [PATCH 07/11] Added static ParseField --- .../spatial/index/mapper/CartesianPoint.java | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) 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 index 23ef296ba87a1..99b6e65d55b1b 100644 --- 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 @@ -7,6 +7,7 @@ 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; @@ -28,6 +29,9 @@ */ 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 float x; private float y; @@ -58,27 +62,31 @@ public CartesianPoint resetFromCoordinates(String value) { String[] vals = value.split(","); if (vals.length != 2) { throw new ElasticsearchParseException("failed to parse [{}], expected 2 coordinates " - + "but found: [{}]", vals.length); + + "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 x value [{}]; " + - "must be between -3.4028234663852886E38 and 3.4028234663852886E38", x); + throw new ElasticsearchParseException("invalid [{}] value [{}]; " + + "must be between -3.4028234663852886E38 and 3.4028234663852886E38", + X_FIELD.getPreferredName(), + x); } } catch (NumberFormatException ex) { - throw new ElasticsearchParseException("x must be a number"); + 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 y value [{}]; " + - "must be between -3.4028234663852886E38 and 3.4028234663852886E38", y); + throw new ElasticsearchParseException("invalid [{}] value [{}]; " + + "must be between -3.4028234663852886E38 and 3.4028234663852886E38", + Y_FIELD.getPreferredName(), + y); } } catch (NumberFormatException ex) { - throw new ElasticsearchParseException("y must be a number"); + throw new ElasticsearchParseException("[{}]] must be a number", Y_FIELD.getPreferredName()); } return reset(x, y); } @@ -92,8 +100,8 @@ private CartesianPoint resetFromWKT(String value) { throw new ElasticsearchParseException("Invalid WKT format", e); } if (geometry.type() != ShapeType.POINT) { - throw new ElasticsearchParseException("[geo_point] supports only POINT among WKT primitives, " + - "but found " + geometry.type()); + 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()); @@ -138,7 +146,7 @@ public String toString() { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - return builder.startObject().field("x", x).field("y", y).endObject(); + return builder.startObject().field(X_FIELD.getPreferredName(), x).field(Y_FIELD.getPreferredName(), y).endObject(); } public static CartesianPoint parsePoint(XContentParser parser, CartesianPoint point) @@ -152,7 +160,7 @@ public static CartesianPoint parsePoint(XContentParser parser, CartesianPoint po while (subParser.nextToken() != XContentParser.Token.END_OBJECT) { if (subParser.currentToken() == XContentParser.Token.FIELD_NAME) { String field = subParser.currentName(); - if ("x".equals(field)) { + if (field.equals(X_FIELD.getPreferredName())) { subParser.nextToken(); switch (subParser.currentToken()) { case VALUE_NUMBER: @@ -164,9 +172,10 @@ public static CartesianPoint parsePoint(XContentParser parser, CartesianPoint po } break; default: - throw new ElasticsearchParseException("x must be a number"); + throw new ElasticsearchParseException("[{}] must be a number", + X_FIELD.getPreferredName()); } - } else if ("y".equals(field)) { + } else if (field.equals(Y_FIELD.getPreferredName())) { subParser.nextToken(); switch (subParser.currentToken()) { case VALUE_NUMBER: @@ -178,10 +187,13 @@ public static CartesianPoint parsePoint(XContentParser parser, CartesianPoint po } break; default: - throw new ElasticsearchParseException("y must be a number"); + throw new ElasticsearchParseException("[{}] must be a number", + Y_FIELD.getPreferredName()); } } else { - throw new ElasticsearchParseException("field must be either [{}] or [{}]", "x", "y"); + throw new ElasticsearchParseException("field must be either [{}] or [{}]", + X_FIELD.getPreferredName(), + Y_FIELD.getPreferredName()); } } else { throw new ElasticsearchParseException("token [{}] not allowed", subParser.currentToken()); @@ -189,11 +201,13 @@ public static CartesianPoint parsePoint(XContentParser parser, CartesianPoint po } } if (numberFormatException != null) { - throw new ElasticsearchParseException("[{}] and [{}] must be valid float values", numberFormatException, "x", "y"); + 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); + throw new ElasticsearchParseException("field [{}] missing", X_FIELD.getPreferredName()); } else if (Float.isNaN(y)) { - throw new ElasticsearchParseException("field [{}] missing", y); + throw new ElasticsearchParseException("field [{}] missing", Y_FIELD.getPreferredName()); } else { return point.reset(x, y); } @@ -209,7 +223,8 @@ public static CartesianPoint parsePoint(XContentParser parser, CartesianPoint po } else if (element == 2) { y = subParser.floatValue(); } else { - throw new ElasticsearchParseException("[point] field type does not accept > 2 dimensions"); + throw new ElasticsearchParseException("[{}}] field type does not accept > 2 dimensions", + PointFieldMapper.CONTENT_TYPE); } } else { throw new ElasticsearchParseException("numeric value expected"); From 4631f44170c8bba83da2cc69892276a4790a8cdc Mon Sep 17 00:00:00 2001 From: iverase Date: Mon, 30 Mar 2020 10:58:59 +0200 Subject: [PATCH 08/11] add base class for testing cartesian field mappers --- .../mapper/CartesianFieldMapperTests.java | 140 ++++++++++++++++++ .../index/mapper/PointFieldMapperTests.java | 128 +--------------- .../index/mapper/ShapeFieldMapperTests.java | 28 ++-- 3 files changed, 158 insertions(+), 138 deletions(-) create mode 100644 x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianFieldMapperTests.java 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 0000000000000..098d497291486 --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianFieldMapperTests.java @@ -0,0 +1,140 @@ +/* + * 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.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.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) throws Exception; + + + public void testWKT() throws Exception { + String mapping = Strings.toString(createDefaultMapping(FIELD_NAME, randomBoolean())); + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser() + .parse("type", new CompressedXContent(mapping)); + + ParsedDocument doc = defaultMapper.parse(new SourceToParse("test", "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 Exception { + String mapping = Strings.toString(createDefaultMapping("", 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 Exception { + String mapping = Strings.toString(createDefaultMapping(FIELD_NAME, true)); + + DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser() + .parse("type", new CompressedXContent(mapping)); + + assertThat(defaultMapper.parse(new SourceToParse("test", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "1", + BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject().startObject(FIELD_NAME).nullField("x").nullField("y").endObject().endObject() + ), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue()); + } +} 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 index 6a1bb81488350..633a6702f8ccd 100644 --- 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 @@ -13,50 +13,27 @@ 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.Mapper; 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 org.hamcrest.CoreMatchers; -import java.util.Collection; - import static org.elasticsearch.xpack.spatial.index.mapper.PointFieldMapper.Names.NULL_VALUE; -import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -public class PointFieldMapperTests extends ESSingleNodeTestCase { +public class PointFieldMapperTests extends CartesianFieldMapperTests { @Override - protected Collection> getPlugins() { - return pluginList(InternalSettingsPlugin.class, SpatialPlugin.class, LocalStateCompositeXPackPlugin.class); - } - - - public void testWKT() throws Exception { + protected XContentBuilder createDefaultMapping(String fieldName, boolean ignored_malformed) 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", "1", - BytesReference.bytes(XContentFactory.jsonBuilder() - .startObject() - .field("point", "POINT (2000.1 305.6)") - .endObject()), - XContentType.JSON)); - - assertThat(doc.rootDoc().getField("point"), notNullValue()); + .startObject("properties").startObject(fieldName).field("type", "point"); + if (ignored_malformed || randomBoolean()) { + xContentBuilder.field("ignore_malformed", ignored_malformed); + } + return xContentBuilder.endObject().endObject().endObject().endObject(); } public void testValuesStored() throws Exception { @@ -229,19 +206,6 @@ public void testArrayArrayStored() throws Exception { assertThat(doc.rootDoc().getFields("point").length, CoreMatchers.equalTo(4)); } - public void testEmptyName() throws Exception { - // after 5.x - String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") - .startObject("properties").startObject("").field("type", "point").endObject().endObject() - .endObject().endObject()); - - 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 testNullValue() throws Exception { String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") .startObject("properties").startObject("location") @@ -286,82 +250,4 @@ public void testNullValue() throws Exception { // Shouldn't matter if we specify the value explicitly or use null value assertThat(defaultValue, not(equalTo(doc.rootDoc().getField("location").binaryValue()))); } - - public void testInvalidPointValuesIgnored() throws Exception { - String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") - .startObject("properties") - .startObject("location") - .field("type", "point") - .field("ignore_malformed", "true") - .endObject() - .endObject().endObject().endObject()); - - DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser() - .parse("type", new CompressedXContent(mapping)); - - assertThat(defaultMapper.parse(new SourceToParse("test", "1", - BytesReference.bytes(XContentFactory.jsonBuilder() - .startObject().field("location", "1234.333").endObject() - ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); - - assertThat(defaultMapper.parse(new SourceToParse("test", "1", - BytesReference.bytes(XContentFactory.jsonBuilder() - .startObject().field("lat", "-").field("x", 1.3).endObject() - ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); - - assertThat(defaultMapper.parse(new SourceToParse("test", "1", - BytesReference.bytes(XContentFactory.jsonBuilder() - .startObject().field("lat", 1.3).field("y", "-").endObject() - ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); - - assertThat(defaultMapper.parse(new SourceToParse("test", "1", - BytesReference.bytes(XContentFactory.jsonBuilder() - .startObject().field("location", "-,1.3").endObject() - ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); - - assertThat(defaultMapper.parse(new SourceToParse("test", "1", - BytesReference.bytes(XContentFactory.jsonBuilder() - .startObject().field("location", "1.3,-").endObject() - ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); - - assertThat(defaultMapper.parse(new SourceToParse("test", "1", - BytesReference.bytes(XContentFactory.jsonBuilder() - .startObject().field("x", "NaN").field("y", "NaN").endObject() - ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); - - assertThat(defaultMapper.parse(new SourceToParse("test", "1", - BytesReference.bytes(XContentFactory.jsonBuilder() - .startObject().field("lat", 12).field("y", "NaN").endObject() - ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); - - assertThat(defaultMapper.parse(new SourceToParse("test", "1", - BytesReference.bytes(XContentFactory.jsonBuilder() - .startObject().field("x", "NaN").field("y", 10).endObject() - ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); - - assertThat(defaultMapper.parse(new SourceToParse("test", "1", - BytesReference.bytes(XContentFactory.jsonBuilder() - .startObject().field("location", "NaN,NaN").endObject() - ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); - - assertThat(defaultMapper.parse(new SourceToParse("test", "1", - BytesReference.bytes(XContentFactory.jsonBuilder() - .startObject().field("location", "10,NaN").endObject() - ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); - - assertThat(defaultMapper.parse(new SourceToParse("test", "1", - BytesReference.bytes(XContentFactory.jsonBuilder() - .startObject().field("location", "NaN,12").endObject() - ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); - - assertThat(defaultMapper.parse(new SourceToParse("test", "1", - BytesReference.bytes(XContentFactory.jsonBuilder() - .startObject().startObject("location").nullField("y").field("x", 1).endObject().endObject() - ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); - - assertThat(defaultMapper.parse(new SourceToParse("test", "1", - BytesReference.bytes(XContentFactory.jsonBuilder() - .startObject().startObject("location").nullField("x").nullField("y").endObject().endObject() - ), XContentType.JSON)).rootDoc().getField("location"), nullValue()); - } } 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 1e97aaa1a9fa1..dbe3fe56b8d97 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.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,26 @@ 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) throws Exception { + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject(fieldName).field("type", "shape"); + if (ignored_malformed || randomBoolean()) { + xContentBuilder.field("ignore_malformed", ignored_malformed); + } + 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,20 +257,6 @@ public void testShapeMapperMerge() throws Exception { assertThat(shapeFieldMapper.fieldType().orientation(), equalTo(ShapeBuilder.Orientation.CW)); } - public void testEmptyName() throws Exception { - 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(); { From 0d4987a70a7f82e3bb0d99ec7707f1b851dd3d6c Mon Sep 17 00:00:00 2001 From: iverase Date: Mon, 30 Mar 2020 12:36:59 +0200 Subject: [PATCH 09/11] add support for Z value --- docs/reference/mapping/types/point.asciidoc | 7 +++ .../spatial/index/mapper/CartesianPoint.java | 63 +++++++++++++++---- .../index/mapper/PointFieldMapper.java | 60 ++++++++++++++---- .../mapper/CartesianFieldMapperTests.java | 49 ++++++++++++--- .../index/mapper/PointFieldMapperTests.java | 50 ++++++++++++++- .../index/mapper/ShapeFieldMapperTests.java | 7 ++- 6 files changed, 200 insertions(+), 36 deletions(-) diff --git a/docs/reference/mapping/types/point.asciidoc b/docs/reference/mapping/types/point.asciidoc index 721a83a3360ef..1a78a121b41b3 100644 --- a/docs/reference/mapping/types/point.asciidoc +++ b/docs/reference/mapping/types/point.asciidoc @@ -80,6 +80,13 @@ The following parameters are accepted by `point` fields: 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. 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 index 99b6e65d55b1b..e49803bfc1fbb 100644 --- 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 @@ -24,6 +24,8 @@ 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. */ @@ -31,6 +33,7 @@ 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; @@ -49,19 +52,19 @@ public CartesianPoint reset(float x, float y) { return this; } - public CartesianPoint resetFromString(String value) { + public CartesianPoint resetFromString(String value, final boolean ignoreZValue) { if (value.toLowerCase(Locale.ROOT).contains("point")) { - return resetFromWKT(value); + return resetFromWKT(value, ignoreZValue); } else { - return resetFromCoordinates(value); + return resetFromCoordinates(value, ignoreZValue); } } - public CartesianPoint resetFromCoordinates(String value) { + public CartesianPoint resetFromCoordinates(String value, final boolean ignoreZValue) { String[] vals = value.split(","); - if (vals.length != 2) { - throw new ElasticsearchParseException("failed to parse [{}], expected 2 coordinates " + 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; @@ -88,13 +91,20 @@ public CartesianPoint resetFromCoordinates(String value) { } 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) { + private CartesianPoint resetFromWKT(String value, boolean ignoreZValue) { Geometry geometry; try { - geometry = new WellKnownText(false, new StandardValidator(true)) + geometry = new WellKnownText(false, new StandardValidator(ignoreZValue)) .fromWKT(value); } catch (Exception e) { throw new ElasticsearchParseException("Invalid WKT format", e); @@ -149,7 +159,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder.startObject().field(X_FIELD.getPreferredName(), x).field(Y_FIELD.getPreferredName(), y).endObject(); } - public static CartesianPoint parsePoint(XContentParser parser, CartesianPoint point) + public static CartesianPoint parsePoint(XContentParser parser, CartesianPoint point, boolean ignoreZvalue) throws IOException, ElasticsearchParseException { float x = Float.NaN; float y = Float.NaN; @@ -190,6 +200,21 @@ public static CartesianPoint parsePoint(XContentParser parser, CartesianPoint po 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(), @@ -234,21 +259,35 @@ public static CartesianPoint parsePoint(XContentParser parser, CartesianPoint po return point.reset(x, y); } else if(parser.currentToken() == XContentParser.Token.VALUE_STRING) { String val = parser.text(); - return point.resetFromString(val); + return point.resetFromString(val, ignoreZvalue); } else { throw new ElasticsearchParseException("point expected"); } } - public static CartesianPoint parsePoint(Object value) throws ElasticsearchParseException { + 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()); + 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 index 4283fab92b6cf..08ccea2856b6d 100644 --- 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 @@ -16,6 +16,7 @@ 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; @@ -24,6 +25,7 @@ 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; @@ -50,13 +52,15 @@ public class PointFieldMapper extends FieldMapper implements ArrayValueMapperPar public static final String CONTENT_TYPE = "point"; public static class Names { - public static final String IGNORE_MALFORMED = "ignore_malformed"; - public static final String NULL_VALUE = "null_value"; + 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); @@ -68,6 +72,7 @@ public static class Defaults { 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); @@ -89,13 +94,24 @@ protected Explicit ignoreMalformed(BuilderContext context) { 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, copyTo); + ignoreMalformed, ignoreZValue(context), copyTo); } @Override @@ -130,10 +146,14 @@ public Mapper.Builder parse(String name, Map node, ParserContext String propName = entry.getKey(); Object propNode = entry.getValue(); - if (propName.equals(Names.IGNORE_MALFORMED)) { + if (propName.equals(Names.IGNORE_MALFORMED.getPreferredName())) { builder.ignoreMalformed(XContentMapValues.nodeBooleanValue(propNode, name + "." + Names.IGNORE_MALFORMED)); iterator.remove(); - } else if (propName.equals(Names.NULL_VALUE)) { + } 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."); } @@ -144,7 +164,8 @@ public Mapper.Builder parse(String name, Map node, ParserContext if (nullValue != null) { boolean ignoreMalformed = builder.ignoreMalformed == null ? Defaults.IGNORE_MALFORMED.value() : builder.ignoreMalformed; - CartesianPoint point = CartesianPoint.parsePoint(nullValue); + boolean ignoreZValue = builder.ignoreZValue == null ? GeoPointFieldMapper.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() + "]"); @@ -160,12 +181,14 @@ public Mapper.Builder parse(String name, Map node, ParserContext } protected Explicit ignoreMalformed; + protected Explicit ignoreZValue; public PointFieldMapper(String simpleName, MappedFieldType fieldType, MappedFieldType defaultFieldType, Settings indexSettings, MultiFields multiFields, Explicit ignoreMalformed, - CopyTo copyTo) { + Explicit ignoreZValue, CopyTo copyTo) { super(simpleName, fieldType, defaultFieldType, indexSettings, multiFields, copyTo); this.ignoreMalformed = ignoreMalformed; + this.ignoreZValue = ignoreZValue; } @Override @@ -175,6 +198,9 @@ protected void doMerge(Mapper mergeWith) { if (gpfmMergeWith.ignoreMalformed.explicit()) { this.ignoreMalformed = gpfmMergeWith.ignoreMalformed; } + if (gpfmMergeWith.ignoreZValue.explicit()) { + this.ignoreZValue = gpfmMergeWith.ignoreZValue; + } } @Override @@ -263,8 +289,10 @@ public void parse(ParseContext context) throws IOException { context.parser().nextToken(); float y = context.parser().floatValue(); token = context.parser().nextToken(); - if (token != XContentParser.Token.END_ARRAY) { - throw new ElasticsearchParseException("[{}] field type does not accept > 2 dimensions", CONTENT_TYPE); + 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 { @@ -293,7 +321,7 @@ public void parse(ParseContext context) throws IOException { */ private void parsePointIgnoringMalformed(ParseContext context, CartesianPoint sparse) throws IOException { try { - parse(context, CartesianPoint.parsePoint(context.parser(), sparse)); + parse(context, CartesianPoint.parsePoint(context.parser(), sparse, ignoreZValue().value())); } catch (ElasticsearchParseException e) { if (ignoreMalformed.value() == false) { throw e; @@ -306,11 +334,17 @@ private void parsePointIgnoringMalformed(ParseContext context, CartesianPoint sp 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, ignoreMalformed.value()); + 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, fieldType().nullValue()); + builder.field(Names.NULL_VALUE.getPreferredName(), fieldType().nullValue()); } } + + public Explicit ignoreZValue() { + return ignoreZValue; + } } 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 index 098d497291486..0ce238a20be60 100644 --- 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 @@ -14,6 +14,7 @@ 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; @@ -22,6 +23,7 @@ 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; @@ -38,11 +40,13 @@ protected Collection> getPlugins() { return pluginList(InternalSettingsPlugin.class, SpatialPlugin.class, LocalStateCompositeXPackPlugin.class); } - protected abstract XContentBuilder createDefaultMapping(String fieldName, boolean ignored_malformed) throws Exception; + protected abstract XContentBuilder createDefaultMapping(String fieldName, + boolean ignored_malformed, + boolean ignoreZValue) throws IOException; - public void testWKT() throws Exception { - String mapping = Strings.toString(createDefaultMapping(FIELD_NAME, randomBoolean())); + 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)); @@ -56,8 +60,8 @@ public void testWKT() throws Exception { assertThat(doc.rootDoc().getField(FIELD_NAME), notNullValue()); } - public void testEmptyName() throws Exception { - String mapping = Strings.toString(createDefaultMapping("", randomBoolean())); + public void testEmptyName() throws IOException { + String mapping = Strings.toString(createDefaultMapping("", randomBoolean(), randomBoolean())); DocumentMapperParser parser = createIndex("test").mapperService().documentMapperParser(); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, @@ -66,8 +70,8 @@ public void testEmptyName() throws Exception { assertThat(e.getMessage(), containsString("name cannot be empty string")); } - public void testInvalidPointValuesIgnored() throws Exception { - String mapping = Strings.toString(createDefaultMapping(FIELD_NAME, true)); + 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)); @@ -137,4 +141,35 @@ public void testInvalidPointValuesIgnored() throws Exception { .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", "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", "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 index 633a6702f8ccd..7a09b84c30d0e 100644 --- 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 @@ -18,6 +18,9 @@ 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; @@ -27,11 +30,16 @@ public class PointFieldMapperTests extends CartesianFieldMapperTests { @Override - protected XContentBuilder createDefaultMapping(String fieldName, boolean ignored_malformed) throws Exception { + 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("ignore_malformed", ignored_malformed); + 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(); } @@ -210,7 +218,7 @@ public void testNullValue() throws Exception { String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") .startObject("properties").startObject("location") .field("type", "point") - .field(NULL_VALUE, "1,2") + .field(NULL_VALUE.getPreferredName(), "1,2") .endObject().endObject() .endObject().endObject()); @@ -250,4 +258,40 @@ public void testNullValue() throws Exception { // 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/ShapeFieldMapperTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/ShapeFieldMapperTests.java index dbe3fe56b8d97..3b118160e8c8a 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 @@ -38,12 +38,17 @@ protected Collection> getPlugins() { } @Override - protected XContentBuilder createDefaultMapping(String fieldName, boolean ignored_malformed) throws Exception { + 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(); } From 0a4f4ebfc9ad701a4c105abb4a35dd2273aed755 Mon Sep 17 00:00:00 2001 From: iverase Date: Mon, 30 Mar 2020 12:53:18 +0200 Subject: [PATCH 10/11] checkStyle --- .../xpack/spatial/index/mapper/PointFieldMapper.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 index 08ccea2856b6d..320e0e019cb18 100644 --- 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 @@ -163,8 +163,10 @@ public Mapper.Builder parse(String name, Map node, ParserContext } if (nullValue != null) { - boolean ignoreMalformed = builder.ignoreMalformed == null ? Defaults.IGNORE_MALFORMED.value() : builder.ignoreMalformed; - boolean ignoreZValue = builder.ignoreZValue == null ? GeoPointFieldMapper.Defaults.IGNORE_Z_VALUE.value() : builder.ignoreZValue; + 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) { @@ -242,8 +244,8 @@ public Query existsQuery(QueryShardContext context) { @Override public Query termQuery(Object value, QueryShardContext context) { - throw new QueryShardException(context, "Geo fields do not support exact searching, use dedicated geo queries instead: [" - + name() + "]"); + throw new QueryShardException(context, "Spatial fields do not support exact searching, " + + "use dedicated spatial queries instead: [" + name() + "]"); } } From 2014fde9a50c2e97c4e1551a9f6d8df0d2ca315d Mon Sep 17 00:00:00 2001 From: iverase Date: Tue, 7 Apr 2020 11:36:11 +0200 Subject: [PATCH 11/11] fix merge issues --- .../xpack/spatial/index/query/ShapeQueryBuilderTests.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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 1df6780edc96b..c1a46a0e1ca69 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 @@ -65,12 +65,6 @@ protected Collection> getPlugins() { return Collections.singleton(SpatialPlugin.class); } - @Override - protected void initializeAdditionalMappings(MapperService mapperService) throws IOException { - mapperService.merge(docType, new CompressedXContent(Strings.toString(PutMappingRequest.simpleMapping( - fieldName(), "type=shape"))), MapperService.MergeReason.MAPPING_UPDATE); - } - protected String fieldName() { return SHAPE_FIELD_NAME; } @@ -220,7 +214,7 @@ public void testIgnoreUnmapped() throws IOException { public void testWrongFieldType() { Geometry shape = getGeometry(); - final ShapeQueryBuilder queryBuilder = new ShapeQueryBuilder(STRING_FIELD_NAME, shape); + 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 or point] but of type [text]")); }