浏览代码

Add new point field. (#53804)

This commit adds a new point field that is able to index arbitrary pair of values (x/y)
 in the cartesian space. It only supports filtering using shape queries at the moment.
Ignacio Vera 5 年之前
父节点
当前提交
6182db5b77
共有 17 个文件被更改,包括 2310 次插入332 次删除
  1. 99 0
      docs/reference/mapping/types/point.asciidoc
  2. 10 5
      docs/reference/query-dsl/shape-queries.asciidoc
  3. 2 0
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialPlugin.java
  4. 293 0
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianPoint.java
  5. 352 0
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapper.java
  6. 5 4
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilder.java
  7. 174 0
      x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryPointProcessor.java
  8. 175 0
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianFieldMapperTests.java
  9. 297 0
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapperTests.java
  10. 16 0
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldTypeTests.java
  11. 16 18
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/ShapeFieldMapperTests.java
  12. 49 0
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderOverPointTests.java
  13. 52 0
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderOverShapeTests.java
  14. 14 35
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderTests.java
  15. 159 0
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryOverPointTests.java
  16. 348 0
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryOverShapeTests.java
  17. 249 270
      x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryTests.java

+ 99 - 0
docs/reference/mapping/types/point.asciidoc

@@ -0,0 +1,99 @@
+[[point]]
+[role="xpack"]
+[testenv="basic"]
+=== Point datatype
+++++
+<titleabbrev>Point</titleabbrev>
+++++
+
+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
+<<query-dsl-shape-query,shape Query>>.
+
+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]
+
+<<ignore-malformed,`ignore_malformed`>>::
+
+    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.
+
+<<null-value,`null_value`>>::
+
+    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.

+ 10 - 5
docs/reference/query-dsl/shape-queries.asciidoc

@@ -3,16 +3,21 @@
 [testenv="basic"]
 == Shape queries
 
+
 Like <<geo-shape,`geo_shape`>> 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
-<<shape,`shape`>> 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:
+<<point,`point`>> fields which support x/y pairs, and
+<<shape,`shape`>> fields, which support points, lines, circles, polygons, multi-polygons, etc.
 
 The queries in this group are:
 
 <<query-dsl-shape-query,`shape`>> 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[]

+ 2 - 0
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialPlugin.java

@@ -17,6 +17,7 @@ import org.elasticsearch.plugins.Plugin;
 import org.elasticsearch.plugins.SearchPlugin;
 import org.elasticsearch.xpack.core.action.XPackInfoFeatureAction;
 import org.elasticsearch.xpack.core.action.XPackUsageFeatureAction;
+import org.elasticsearch.xpack.spatial.index.mapper.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 class SpatialPlugin extends Plugin implements ActionPlugin, MapperPlugin,
     public Map<String, Mapper.TypeParser> getMappers() {
         Map<String, Mapper.TypeParser> mappers = new LinkedHashMap<>();
         mappers.put(ShapeFieldMapper.CONTENT_TYPE, new ShapeFieldMapper.TypeParser());
+        mappers.put(PointFieldMapper.CONTENT_TYPE, new PointFieldMapper.TypeParser());
         return Collections.unmodifiableMap(mappers);
     }
 

+ 293 - 0
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianPoint.java

@@ -0,0 +1,293 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.spatial.index.mapper;
+
+import org.elasticsearch.ElasticsearchParseException;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
+import org.elasticsearch.common.xcontent.NamedXContentRegistry;
+import org.elasticsearch.common.xcontent.ToXContentFragment;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentSubParser;
+import org.elasticsearch.common.xcontent.support.MapXContentParser;
+import org.elasticsearch.geometry.Geometry;
+import org.elasticsearch.geometry.ShapeType;
+import org.elasticsearch.geometry.utils.StandardValidator;
+import org.elasticsearch.geometry.utils.WellKnownText;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Locale;
+
+import static org.elasticsearch.index.mapper.GeoPointFieldMapper.Names.IGNORE_Z_VALUE;
+
+/**
+ * Represents a point in the cartesian space.
+ */
+public final class CartesianPoint implements ToXContentFragment {
+
+    private static final ParseField X_FIELD = new ParseField("x");
+    private static final ParseField Y_FIELD = new ParseField("y");
+    private static final ParseField Z_FIELD = new ParseField("z");
+
+    private float x;
+    private float y;
+
+    public CartesianPoint() {
+    }
+
+    public CartesianPoint(float x, float y) {
+        this.x = x;
+        this.y = y;
+    }
+
+    public CartesianPoint reset(float x, float y) {
+        this.x = x;
+        this.y = y;
+        return this;
+    }
+
+    public CartesianPoint resetFromString(String value, final boolean ignoreZValue) {
+        if (value.toLowerCase(Locale.ROOT).contains("point")) {
+            return resetFromWKT(value, ignoreZValue);
+        } else {
+            return resetFromCoordinates(value, ignoreZValue);
+        }
+    }
+
+
+    public CartesianPoint resetFromCoordinates(String value, final boolean ignoreZValue) {
+        String[] vals = value.split(",");
+        if (vals.length > 3 || vals.length < 2) {
+            throw new ElasticsearchParseException("failed to parse [{}], expected 2 or 3 coordinates "
+                + "but found: [{}]", vals, vals.length);
+        }
+        final float x;
+        final float y;
+        try {
+            x = Float.parseFloat(vals[0].trim());
+            if (Float.isFinite(x) == false) {
+                throw new ElasticsearchParseException("invalid [{}] value [{}]; " +
+                    "must be between -3.4028234663852886E38 and 3.4028234663852886E38",
+                    X_FIELD.getPreferredName(),
+                    x);
+            }
+         } catch (NumberFormatException ex) {
+            throw new ElasticsearchParseException("[{}]] must be a number", X_FIELD.getPreferredName());
+        }
+        try {
+            y = Float.parseFloat(vals[1].trim());
+            if (Float.isFinite(y) == false) {
+                throw new ElasticsearchParseException("invalid [{}] value [{}]; " +
+                    "must be between -3.4028234663852886E38 and 3.4028234663852886E38",
+                    Y_FIELD.getPreferredName(),
+                    y);
+            }
+        } catch (NumberFormatException ex) {
+            throw new ElasticsearchParseException("[{}]] must be a number", Y_FIELD.getPreferredName());
+        }
+        if (vals.length > 2) {
+            try {
+                CartesianPoint.assertZValue(ignoreZValue, Float.parseFloat(vals[2].trim()));
+            } catch (NumberFormatException ex) {
+                throw new ElasticsearchParseException("[{}]] must be a number", Y_FIELD.getPreferredName());
+            }
+        }
+        return reset(x, y);
+    }
+
+    private CartesianPoint resetFromWKT(String value, boolean ignoreZValue) {
+        Geometry geometry;
+        try {
+            geometry = new WellKnownText(false, new StandardValidator(ignoreZValue))
+                .fromWKT(value);
+        } catch (Exception e) {
+            throw new ElasticsearchParseException("Invalid WKT format", e);
+        }
+        if (geometry.type() != ShapeType.POINT) {
+            throw new ElasticsearchParseException("[{}] supports only POINT among WKT primitives, " +
+                "but found {}", PointFieldMapper.CONTENT_TYPE, geometry.type());
+        }
+        org.elasticsearch.geometry.Point point = (org.elasticsearch.geometry.Point) geometry;
+        return reset((float) point.getX(), (float) point.getY());
+    }
+
+    public float getX() {
+        return this.x;
+    }
+
+    public float getY() {
+        return this.y;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        CartesianPoint point = (CartesianPoint) o;
+
+        if (Float.compare(point.x, x) != 0) return false;
+        if (Float.compare(point.y, y) != 0) return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result;
+        int temp;
+        temp = x != +0.0f ? Float.floatToIntBits(x) : 0;
+        result = Integer.hashCode(temp);
+        temp = y != +0.0f ? Float.floatToIntBits(y) : 0;
+        result = 31 * result + Integer.hashCode(temp);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return x + ", " + y;
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        return builder.startObject().field(X_FIELD.getPreferredName(), x).field(Y_FIELD.getPreferredName(), y).endObject();
+    }
+
+    public static CartesianPoint parsePoint(XContentParser parser, CartesianPoint point, boolean ignoreZvalue)
+        throws IOException, ElasticsearchParseException {
+        float x = Float.NaN;
+        float y = Float.NaN;
+        NumberFormatException numberFormatException = null;
+
+        if(parser.currentToken() == XContentParser.Token.START_OBJECT) {
+            try (XContentSubParser subParser = new XContentSubParser(parser)) {
+                while (subParser.nextToken() != XContentParser.Token.END_OBJECT) {
+                    if (subParser.currentToken() == XContentParser.Token.FIELD_NAME) {
+                        String field = subParser.currentName();
+                        if (field.equals(X_FIELD.getPreferredName())) {
+                            subParser.nextToken();
+                            switch (subParser.currentToken()) {
+                                case VALUE_NUMBER:
+                                case VALUE_STRING:
+                                    try {
+                                        x = subParser.floatValue(true);
+                                    } catch (NumberFormatException e) {
+                                        numberFormatException = e;
+                                    }
+                                    break;
+                                default:
+                                    throw new ElasticsearchParseException("[{}] must be a number",
+                                        X_FIELD.getPreferredName());
+                            }
+                        } else if (field.equals(Y_FIELD.getPreferredName())) {
+                            subParser.nextToken();
+                            switch (subParser.currentToken()) {
+                                case VALUE_NUMBER:
+                                case VALUE_STRING:
+                                    try {
+                                        y = subParser.floatValue(true);
+                                    } catch (NumberFormatException e) {
+                                        numberFormatException = e;
+                                    }
+                                    break;
+                                default:
+                                    throw new ElasticsearchParseException("[{}] must be a number",
+                                        Y_FIELD.getPreferredName());
+                            }
+                        } else if (field.equals(Z_FIELD.getPreferredName())) {
+                            subParser.nextToken();
+                            switch (subParser.currentToken()) {
+                                case VALUE_NUMBER:
+                                case VALUE_STRING:
+                                    try {
+                                         CartesianPoint.assertZValue(ignoreZvalue, subParser.floatValue(true));
+                                    } catch (NumberFormatException e) {
+                                        numberFormatException = e;
+                                    }
+                                    break;
+                                default:
+                                    throw new ElasticsearchParseException("[{}] must be a number",
+                                        Z_FIELD.getPreferredName());
+                            }
+                        } else {
+                            throw new ElasticsearchParseException("field must be either [{}] or [{}]",
+                                X_FIELD.getPreferredName(),
+                                Y_FIELD.getPreferredName());
+                        }
+                    } else {
+                        throw new ElasticsearchParseException("token [{}] not allowed", subParser.currentToken());
+                    }
+                }
+            }
+           if (numberFormatException != null) {
+                throw new ElasticsearchParseException("[{}] and [{}] must be valid float values", numberFormatException,
+                    X_FIELD.getPreferredName(),
+                    Y_FIELD.getPreferredName());
+            } else if (Float.isNaN(x)) {
+                throw new ElasticsearchParseException("field [{}] missing", X_FIELD.getPreferredName());
+            } else if (Float.isNaN(y)) {
+                throw new ElasticsearchParseException("field [{}] missing", Y_FIELD.getPreferredName());
+            } else {
+                return point.reset(x, y);
+            }
+
+        } else if(parser.currentToken() == XContentParser.Token.START_ARRAY) {
+            try (XContentSubParser subParser = new XContentSubParser(parser)) {
+                int element = 0;
+                while (subParser.nextToken() != XContentParser.Token.END_ARRAY) {
+                    if (subParser.currentToken() == XContentParser.Token.VALUE_NUMBER) {
+                        element++;
+                        if (element == 1) {
+                            x = subParser.floatValue();
+                        } else if (element == 2) {
+                            y = subParser.floatValue();
+                        } else {
+                            throw new ElasticsearchParseException("[{}}] field type does not accept > 2 dimensions",
+                                PointFieldMapper.CONTENT_TYPE);
+                        }
+                    } else {
+                        throw new ElasticsearchParseException("numeric value expected");
+                    }
+                }
+            }
+            return point.reset(x, y);
+        } else if(parser.currentToken() == XContentParser.Token.VALUE_STRING) {
+            String val = parser.text();
+            return point.resetFromString(val, ignoreZvalue);
+        } else {
+            throw new ElasticsearchParseException("point expected");
+        }
+    }
+
+    public static CartesianPoint parsePoint(Object value, boolean ignoreZValue) throws ElasticsearchParseException {
+        try (XContentParser parser = new MapXContentParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE,
+            Collections.singletonMap("null_value", value), null)) {
+            parser.nextToken(); // start object
+            parser.nextToken(); // field name
+            parser.nextToken(); // field value
+            return parsePoint(parser, new CartesianPoint(), ignoreZValue);
+        } catch (IOException ex) {
+            throw new ElasticsearchParseException("error parsing point", ex);
+        }
+    }
+
+    public static double assertZValue(final boolean ignoreZValue, float zValue) {
+        if (ignoreZValue == false) {
+            throw new ElasticsearchParseException("Exception parsing coordinates: found Z value [{}] but [{}] "
+                + "parameter is [{}]", zValue, IGNORE_Z_VALUE, ignoreZValue);
+        }
+        if (Float.isFinite(zValue) == false) {
+            throw new ElasticsearchParseException("invalid [{}] value [{}]; " +
+                "must be between -3.4028234663852886E38 and 3.4028234663852886E38",
+                Z_FIELD.getPreferredName(),
+                zValue);
+        }
+        return zValue;
+    }
+}

+ 352 - 0
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapper.java

@@ -0,0 +1,352 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+package org.elasticsearch.xpack.spatial.index.mapper;
+
+import org.apache.lucene.document.StoredField;
+import org.apache.lucene.document.XYDocValuesField;
+import org.apache.lucene.document.XYPointField;
+import org.apache.lucene.index.IndexOptions;
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.DocValuesFieldExistsQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.elasticsearch.ElasticsearchParseException;
+import org.elasticsearch.common.Explicit;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.support.XContentMapValues;
+import org.elasticsearch.index.mapper.AbstractSearchableGeometryFieldType;
+import org.elasticsearch.index.mapper.ArrayValueMapperParser;
+import org.elasticsearch.index.mapper.FieldMapper;
+import org.elasticsearch.index.mapper.FieldNamesFieldMapper;
+import org.elasticsearch.index.mapper.GeoPointFieldMapper;
+import org.elasticsearch.index.mapper.MappedFieldType;
+import org.elasticsearch.index.mapper.Mapper;
+import org.elasticsearch.index.mapper.MapperParsingException;
+import org.elasticsearch.index.mapper.ParseContext;
+import org.elasticsearch.index.query.QueryShardContext;
+import org.elasticsearch.index.query.QueryShardException;
+import org.elasticsearch.xpack.spatial.index.query.ShapeQueryPointProcessor;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import static org.elasticsearch.index.mapper.TypeParsers.parseField;
+
+
+/**
+ * Field Mapper for point type.
+ *
+ * Uses lucene 8 XYPoint encoding
+ */
+public class PointFieldMapper extends FieldMapper implements ArrayValueMapperParser {
+    public static final String CONTENT_TYPE = "point";
+
+    public static class Names {
+        public static final ParseField IGNORE_MALFORMED = new ParseField("ignore_malformed");
+        public static final ParseField IGNORE_Z_VALUE = new ParseField("ignore_z_value");
+        public static final ParseField NULL_VALUE = new ParseField("null_value");
+    }
+
+    public static class Defaults {
+        public static final Explicit<Boolean> IGNORE_MALFORMED = new Explicit<>(false, false);
+        public static final PointFieldType FIELD_TYPE = new PointFieldType();
+        public static final Explicit<Boolean> IGNORE_Z_VALUE = new Explicit<>(true, false);
+
+        static {
+            FIELD_TYPE.setTokenized(false);
+            FIELD_TYPE.setHasDocValues(true);
+            FIELD_TYPE.setDimensions(2, Integer.BYTES);
+            FIELD_TYPE.freeze();
+        }
+    }
+
+    public static class Builder extends FieldMapper.Builder<Builder, PointFieldMapper> {
+        protected Boolean ignoreMalformed;
+        private Boolean ignoreZValue;
+
+        public Builder(String name) {
+            super(name, Defaults.FIELD_TYPE, Defaults.FIELD_TYPE);
+            builder = this;
+        }
+
+        public Builder ignoreMalformed(boolean ignoreMalformed) {
+            this.ignoreMalformed = ignoreMalformed;
+            return builder;
+        }
+
+        protected Explicit<Boolean> ignoreMalformed(BuilderContext context) {
+            if (ignoreMalformed != null) {
+                return new Explicit<>(ignoreMalformed, true);
+            }
+            if (context.indexSettings() != null) {
+                return new Explicit<>(IGNORE_MALFORMED_SETTING.get(context.indexSettings()), false);
+            }
+            return PointFieldMapper.Defaults.IGNORE_MALFORMED;
+        }
+
+        protected Explicit<Boolean> 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<Boolean> ignoreMalformed,
+                                      CopyTo copyTo) {
+            setupFieldType(context);
+            return new PointFieldMapper(simpleName, fieldType, defaultFieldType, indexSettings, multiFields,
+                ignoreMalformed, ignoreZValue(context), copyTo);
+        }
+
+        @Override
+        public PointFieldType fieldType() {
+            return (PointFieldType)fieldType;
+        }
+
+        @Override
+        public PointFieldMapper build(BuilderContext context) {
+            return build(context, name, fieldType, defaultFieldType, context.indexSettings(),
+                multiFieldsBuilder.build(this, context), ignoreMalformed(context), copyTo);
+        }
+
+        @Override
+        protected void setupFieldType(BuilderContext context) {
+            super.setupFieldType(context);
+
+            fieldType().setGeometryQueryBuilder(new ShapeQueryPointProcessor());
+        }
+    }
+
+    public static class TypeParser implements Mapper.TypeParser {
+        @Override
+        @SuppressWarnings("rawtypes")
+        public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext parserContext)
+                throws MapperParsingException {
+            Builder builder = new PointFieldMapper.Builder(name);
+            parseField(builder, name, node, parserContext);
+            Object nullValue = null;
+            for (Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); iterator.hasNext();) {
+                Map.Entry<String, Object> entry = iterator.next();
+                String propName = entry.getKey();
+                Object propNode = entry.getValue();
+
+                if (propName.equals(Names.IGNORE_MALFORMED.getPreferredName())) {
+                    builder.ignoreMalformed(XContentMapValues.nodeBooleanValue(propNode, name + "." + Names.IGNORE_MALFORMED));
+                    iterator.remove();
+                } else if (propName.equals(PointFieldMapper.Names.IGNORE_Z_VALUE.getPreferredName())) {
+                    builder.ignoreZValue(XContentMapValues.nodeBooleanValue(propNode,
+                        name + "." + PointFieldMapper.Names.IGNORE_Z_VALUE.getPreferredName()));
+                    iterator.remove();
+                } else if (propName.equals(Names.NULL_VALUE.getPreferredName())) {
+                    if (propNode == null) {
+                        throw new MapperParsingException("Property [null_value] cannot be null.");
+                    }
+                    nullValue = propNode;
+                    iterator.remove();
+                }
+            }
+
+            if (nullValue != null) {
+                boolean ignoreMalformed = builder.ignoreMalformed == null ?
+                    Defaults.IGNORE_MALFORMED.value() : builder.ignoreMalformed;
+                boolean ignoreZValue = builder.ignoreZValue == null ?
+                    Defaults.IGNORE_Z_VALUE.value() : builder.ignoreZValue;
+                CartesianPoint point = CartesianPoint.parsePoint(nullValue, ignoreZValue);
+                if (ignoreMalformed == false) {
+                    if (Float.isFinite(point.getX()) == false) {
+                        throw new IllegalArgumentException("illegal x value [" + point.getX() + "]");
+                    }
+                    if (Float.isFinite(point.getY()) == false) {
+                        throw new IllegalArgumentException("illegal y value [" + point.getY() + "]");
+                    }
+                }
+                builder.nullValue(point);
+            }
+            return builder;
+        }
+    }
+
+    protected Explicit<Boolean> ignoreMalformed;
+    protected Explicit<Boolean> ignoreZValue;
+
+    public PointFieldMapper(String simpleName, MappedFieldType fieldType, MappedFieldType defaultFieldType,
+                            Settings indexSettings, MultiFields multiFields, Explicit<Boolean> ignoreMalformed,
+                            Explicit<Boolean> ignoreZValue, CopyTo copyTo) {
+        super(simpleName, fieldType, defaultFieldType, indexSettings, multiFields, copyTo);
+        this.ignoreMalformed = ignoreMalformed;
+        this.ignoreZValue = ignoreZValue;
+    }
+
+    @Override
+    protected void doMerge(Mapper mergeWith) {
+        super.doMerge(mergeWith);
+        PointFieldMapper gpfmMergeWith = (PointFieldMapper) mergeWith;
+        if (gpfmMergeWith.ignoreMalformed.explicit()) {
+            this.ignoreMalformed = gpfmMergeWith.ignoreMalformed;
+        }
+        if (gpfmMergeWith.ignoreZValue.explicit()) {
+            this.ignoreZValue = gpfmMergeWith.ignoreZValue;
+        }
+    }
+
+    @Override
+    protected String contentType() {
+        return CONTENT_TYPE;
+    }
+
+    @Override
+    protected void parseCreateField(ParseContext context, List<IndexableField> fields) throws IOException {
+        throw new UnsupportedOperationException("Parsing is implemented in parse(), this method should NEVER be called");
+    }
+
+    public static class PointFieldType extends AbstractSearchableGeometryFieldType {
+        public PointFieldType() {
+        }
+
+        PointFieldType(PointFieldType ref) {
+            super(ref);
+        }
+
+        @Override
+        public String typeName() {
+            return CONTENT_TYPE;
+        }
+
+        @Override
+        public MappedFieldType clone() {
+            return new PointFieldType(this);
+        }
+
+        @Override
+        public Query existsQuery(QueryShardContext context) {
+            if (hasDocValues()) {
+                return new DocValuesFieldExistsQuery(name());
+            } else {
+                return new TermQuery(new Term(FieldNamesFieldMapper.NAME, name()));
+            }
+        }
+
+        @Override
+        public Query termQuery(Object value, QueryShardContext context) {
+            throw new QueryShardException(context, "Spatial fields do not support exact searching, " +
+                "use dedicated spatial queries instead: [" + name() + "]");
+        }
+    }
+
+    protected void parse(ParseContext context, CartesianPoint point) throws IOException {
+
+        if (fieldType().indexOptions() != IndexOptions.NONE) {
+            context.doc().add(new XYPointField(fieldType().name(), point.getX(), point.getY()));
+        }
+        if (fieldType().stored()) {
+            context.doc().add(new StoredField(fieldType().name(), point.toString()));
+        }
+        if (fieldType.hasDocValues()) {
+            context.doc().add(new XYDocValuesField(fieldType().name(), point.getX(), point.getY()));
+        } else if (fieldType().stored() || fieldType().indexOptions() != IndexOptions.NONE) {
+            List<IndexableField> fields = new ArrayList<>(1);
+            createFieldNamesField(context, fields);
+            for (IndexableField field : fields) {
+                context.doc().add(field);
+            }
+        }
+        // if the mapping contains multi-fields then throw an error?
+        if (multiFields.iterator().hasNext()) {
+            throw new ElasticsearchParseException("[{}] field type does not accept multi-fields", CONTENT_TYPE);
+        }
+    }
+
+    @Override
+    public void parse(ParseContext context) throws IOException {
+        context.path().add(simpleName());
+
+        try {
+            CartesianPoint sparse = context.parseExternalValue(CartesianPoint.class);
+
+            if (sparse != null) {
+                parse(context, sparse);
+            } else {
+                sparse = new CartesianPoint();
+                XContentParser.Token token = context.parser().currentToken();
+                if (token == XContentParser.Token.START_ARRAY) {
+                    token = context.parser().nextToken();
+                    if (token == XContentParser.Token.VALUE_NUMBER) {
+                        float x = context.parser().floatValue();
+                        context.parser().nextToken();
+                        float y = context.parser().floatValue();
+                        token = context.parser().nextToken();
+                        if (token == XContentParser.Token.VALUE_NUMBER) {
+                            CartesianPoint.assertZValue(ignoreZValue.value(), context.parser().floatValue());
+                        } else if (token != XContentParser.Token.END_ARRAY) {
+                            throw new ElasticsearchParseException("[{}] field type does not accept > 3 dimensions", CONTENT_TYPE);
+                        }
+                        parse(context, sparse.reset(x, y));
+                    } else {
+                        while (token != XContentParser.Token.END_ARRAY) {
+                            parsePointIgnoringMalformed(context, sparse);
+                            token = context.parser().nextToken();
+                        }
+                    }
+                } else if (token == XContentParser.Token.VALUE_NULL) {
+                    if (fieldType.nullValue() != null) {
+                        parse(context, (CartesianPoint) fieldType.nullValue());
+                    }
+                } else {
+                    parsePointIgnoringMalformed(context, sparse);
+                }
+            }
+        } catch (Exception ex) {
+            throw new MapperParsingException("failed to parse field [{}] of type [{}]", ex, fieldType().name(), fieldType().typeName());
+        }
+
+        context.path().remove();
+    }
+
+    /**
+     * Parses point represented as an object or an array, ignores malformed points if needed
+     */
+    private void parsePointIgnoringMalformed(ParseContext context, CartesianPoint sparse) throws IOException {
+        try {
+            parse(context, CartesianPoint.parsePoint(context.parser(), sparse, ignoreZValue().value()));
+        } catch (ElasticsearchParseException e) {
+            if (ignoreMalformed.value() == false) {
+                throw e;
+            }
+            context.addIgnoredField(fieldType.name());
+        }
+    }
+
+    @Override
+    protected void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException {
+        super.doXContentBody(builder, includeDefaults, params);
+        if (includeDefaults || ignoreMalformed.explicit()) {
+            builder.field(Names.IGNORE_MALFORMED.getPreferredName(), ignoreMalformed.value());
+        }
+        if (includeDefaults || ignoreZValue.explicit()) {
+            builder.field(GeoPointFieldMapper.Names.IGNORE_Z_VALUE.getPreferredName(), ignoreZValue.value());
+        }
+        if (includeDefaults || fieldType().nullValue() != null) {
+            builder.field(Names.NULL_VALUE.getPreferredName(), fieldType().nullValue());
+        }
+    }
+
+    public Explicit<Boolean> ignoreZValue() {
+        return ignoreZValue;
+    }
+}

+ 5 - 4
x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilder.java

@@ -14,12 +14,13 @@ import org.elasticsearch.common.io.stream.StreamOutput;
 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<ShapeQueryBu
     public static final String NAME = "shape";
 
     protected static final List<String> 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 class ShapeQueryBuilder extends AbstractGeometryQueryBuilder<ShapeQueryBu
                     + "] 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

+ 174 - 0
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);
+        // 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 XYPoint query 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<Query, RuntimeException> {
+        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;
+        }
+    }
+}

+ 175 - 0
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianFieldMapperTests.java

@@ -0,0 +1,175 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.spatial.index.mapper;
+
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.compress.CompressedXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.index.mapper.DocumentMapper;
+import org.elasticsearch.index.mapper.DocumentMapperParser;
+import org.elasticsearch.index.mapper.MapperParsingException;
+import org.elasticsearch.index.mapper.ParsedDocument;
+import org.elasticsearch.index.mapper.SourceToParse;
+import org.elasticsearch.plugins.Plugin;
+import org.elasticsearch.test.ESSingleNodeTestCase;
+import org.elasticsearch.test.InternalSettingsPlugin;
+import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
+import org.elasticsearch.xpack.spatial.SpatialPlugin;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+
+/** Base class for testing cartesian field mappers */
+public abstract class CartesianFieldMapperTests  extends ESSingleNodeTestCase {
+
+    private static final String FIELD_NAME = "location";
+
+    @Override
+    protected Collection<Class<? extends Plugin>> getPlugins() {
+        return pluginList(InternalSettingsPlugin.class, SpatialPlugin.class, LocalStateCompositeXPackPlugin.class);
+    }
+
+    protected abstract XContentBuilder createDefaultMapping(String fieldName,
+                                                            boolean ignored_malformed,
+                                                            boolean ignoreZValue) throws IOException;
+
+
+    public void testWKT() throws IOException {
+        String mapping = Strings.toString(createDefaultMapping(FIELD_NAME, randomBoolean(), randomBoolean()));
+        DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
+            .parse("type", new CompressedXContent(mapping));
+
+        ParsedDocument doc = defaultMapper.parse(new SourceToParse("test", "1",
+            BytesReference.bytes(XContentFactory.jsonBuilder()
+                .startObject()
+                .field(FIELD_NAME, "POINT (2000.1 305.6)")
+                .endObject()),
+            XContentType.JSON));
+
+        assertThat(doc.rootDoc().getField(FIELD_NAME), notNullValue());
+    }
+
+    public void testEmptyName() throws IOException {
+        String mapping = Strings.toString(createDefaultMapping("", randomBoolean(), randomBoolean()));
+
+        DocumentMapperParser parser = createIndex("test").mapperService().documentMapperParser();
+        IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
+            () -> parser.parse("type", new CompressedXContent(mapping))
+        );
+        assertThat(e.getMessage(), containsString("name cannot be empty string"));
+    }
+
+    public void testInvalidPointValuesIgnored() throws IOException {
+        String mapping = Strings.toString(createDefaultMapping(FIELD_NAME, true, randomBoolean()));
+
+        DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
+            .parse("type", new CompressedXContent(mapping));
+
+        assertThat(defaultMapper.parse(new SourceToParse("test", "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());
+    }
+
+    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]"));
+    }
+}

+ 297 - 0
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapperTests.java

@@ -0,0 +1,297 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+package org.elasticsearch.xpack.spatial.index.mapper;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.compress.CompressedXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.index.mapper.DocumentMapper;
+import org.elasticsearch.index.mapper.Mapper;
+import org.elasticsearch.index.mapper.ParsedDocument;
+import org.elasticsearch.index.mapper.SourceToParse;
+import org.hamcrest.CoreMatchers;
+
+import java.io.IOException;
+
+import static org.elasticsearch.index.mapper.GeoPointFieldMapper.Names.IGNORE_Z_VALUE;
+import static org.elasticsearch.xpack.spatial.index.mapper.PointFieldMapper.Names.NULL_VALUE;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.notNullValue;
+
+public class PointFieldMapperTests extends CartesianFieldMapperTests {
+
+    @Override
+    protected XContentBuilder createDefaultMapping(String fieldName,
+                                                   boolean ignored_malformed,
+                                                   boolean ignoreZValue) throws IOException {
+        XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type")
+            .startObject("properties").startObject(fieldName).field("type", "point");
+        if (ignored_malformed || randomBoolean()) {
+            xContentBuilder.field(PointFieldMapper.Names.IGNORE_MALFORMED.getPreferredName(), ignored_malformed);
+        }
+        if (ignoreZValue == false || randomBoolean()) {
+            xContentBuilder.field(PointFieldMapper.Names.IGNORE_Z_VALUE.getPreferredName(), ignoreZValue);
+        }
+        return xContentBuilder.endObject().endObject().endObject().endObject();
+    }
+
+    public void testValuesStored() throws Exception {
+        XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type")
+            .startObject("properties").startObject("point").field("type", "point");
+        String mapping = Strings.toString(xContentBuilder.field("store", true).endObject().endObject().endObject().endObject());
+        DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
+            .parse("type", new CompressedXContent(mapping));
+
+        ParsedDocument doc = defaultMapper.parse(new SourceToParse("test", "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 testNullValue() throws Exception {
+        String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type")
+            .startObject("properties").startObject("location")
+            .field("type", "point")
+            .field(NULL_VALUE.getPreferredName(), "1,2")
+            .endObject().endObject()
+            .endObject().endObject());
+
+        DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
+            .parse("type", new CompressedXContent(mapping));
+        Mapper fieldMapper = defaultMapper.mappers().getMapper("location");
+        assertThat(fieldMapper, instanceOf(PointFieldMapper.class));
+
+        Object nullValue = ((PointFieldMapper) fieldMapper).fieldType().nullValue();
+        assertThat(nullValue, equalTo(new CartesianPoint(1, 2)));
+
+        ParsedDocument doc = defaultMapper.parse(new SourceToParse("test", "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())));
+    }
+
+    /**
+     * 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));
+    }
+}

+ 16 - 0
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldTypeTests.java

@@ -0,0 +1,16 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+package org.elasticsearch.xpack.spatial.index.mapper;
+
+import org.elasticsearch.index.mapper.FieldTypeTestCase;
+import org.elasticsearch.index.mapper.MappedFieldType;
+
+public class PointFieldTypeTests extends FieldTypeTestCase {
+    @Override
+    protected MappedFieldType createDefaultFieldType() {
+        return new PointFieldMapper.PointFieldType();
+    }
+}

+ 16 - 18
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/ShapeFieldMapperTests.java

@@ -18,7 +18,6 @@ import org.elasticsearch.index.mapper.DocumentMapperParser;
 import org.elasticsearch.index.mapper.Mapper;
 import org.elasticsearch.index.mapper.MapperService;
 import org.elasticsearch.plugins.Plugin;
-import org.elasticsearch.test.ESSingleNodeTestCase;
 import org.elasticsearch.test.InternalSettingsPlugin;
 import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
 import org.elasticsearch.xpack.spatial.SpatialPlugin;
@@ -28,17 +27,31 @@ import java.util.Collection;
 import java.util.Collections;
 
 import static org.elasticsearch.index.mapper.GeoPointFieldMapper.Names.IGNORE_Z_VALUE;
-import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.instanceOf;
 
 /** testing for {@link org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper} */
-public class ShapeFieldMapperTests extends ESSingleNodeTestCase {
+public class ShapeFieldMapperTests extends CartesianFieldMapperTests {
     @Override
     protected Collection<Class<? extends Plugin>> getPlugins() {
         return pluginList(InternalSettingsPlugin.class, SpatialPlugin.class, LocalStateCompositeXPackPlugin.class);
     }
 
+    @Override
+    protected XContentBuilder createDefaultMapping(String fieldName,
+                                                   boolean ignored_malformed,
+                                                   boolean ignoreZValue) throws IOException {
+        XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type")
+            .startObject("properties").startObject(fieldName).field("type", "shape");
+        if (ignored_malformed || randomBoolean()) {
+            xContentBuilder.field("ignore_malformed", ignored_malformed);
+        }
+        if (ignoreZValue == false || randomBoolean()) {
+            xContentBuilder.field(PointFieldMapper.Names.IGNORE_Z_VALUE.getPreferredName(), ignoreZValue);
+        }
+        return xContentBuilder.endObject().endObject().endObject().endObject();
+    }
+
     public void testDefaultConfiguration() throws IOException {
         String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1")
             .startObject("properties").startObject("location")
@@ -249,21 +262,6 @@ public class ShapeFieldMapperTests extends ESSingleNodeTestCase {
         assertThat(shapeFieldMapper.fieldType().orientation(), equalTo(ShapeBuilder.Orientation.CW));
     }
 
-    public void testEmptyName() throws Exception {
-        // after 5.x
-        String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1")
-            .startObject("properties").startObject("")
-            .field("type", "shape")
-            .endObject().endObject()
-            .endObject().endObject());
-        DocumentMapperParser parser = createIndex("test").mapperService().documentMapperParser();
-
-        IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
-            () -> parser.parse("type1", new CompressedXContent(mapping))
-        );
-        assertThat(e.getMessage(), containsString("name cannot be empty string"));
-    }
-
     public void testSerializeDefaults() throws Exception {
         DocumentMapperParser parser = createIndex("test").mapperService().documentMapperParser();
         {

+ 49 - 0
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();
+        }
+    }
+}

+ 52 - 0
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);
+    }
+}

+ 14 - 35
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryBuilderTests.java

@@ -10,13 +10,10 @@ import org.apache.lucene.search.ConstantScoreQuery;
 import org.apache.lucene.search.MatchNoDocsQuery;
 import org.apache.lucene.search.Query;
 import org.elasticsearch.ElasticsearchException;
-import org.elasticsearch.Version;
-import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
 import org.elasticsearch.action.get.GetRequest;
 import org.elasticsearch.action.get.GetResponse;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.bytes.BytesArray;
-import org.elasticsearch.common.compress.CompressedXContent;
 import org.elasticsearch.common.geo.GeoJson;
 import org.elasticsearch.common.geo.ShapeRelation;
 import org.elasticsearch.common.io.stream.BytesStreamOutput;
@@ -27,7 +24,6 @@ import org.elasticsearch.common.xcontent.XContentParser;
 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.index.query.Rewriteable;
 import org.elasticsearch.plugins.Plugin;
 import org.elasticsearch.test.AbstractQueryTestCase;
 import org.elasticsearch.xpack.spatial.SpatialPlugin;
-import org.elasticsearch.xpack.spatial.util.ShapeTestUtils;
 import org.junit.After;
 
 import java.io.IOException;
@@ -49,11 +44,11 @@ import static org.hamcrest.CoreMatchers.notNullValue;
 import static org.hamcrest.Matchers.anyOf;
 import static org.hamcrest.Matchers.equalTo;
 
-public class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuilder> {
+public abstract class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuilder> {
 
     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,17 +56,15 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuil
     protected static String indexedShapeRouting;
     protected static Geometry indexedShapeToReturn;
 
+    protected abstract ShapeRelation getShapeRelation(ShapeType type);
+
+    protected abstract Geometry getGeometry();
+
     @Override
     protected Collection<Class<? extends Plugin>> 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 +75,7 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuil
     }
 
     protected ShapeQueryBuilder doCreateTestQueryBuilder(boolean indexedShape) {
-        Geometry shape = ShapeTestUtils.randomGeometry(false);
+        Geometry shape = getGeometry();
 
         ShapeQueryBuilder builder;
         clearShapeFields();
@@ -107,21 +100,7 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuil
         }
 
         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 +128,7 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuil
     }
 
     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 +144,7 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuil
     }
 
     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 +199,7 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuil
     }
 
     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 +209,14 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuil
         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(TEXT_FIELD_NAME, shape);
         QueryShardException e = expectThrows(QueryShardException.class, () -> queryBuilder.toQuery(createShardContext()));
-        assertThat(e.getMessage(), containsString("Field [mapped_string] is not of type [shape] but of type [text]"));
+        assertThat(e.getMessage(), containsString("Field [mapped_string] is not of type [shape or point] but of type [text]"));
     }
 
     public void testSerializationFailsUnlessFetched() throws IOException {

+ 159 - 0
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryOverPointTests.java

@@ -0,0 +1,159 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+package org.elasticsearch.xpack.spatial.search;
+
+import org.elasticsearch.action.search.SearchAction;
+import org.elasticsearch.action.search.SearchPhaseExecutionException;
+import org.elasticsearch.action.search.SearchRequestBuilder;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.geo.ShapeRelation;
+import org.elasticsearch.common.geo.builders.CoordinatesBuilder;
+import org.elasticsearch.common.geo.builders.LineStringBuilder;
+import org.elasticsearch.common.geo.builders.MultiLineStringBuilder;
+import org.elasticsearch.common.geo.builders.MultiPointBuilder;
+import org.elasticsearch.common.geo.builders.PointBuilder;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.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"));
+        }
+    }
+
+}

+ 348 - 0
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<Class<? extends Plugin>> 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);
+        }
+    }
+}

+ 249 - 270
x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/ShapeQueryTests.java

@@ -8,335 +8,314 @@ package org.elasticsearch.xpack.spatial.search;
 import org.elasticsearch.action.get.GetResponse;
 import org.elasticsearch.action.index.IndexRequest;
 import org.elasticsearch.action.search.SearchResponse;
-import org.elasticsearch.common.geo.GeoJson;
+import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.geo.ShapeRelation;
 import org.elasticsearch.common.geo.builders.CircleBuilder;
+import org.elasticsearch.common.geo.builders.CoordinatesBuilder;
 import org.elasticsearch.common.geo.builders.EnvelopeBuilder;
 import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder;
-import org.elasticsearch.common.geo.builders.MultiPointBuilder;
+import org.elasticsearch.common.geo.builders.MultiPolygonBuilder;
 import org.elasticsearch.common.geo.builders.PointBuilder;
-import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.geo.builders.PolygonBuilder;
 import org.elasticsearch.common.unit.DistanceUnit;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentFactory;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.geometry.Geometry;
-import org.elasticsearch.geometry.ShapeType;
-import org.elasticsearch.index.query.ExistsQueryBuilder;
-import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.geometry.Rectangle;
 import org.elasticsearch.plugins.Plugin;
+import org.elasticsearch.search.SearchHits;
 import org.elasticsearch.test.ESSingleNodeTestCase;
 import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
 import org.elasticsearch.xpack.spatial.SpatialPlugin;
 import org.elasticsearch.xpack.spatial.index.query.ShapeQueryBuilder;
-import org.elasticsearch.xpack.spatial.util.ShapeTestUtils;
 import org.locationtech.jts.geom.Coordinate;
 
-import java.io.IOException;
+
 import java.util.Collection;
-import java.util.Locale;
 
 import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE;
 import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
-import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
-import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
-import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse;
-import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.nullValue;
 
-public class ShapeQueryTests extends ESSingleNodeTestCase {
+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<Class<? extends Plugin>> 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<Class<? extends Plugin>> 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 {