diff --git a/pom.xml b/pom.xml
index e53dc82acf6..48380fc5cfd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -152,6 +152,20 @@
+
+ com.spatial4j
+ spatial4j
+ 0.2
+ compile
+
+
+
+ com.vividsolutions
+ jts
+ 1.12
+ compile
+
+
log4j
log4j
diff --git a/src/main/assemblies/common-bin.xml b/src/main/assemblies/common-bin.xml
index 8ed5f3fc222..dd2f2003aee 100644
--- a/src/main/assemblies/common-bin.xml
+++ b/src/main/assemblies/common-bin.xml
@@ -8,6 +8,8 @@
log4j:log4j
net.java.dev.jna:jna
org.xerial.snappy:snappy-java
+ com.spatial4j:spatial4j
+ com.vividsolutions:jts
diff --git a/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeParser.java b/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeParser.java
new file mode 100644
index 00000000000..484a6c76fa2
--- /dev/null
+++ b/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeParser.java
@@ -0,0 +1,186 @@
+package org.elasticsearch.common.geo;
+
+import com.spatial4j.core.shape.Shape;
+import com.spatial4j.core.shape.jts.JtsGeometry;
+import com.spatial4j.core.shape.jts.JtsPoint;
+import com.spatial4j.core.shape.simple.RectangleImpl;
+import com.vividsolutions.jts.geom.*;
+import org.elasticsearch.ElasticSearchParseException;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Parsers which supports reading {@link Shape}s in GeoJSON format from a given
+ * {@link XContentParser}.
+ *
+ * An example of the format used for polygons:
+ *
+ * {
+ * "type": "Polygon",
+ * "coordinates": [
+ * [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],
+ * [100.0, 1.0], [100.0, 0.0] ]
+ * ]
+ * }
+ *
+ * Note, currently MultiPolygon and GeometryCollections are not supported
+ */
+public class GeoJSONShapeParser {
+
+ private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
+
+ private GeoJSONShapeParser() {
+ }
+
+ /**
+ * Parses the current object from the given {@link XContentParser}, creating
+ * the {@link Shape} representation
+ *
+ * @param parser Parser that will be read from
+ * @return Shape representation of the geojson defined Shape
+ * @throws IOException Thrown if an error occurs while reading from the XContentParser
+ */
+ public static Shape parse(XContentParser parser) throws IOException {
+ if (parser.currentToken() != XContentParser.Token.START_OBJECT) {
+ throw new ElasticSearchParseException("Shape must be an object consisting of type and coordinates");
+ }
+
+ String shapeType = null;
+ CoordinateNode node = null;
+
+ XContentParser.Token token;
+ while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+ if (token == XContentParser.Token.FIELD_NAME) {
+ String fieldName = parser.currentName();
+
+ if ("type".equals(fieldName)) {
+ token = parser.nextToken();
+ shapeType = parser.text().toLowerCase(Locale.ENGLISH);
+ if (shapeType == null) {
+ throw new ElasticSearchParseException("Unknown Shape type [" + parser.text() + "]");
+ }
+ } else if ("coordinates".equals(fieldName)) {
+ token = parser.nextToken();
+ node = parseCoordinates(parser);
+ }
+ }
+ }
+
+ if (shapeType == null) {
+ throw new ElasticSearchParseException("Shape type not included");
+ } else if (node == null) {
+ throw new ElasticSearchParseException("Coordinates not included");
+ }
+
+ return buildShape(shapeType, node);
+ }
+
+ /**
+ * Recursive method which parses the arrays of coordinates used to define Shapes
+ *
+ * @param parser Parser that will be read from
+ * @return CoordinateNode representing the start of the coordinate tree
+ * @throws IOException Thrown if an error occurs while reading from the XContentParser
+ */
+ private static CoordinateNode parseCoordinates(XContentParser parser) throws IOException {
+ XContentParser.Token token = parser.nextToken();
+
+ // Base case
+ if (token != XContentParser.Token.START_ARRAY) {
+ double lon = parser.doubleValue();
+ token = parser.nextToken();
+ double lat = parser.doubleValue();
+ token = parser.nextToken();
+ return new CoordinateNode(new Coordinate(lon, lat));
+ }
+
+ List nodes = new ArrayList();
+ while (token != XContentParser.Token.END_ARRAY) {
+ nodes.add(parseCoordinates(parser));
+ token = parser.nextToken();
+ }
+
+ return new CoordinateNode(nodes);
+ }
+
+ /**
+ * Builds the actual {@link Shape} with the given shape type from the tree
+ * of coordinates
+ *
+ * @param shapeType Type of Shape to be built
+ * @param node Root node of the coordinate tree
+ * @return Shape built from the coordinates
+ */
+ private static Shape buildShape(String shapeType, CoordinateNode node) {
+ if ("point".equals(shapeType)) {
+ return new JtsPoint(GEOMETRY_FACTORY.createPoint(node.coordinate));
+ } else if ("linestring".equals(shapeType)) {
+ return new JtsGeometry(GEOMETRY_FACTORY.createLineString(toCoordinates(node)));
+ } else if ("polygon".equals(shapeType)) {
+ LinearRing shell = GEOMETRY_FACTORY.createLinearRing(toCoordinates(node.children.get(0)));
+ LinearRing[] holes = null;
+ if (node.children.size() > 1) {
+ holes = new LinearRing[node.children.size() - 1];
+ for (int i = 0; i < node.children.size() - 1; i++) {
+ holes[i] = GEOMETRY_FACTORY.createLinearRing(toCoordinates(node.children.get(i + 1)));
+ }
+ }
+ return new JtsGeometry(GEOMETRY_FACTORY.createPolygon(shell, holes));
+ } else if ("multipoint".equals(shapeType)) {
+ return new JtsGeometry(GEOMETRY_FACTORY.createMultiPoint(toCoordinates(node)));
+ } else if ("envelope".equals(shapeType)) {
+ Coordinate[] coordinates = toCoordinates(node);
+ return new RectangleImpl(coordinates[0].x, coordinates[1].x, coordinates[1].y, coordinates[0].y);
+ }
+
+ throw new UnsupportedOperationException("ShapeType [" + shapeType + "] not supported");
+ }
+
+ /**
+ * Converts the children of the given CoordinateNode into an array of
+ * {@link Coordinate}.
+ *
+ * @param node CoordinateNode whose children will be converted
+ * @return Coordinate array with the values taken from the children of the Node
+ */
+ private static Coordinate[] toCoordinates(CoordinateNode node) {
+ Coordinate[] coordinates = new Coordinate[node.children.size()];
+ for (int i = 0; i < node.children.size(); i++) {
+ coordinates[i] = node.children.get(i).coordinate;
+ }
+ return coordinates;
+ }
+
+ /**
+ * Node used to represent a tree of coordinates.
+ *
+ * Can either be a leaf node consisting of a Coordinate, or a parent with children
+ */
+ private static class CoordinateNode {
+
+ private Coordinate coordinate;
+ private List children;
+
+ /**
+ * Creates a new leaf CoordinateNode
+ *
+ * @param coordinate Coordinate for the Node
+ */
+ private CoordinateNode(Coordinate coordinate) {
+ this.coordinate = coordinate;
+ }
+
+ /**
+ * Creates a new parent CoordinateNode
+ *
+ * @param children Children of the Node
+ */
+ private CoordinateNode(List children) {
+ this.children = children;
+ }
+ }
+}
diff --git a/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeSerializer.java b/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeSerializer.java
new file mode 100644
index 00000000000..c20ddde60a6
--- /dev/null
+++ b/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeSerializer.java
@@ -0,0 +1,179 @@
+package org.elasticsearch.common.geo;
+
+import com.spatial4j.core.shape.*;
+import com.spatial4j.core.shape.jts.JtsGeometry;
+import com.vividsolutions.jts.geom.*;
+import com.vividsolutions.jts.geom.Point;
+import org.elasticsearch.ElasticSearchIllegalArgumentException;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+
+/**
+ * Serializes {@link Shape} instances into GeoJSON format
+ *
+ * Example of the format used for points:
+ *
+ * { "type": "Point", "coordinates": [100.0, 0.0] }
+ */
+public class GeoJSONShapeSerializer {
+
+ private GeoJSONShapeSerializer() {
+ }
+
+ /**
+ * Serializes the given {@link Shape} as GeoJSON format into the given
+ * {@link XContentBuilder}
+ *
+ * @param shape Shape that will be serialized
+ * @param builder XContentBuilder it will be serialized to
+ * @throws IOException Thrown if an error occurs while writing to the XContentBuilder
+ */
+ public static void serialize(Shape shape, XContentBuilder builder) throws IOException {
+ if (shape instanceof JtsGeometry) {
+ Geometry geometry = ((JtsGeometry) shape).geo;
+ if (geometry instanceof Point) {
+ serializePoint((Point) geometry, builder);
+ } else if (geometry instanceof LineString) {
+ serializeLineString((LineString) geometry, builder);
+ } else if (geometry instanceof Polygon) {
+ serializePolygon((Polygon) geometry, builder);
+ } else if (geometry instanceof MultiPoint) {
+ serializeMultiPoint((MultiPoint) geometry, builder);
+ } else {
+ throw new ElasticSearchIllegalArgumentException("Geometry type [" + geometry.getGeometryType() + "] not supported");
+ }
+ } else if (shape instanceof com.spatial4j.core.shape.Point) {
+ serializePoint((com.spatial4j.core.shape.Point) shape, builder);
+ } else if (shape instanceof Rectangle) {
+ serializeRectangle((Rectangle) shape, builder);
+ } else {
+ throw new ElasticSearchIllegalArgumentException("Shape type [" + shape.getClass().getSimpleName() + "] not supported");
+ }
+ }
+
+ /**
+ * Serializes the given {@link Rectangle}
+ *
+ * @param rectangle Rectangle that will be serialized
+ * @param builder XContentBuilder it will be serialized to
+ * @throws IOException Thrown if an error occurs while writing to the XContentBuilder
+ */
+ private static void serializeRectangle(Rectangle rectangle, XContentBuilder builder) throws IOException {
+ builder.field("type", "Envelope")
+ .startArray("coordinates")
+ .startArray().value(rectangle.getMinX()).value(rectangle.getMaxY()).endArray()
+ .startArray().value(rectangle.getMaxX()).value(rectangle.getMinY()).endArray()
+ .endArray();
+ }
+
+ /**
+ * Serializes the given {@link Point}
+ *
+ * @param point Point that will be serialized
+ * @param builder XContentBuilder it will be serialized to
+ * @throws IOException Thrown if an error occurs while writing to the XContentBuilder
+ */
+ private static void serializePoint(Point point, XContentBuilder builder) throws IOException {
+ builder.field("type", "Point")
+ .startArray("coordinates")
+ .value(point.getX()).value(point.getY())
+ .endArray();
+ }
+
+ /**
+ * Serializes the given {@link com.spatial4j.core.shape.Point}
+ *
+ * @param point Point that will be serialized
+ * @param builder XContentBuilder it will be serialized to
+ * @throws IOException Thrown if an error occurs while writing to the XContentBuilder
+ */
+ private static void serializePoint(com.spatial4j.core.shape.Point point, XContentBuilder builder) throws IOException {
+ builder.field("type", "Point")
+ .startArray("coordinates")
+ .value(point.getX()).value(point.getY())
+ .endArray();
+ }
+
+ /**
+ * Serializes the given {@link LineString}
+ *
+ * @param lineString LineString that will be serialized
+ * @param builder XContentBuilder it will be serialized to
+ * @throws IOException Thrown if an error occurs while writing to the XContentBuilder
+ */
+ private static void serializeLineString(LineString lineString, XContentBuilder builder) throws IOException {
+ builder.field("type", "LineString")
+ .startArray("coordinates");
+
+ for (Coordinate coordinate : lineString.getCoordinates()) {
+ serializeCoordinate(coordinate, builder);
+ }
+
+ builder.endArray();
+ }
+
+ /**
+ * Serializes the given {@link Polygon}
+ *
+ * @param polygon Polygon that will be serialized
+ * @param builder XContentBuilder it will be serialized to
+ * @throws IOException Thrown if an error occurs while writing to the XContentBuilder
+ */
+ private static void serializePolygon(Polygon polygon, XContentBuilder builder) throws IOException {
+ builder.field("type", "Polygon")
+ .startArray("coordinates");
+
+ builder.startArray(); // start outer ring
+
+ for (Coordinate coordinate : polygon.getExteriorRing().getCoordinates()) {
+ serializeCoordinate(coordinate, builder);
+ }
+
+ builder.endArray(); // end outer ring
+
+ for (int i = 0; i < polygon.getNumInteriorRing(); i++) {
+ LineString interiorRing = polygon.getInteriorRingN(i);
+
+ builder.startArray();
+
+ for (Coordinate coordinate : interiorRing.getCoordinates()) {
+ serializeCoordinate(coordinate, builder);
+ }
+
+ builder.endArray();
+ }
+
+
+ builder.endArray();
+ }
+
+ /**
+ * Serializes the given {@link MultiPoint}
+ *
+ * @param multiPoint MulitPoint that will be serialized
+ * @param builder XContentBuilder it will be serialized to
+ * @throws IOException Thrown if an error occurs while writing to the XContentBuilder
+ */
+ private static void serializeMultiPoint(MultiPoint multiPoint, XContentBuilder builder) throws IOException {
+ builder.field("type", "MultiPoint")
+ .startArray("coordinates");
+
+ for (Coordinate coordinate : multiPoint.getCoordinates()) {
+ serializeCoordinate(coordinate, builder);
+ }
+
+ builder.endArray();
+ }
+
+ /**
+ * Serializes the given {@link Coordinate}
+ *
+ * @param coordinate Coordinate that will be serialized
+ * @param builder XContentBuilder it will be serialized to
+ * @throws IOException Thrown if an error occurs while writing to the XContentBuilder
+ */
+ private static void serializeCoordinate(Coordinate coordinate, XContentBuilder builder) throws IOException {
+ builder.startArray().value(coordinate.x).value(coordinate.y).endArray();
+ }
+}
diff --git a/src/main/java/org/elasticsearch/common/geo/ShapeBuilder.java b/src/main/java/org/elasticsearch/common/geo/ShapeBuilder.java
new file mode 100644
index 00000000000..fa573612061
--- /dev/null
+++ b/src/main/java/org/elasticsearch/common/geo/ShapeBuilder.java
@@ -0,0 +1,172 @@
+package org.elasticsearch.common.geo;
+
+import com.spatial4j.core.shape.Point;
+import com.spatial4j.core.shape.Rectangle;
+import com.spatial4j.core.shape.Shape;
+import com.spatial4j.core.shape.jts.JtsGeometry;
+import com.spatial4j.core.shape.jts.JtsPoint;
+import com.spatial4j.core.shape.simple.PointImpl;
+import com.spatial4j.core.shape.simple.RectangleImpl;
+import com.vividsolutions.jts.geom.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class for building {@link Shape} instances like {@link Point},
+ * {@link Rectangle} and Polygons.
+ */
+public class ShapeBuilder {
+
+ private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
+
+ private ShapeBuilder() {
+ }
+
+ /**
+ * Creates a new {@link Point}
+ *
+ * @param lon Longitude of point
+ * @param lat Latitude of point
+ * @return Point with the latitude and longitude
+ */
+ public static Point newPoint(double lon, double lat) {
+ return new PointImpl(lon, lat);
+ }
+
+ /**
+ * Creates a new {@link RectangleBuilder} to build a {@link Rectangle}
+ *
+ * @return RectangleBuilder instance
+ */
+ public static RectangleBuilder newRectangle() {
+ return new RectangleBuilder();
+ }
+
+ /**
+ * Creates a new {@link PolygonBuilder} to build a Polygon
+ *
+ * @return PolygonBuilder instance
+ */
+ public static PolygonBuilder newPolygon() {
+ return new PolygonBuilder();
+ }
+
+ /**
+ * Converts the given Shape into the JTS {@link Geometry} representation.
+ * If the Shape already uses a Geometry, that is returned.
+ *
+ * @param shape Shape to convert
+ * @return Geometry representation of the Shape
+ */
+ public static Geometry toJTSGeometry(Shape shape) {
+ if (shape instanceof JtsGeometry) {
+ return ((JtsGeometry) shape).geo;
+ } else if (shape instanceof JtsPoint) {
+ return ((JtsPoint) shape).getJtsPoint();
+ } else if (shape instanceof Rectangle) {
+ Rectangle rectangle = (Rectangle) shape;
+
+ if (rectangle.getCrossesDateLine()) {
+ throw new IllegalArgumentException("Cannot convert Rectangles that cross the dateline into JTS Geometrys");
+ }
+
+ return newPolygon().point(rectangle.getMinX(), rectangle.getMaxY())
+ .point(rectangle.getMaxX(), rectangle.getMaxY())
+ .point(rectangle.getMaxX(), rectangle.getMinY())
+ .point(rectangle.getMinX(), rectangle.getMinY())
+ .point(rectangle.getMinX(), rectangle.getMaxY()).toPolygon();
+ } else if (shape instanceof Point) {
+ Point point = (Point) shape;
+ return GEOMETRY_FACTORY.createPoint(new Coordinate(point.getX(), point.getY()));
+ }
+
+ throw new IllegalArgumentException("Shape type [" + shape.getClass().getSimpleName() + "] not supported");
+ }
+
+ /**
+ * Builder for creating a {@link Rectangle} instance
+ */
+ public static class RectangleBuilder {
+
+ private Point topLeft;
+ private Point bottomRight;
+
+ /**
+ * Sets the top left point of the Rectangle
+ *
+ * @param lon Longitude of the top left point
+ * @param lat Latitude of the top left point
+ * @return this
+ */
+ public RectangleBuilder topLeft(double lon, double lat) {
+ this.topLeft = new PointImpl(lon, lat);
+ return this;
+ }
+
+ /**
+ * Sets the bottom right point of the Rectangle
+ *
+ * @param lon Longitude of the bottom right point
+ * @param lat Latitude of the bottom right point
+ * @return this
+ */
+ public RectangleBuilder bottomRight(double lon, double lat) {
+ this.bottomRight = new PointImpl(lon, lat);
+ return this;
+ }
+
+ /**
+ * Builds the {@link Rectangle} instance
+ *
+ * @return Built Rectangle
+ */
+ public Rectangle build() {
+ return new RectangleImpl(topLeft.getX(), bottomRight.getX(), bottomRight.getY(), topLeft.getY());
+ }
+ }
+
+ /**
+ * Builder for creating a {@link Shape} instance of a Polygon
+ */
+ public static class PolygonBuilder {
+
+ private final List points = new ArrayList();
+
+ /**
+ * Adds a point to the Polygon
+ *
+ * @param lon Longitude of the point
+ * @param lat Latitude of the point
+ * @return this
+ */
+ public PolygonBuilder point(double lon, double lat) {
+ points.add(new PointImpl(lon, lat));
+ return this;
+ }
+
+ /**
+ * Builds a {@link Shape} instance representing the polygon
+ *
+ * @return Built polygon
+ */
+ public Shape build() {
+ return new JtsGeometry(toPolygon());
+ }
+
+ /**
+ * Creates the raw {@link Polygon}
+ *
+ * @return Built polygon
+ */
+ public Polygon toPolygon() {
+ Coordinate[] coordinates = new Coordinate[points.size()];
+ for (int i = 0; i < points.size(); i++) {
+ coordinates[i] = new Coordinate(points.get(i).getX(), points.get(i).getY());
+ }
+
+ LinearRing ring = GEOMETRY_FACTORY.createLinearRing(coordinates);
+ return GEOMETRY_FACTORY.createPolygon(ring, null);
+ }
+ }
+}
diff --git a/src/main/java/org/elasticsearch/common/geo/ShapeRelation.java b/src/main/java/org/elasticsearch/common/geo/ShapeRelation.java
new file mode 100644
index 00000000000..1aaeb4f1d70
--- /dev/null
+++ b/src/main/java/org/elasticsearch/common/geo/ShapeRelation.java
@@ -0,0 +1,34 @@
+package org.elasticsearch.common.geo;
+
+import java.util.Locale;
+
+/**
+ * Enum representing the relationship between a Query / Filter Shape and indexed Shapes
+ * that will be used to determine if a Document should be matched or not
+ */
+public enum ShapeRelation {
+
+ INTERSECTS("intersects"),
+ DISJOINT("disjoint"),
+ CONTAINS("contains");
+
+ private final String relationName;
+
+ ShapeRelation(String relationName) {
+ this.relationName = relationName;
+ }
+
+ public static ShapeRelation getRelationByName(String name) {
+ name = name.toLowerCase(Locale.ENGLISH);
+ for (ShapeRelation relation : ShapeRelation.values()) {
+ if (relation.relationName.equals(name)) {
+ return relation;
+ }
+ }
+ return null;
+ }
+
+ public String getRelationName() {
+ return relationName;
+ }
+}
diff --git a/src/main/java/org/elasticsearch/common/lucene/spatial/SpatialStrategy.java b/src/main/java/org/elasticsearch/common/lucene/spatial/SpatialStrategy.java
new file mode 100644
index 00000000000..a2c90834ed7
--- /dev/null
+++ b/src/main/java/org/elasticsearch/common/lucene/spatial/SpatialStrategy.java
@@ -0,0 +1,188 @@
+package org.elasticsearch.common.lucene.spatial;
+
+import com.spatial4j.core.shape.Shape;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.Fieldable;
+import org.apache.lucene.search.Filter;
+import org.apache.lucene.search.Query;
+import org.elasticsearch.common.geo.ShapeRelation;
+import org.elasticsearch.common.lucene.spatial.prefix.NodeTokenStream;
+import org.elasticsearch.common.lucene.spatial.prefix.tree.Node;
+import org.elasticsearch.common.lucene.spatial.prefix.tree.SpatialPrefixTree;
+import org.elasticsearch.index.cache.filter.FilterCache;
+import org.elasticsearch.index.mapper.FieldMapper;
+
+import java.util.List;
+
+/**
+ * Abstraction of the logic used to index and filter Shapes.
+ */
+public abstract class SpatialStrategy {
+
+ private final FieldMapper.Names fieldName;
+ private final double distanceErrorPct;
+ private final SpatialPrefixTree prefixTree;
+
+ private ThreadLocal nodeTokenStream = new ThreadLocal() {
+
+ @Override
+ protected NodeTokenStream initialValue() {
+ return new NodeTokenStream();
+ }
+ };
+
+ /**
+ * Creates a new SpatialStrategy that will index and Filter using the
+ * given field
+ *
+ * @param fieldName Name of the field that the Strategy will index in and Filter
+ * @param prefixTree SpatialPrefixTree that will be used to represent Shapes
+ * @param distanceErrorPct Distance Error Percentage used to guide the
+ * SpatialPrefixTree on how precise it should be
+ */
+ protected SpatialStrategy(FieldMapper.Names fieldName, SpatialPrefixTree prefixTree, double distanceErrorPct) {
+ this.fieldName = fieldName;
+ this.prefixTree = prefixTree;
+ this.distanceErrorPct = distanceErrorPct;
+ }
+
+ /**
+ * Converts the given Shape into its indexable format. Implementations
+ * should not store the Shape value as well.
+ *
+ * @param shape Shape to convert ints its indexable format
+ * @return Fieldable for indexing the Shape
+ */
+ public Fieldable createField(Shape shape) {
+ int detailLevel = prefixTree.getMaxLevelForPrecision(shape, distanceErrorPct);
+ List nodes = prefixTree.getNodes(shape, detailLevel, true);
+ NodeTokenStream tokenStream = nodeTokenStream.get();
+ tokenStream.setNodes(nodes);
+ return new Field(fieldName.indexName(), tokenStream);
+ }
+
+ /**
+ * Creates a Filter that will find all indexed Shapes that relate to the
+ * given Shape
+ *
+ * @param shape Shape the indexed shapes will relate to
+ * @param relation Nature of the relation
+ * @return Filter for finding the related shapes
+ */
+ public Filter createFilter(Shape shape, ShapeRelation relation) {
+ switch (relation) {
+ case INTERSECTS:
+ return createIntersectsFilter(shape);
+ case CONTAINS:
+ return createContainsFilter(shape);
+ case DISJOINT:
+ return createDisjointFilter(shape);
+ default:
+ throw new UnsupportedOperationException("Shape Relation [" + relation.getRelationName() + "] not currently supported");
+ }
+ }
+
+ /**
+ * Creates a Query that will find all indexed Shapes that relate to the
+ * given Shape
+ *
+ * @param shape Shape the indexed shapes will relate to
+ * @param relation Nature of the relation
+ * @return Query for finding the related shapes
+ */
+ public Query createQuery(Shape shape, ShapeRelation relation) {
+ switch (relation) {
+ case INTERSECTS:
+ return createIntersectsQuery(shape);
+ case CONTAINS:
+ return createContainsQuery(shape);
+ case DISJOINT:
+ return createDisjointQuery(shape);
+ default:
+ throw new UnsupportedOperationException("Shape Relation [" + relation.getRelationName() + "] not currently supported");
+ }
+ }
+
+ /**
+ * Creates a Filter that will find all indexed Shapes that intersect with
+ * the given Shape
+ *
+ * @param shape Shape to find the intersection Shapes of
+ * @return Filter finding the intersecting indexed Shapes
+ */
+ public abstract Filter createIntersectsFilter(Shape shape);
+
+ /**
+ * Creates a Query that will find all indexed Shapes that intersect with
+ * the given Shape
+ *
+ * @param shape Shape to find the intersection Shapes of
+ * @return Query finding the intersecting indexed Shapes
+ */
+ public abstract Query createIntersectsQuery(Shape shape);
+
+ /**
+ * Creates a Filter that will find all indexed Shapes that are disjoint
+ * to the given Shape
+ *
+ * @param shape Shape to find the disjoint Shapes of
+ * @return Filter for finding the disjoint indexed Shapes
+ */
+ public abstract Filter createDisjointFilter(Shape shape);
+
+ /**
+ * Creates a Query that will find all indexed Shapes that are disjoint
+ * to the given Shape
+ *
+ * @param shape Shape to find the disjoint Shapes of
+ * @return Query for finding the disjoint indexed Shapes
+ */
+ public abstract Query createDisjointQuery(Shape shape);
+
+ /**
+ * Creates a Filter that will find all indexed Shapes that are properly
+ * contained within the given Shape (the indexed Shapes will not have
+ * any area outside of the given Shape).
+ *
+ * @param shape Shape to find the contained Shapes of
+ * @return Filter for finding the contained indexed Shapes
+ */
+ public abstract Filter createContainsFilter(Shape shape);
+
+ /**
+ * Creates a Query that will find all indexed Shapes that are properly
+ * contained within the given Shape (the indexed Shapes will not have
+ * any area outside of the given Shape).
+ *
+ * @param shape Shape to find the contained Shapes of
+ * @return Query for finding the contained indexed Shapes
+ */
+ public abstract Query createContainsQuery(Shape shape);
+
+ /**
+ * Returns the name of the field this Strategy applies to
+ *
+ * @return Name of the field the Strategy applies to
+ */
+ public FieldMapper.Names getFieldName() {
+ return fieldName;
+ }
+
+ /**
+ * Returns the distance error percentage for this Strategy
+ *
+ * @return Distance error percentage for the Strategy
+ */
+ public double getDistanceErrorPct() {
+ return distanceErrorPct;
+ }
+
+ /**
+ * Returns the {@link SpatialPrefixTree} used by this Strategy
+ *
+ * @return SpatialPrefixTree used by the Strategy
+ */
+ public SpatialPrefixTree getPrefixTree() {
+ return prefixTree;
+ }
+}
diff --git a/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/NodeTokenStream.java b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/NodeTokenStream.java
new file mode 100644
index 00000000000..cdc8dba1354
--- /dev/null
+++ b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/NodeTokenStream.java
@@ -0,0 +1,58 @@
+package org.elasticsearch.common.lucene.spatial.prefix;
+
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
+import org.elasticsearch.common.lucene.spatial.prefix.tree.Node;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Custom {@link TokenStream} used to convert a list of {@link Node} representing
+ * a Shape, into indexable terms.
+ */
+public final class NodeTokenStream extends TokenStream {
+
+ private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
+
+ private List nodes;
+ private Iterator iterator;
+ private CharSequence nextTokenStringNeedingLeaf = null;
+
+ @Override
+ public final boolean incrementToken() throws IOException {
+ clearAttributes();
+ if (nextTokenStringNeedingLeaf != null) {
+ termAtt.append(nextTokenStringNeedingLeaf);
+ termAtt.append((char) Node.LEAF_BYTE);
+ nextTokenStringNeedingLeaf = null;
+ return true;
+ }
+ if (iterator.hasNext()) {
+ Node cell = iterator.next();
+ CharSequence token = cell.getTokenString();
+ termAtt.append(token);
+ if (cell.isLeaf()) {
+ nextTokenStringNeedingLeaf = token;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void reset() throws IOException {
+ iterator = nodes.iterator();
+ nextTokenStringNeedingLeaf = null;
+ }
+
+ /**
+ * Sets the Nodes that will be converted into their indexable form
+ *
+ * @param nodes Nodes to be converted
+ */
+ public void setNodes(List nodes) {
+ this.nodes = nodes;
+ }
+}
diff --git a/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/TermQueryPrefixTreeStrategy.java b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/TermQueryPrefixTreeStrategy.java
new file mode 100644
index 00000000000..70878c7ea22
--- /dev/null
+++ b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/TermQueryPrefixTreeStrategy.java
@@ -0,0 +1,149 @@
+package org.elasticsearch.common.lucene.spatial.prefix;
+
+import com.spatial4j.core.shape.Shape;
+import com.spatial4j.core.shape.jts.JtsGeometry;
+import com.vividsolutions.jts.geom.Geometry;
+import com.vividsolutions.jts.operation.buffer.BufferOp;
+import com.vividsolutions.jts.operation.buffer.BufferParameters;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.*;
+import org.elasticsearch.common.lucene.search.OrFilter;
+import org.elasticsearch.common.lucene.search.TermFilter;
+import org.elasticsearch.common.lucene.search.XBooleanFilter;
+import org.elasticsearch.common.lucene.spatial.SpatialStrategy;
+import org.elasticsearch.common.lucene.spatial.prefix.tree.Node;
+import org.elasticsearch.common.lucene.spatial.prefix.tree.SpatialPrefixTree;
+import org.elasticsearch.common.geo.ShapeBuilder;
+import org.elasticsearch.index.cache.filter.FilterCache;
+import org.elasticsearch.index.mapper.FieldMapper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implementation of {@link SpatialStrategy} that uses TermQuerys / TermFilters
+ * to query and filter for Shapes related to other Shapes.
+ */
+public class TermQueryPrefixTreeStrategy extends SpatialStrategy {
+
+ private static final double CONTAINS_BUFFER_DISTANCE = 0.5;
+ private static final BufferParameters BUFFER_PARAMETERS = new BufferParameters(3, BufferParameters.CAP_SQUARE);
+
+ /**
+ * Creates a new TermQueryPrefixTreeStrategy
+ *
+ * @param fieldName Name of the field the Strategy applies to
+ * @param prefixTree SpatialPrefixTree that will be used to represent Shapes
+ * @param distanceErrorPct Distance Error Percentage used to guide the
+ * SpatialPrefixTree on how precise it should be
+ */
+ public TermQueryPrefixTreeStrategy(FieldMapper.Names fieldName, SpatialPrefixTree prefixTree, double distanceErrorPct) {
+ super(fieldName, prefixTree, distanceErrorPct);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Filter createIntersectsFilter(Shape shape) {
+ int detailLevel = getPrefixTree().getMaxLevelForPrecision(shape, getDistanceErrorPct());
+ List nodes = getPrefixTree().getNodes(shape, detailLevel, false);
+
+ Term[] nodeTerms = new Term[nodes.size()];
+ for (int i = 0; i < nodes.size(); i++) {
+ nodeTerms[i] = getFieldName().createIndexNameTerm(nodes.get(i).getTokenString());
+ }
+ return new XTermsFilter(nodeTerms);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Query createIntersectsQuery(Shape shape) {
+ int detailLevel = getPrefixTree().getMaxLevelForPrecision(shape, getDistanceErrorPct());
+ List nodes = getPrefixTree().getNodes(shape, detailLevel, false);
+
+ BooleanQuery query = new BooleanQuery();
+ for (Node node : nodes) {
+ query.add(new TermQuery(getFieldName().createIndexNameTerm(node.getTokenString())),
+ BooleanClause.Occur.SHOULD);
+ }
+
+ return new ConstantScoreQuery(query);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Filter createDisjointFilter(Shape shape) {
+ int detailLevel = getPrefixTree().getMaxLevelForPrecision(shape, getDistanceErrorPct());
+ List nodes = getPrefixTree().getNodes(shape, detailLevel, false);
+
+ XBooleanFilter filter = new XBooleanFilter();
+ for (Node node : nodes) {
+ filter.addNot(new TermFilter(getFieldName().createIndexNameTerm(node.getTokenString())));
+ }
+
+ return filter;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Query createDisjointQuery(Shape shape) {
+ int detailLevel = getPrefixTree().getMaxLevelForPrecision(shape, getDistanceErrorPct());
+ List nodes = getPrefixTree().getNodes(shape, detailLevel, false);
+
+ BooleanQuery query = new BooleanQuery();
+ query.add(new MatchAllDocsQuery(), BooleanClause.Occur.SHOULD);
+ for (Node node : nodes) {
+ query.add(new TermQuery(getFieldName().createIndexNameTerm(node.getTokenString())),
+ BooleanClause.Occur.MUST_NOT);
+ }
+
+ return new ConstantScoreQuery(query);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Filter createContainsFilter(Shape shape) {
+ Filter intersectsFilter = createIntersectsFilter(shape);
+
+ Geometry shapeGeometry = ShapeBuilder.toJTSGeometry(shape);
+ // TODO: Need some way to detect if having the buffer is going to push the shape over the dateline
+ // and throw an error in this instance
+ Geometry buffer = BufferOp.bufferOp(shapeGeometry, CONTAINS_BUFFER_DISTANCE, BUFFER_PARAMETERS);
+ Shape bufferedShape = new JtsGeometry(buffer.difference(shapeGeometry));
+ Filter bufferedFilter = createIntersectsFilter(bufferedShape);
+
+ XBooleanFilter filter = new XBooleanFilter();
+ filter.addShould(intersectsFilter);
+ filter.addNot(bufferedFilter);
+
+ return filter;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Query createContainsQuery(Shape shape) {
+ Query intersectsQuery = createIntersectsQuery(shape);
+
+ Geometry shapeGeometry = ShapeBuilder.toJTSGeometry(shape);
+ Geometry buffer = BufferOp.bufferOp(shapeGeometry, CONTAINS_BUFFER_DISTANCE, BUFFER_PARAMETERS);
+ Shape bufferedShape = new JtsGeometry(buffer.difference(shapeGeometry));
+ Query bufferedQuery = createIntersectsQuery(bufferedShape);
+
+ BooleanQuery query = new BooleanQuery();
+ query.add(intersectsQuery, BooleanClause.Occur.SHOULD);
+ query.add(bufferedQuery, BooleanClause.Occur.MUST_NOT);
+
+ return new ConstantScoreQuery(query);
+ }
+}
diff --git a/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/GeohashPrefixTree.java b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/GeohashPrefixTree.java
new file mode 100644
index 00000000000..3bf0019f6e8
--- /dev/null
+++ b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/GeohashPrefixTree.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.elasticsearch.common.lucene.spatial.prefix.tree;
+
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.shape.Point;
+import com.spatial4j.core.shape.Rectangle;
+import com.spatial4j.core.shape.Shape;
+import com.spatial4j.core.util.GeohashUtils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+
+/**
+ * A SpatialPrefixGrid based on Geohashes. Uses {@link GeohashUtils} to do all the geohash work.
+ */
+public class GeohashPrefixTree extends SpatialPrefixTree {
+
+ public GeohashPrefixTree(SpatialContext ctx, int maxLevels) {
+ super(ctx, maxLevels);
+ Rectangle bounds = ctx.getWorldBounds();
+ if (bounds.getMinX() != -180)
+ throw new IllegalArgumentException("Geohash only supports lat-lon world bounds. Got "+bounds);
+ int MAXP = getMaxLevelsPossible();
+ if (maxLevels <= 0 || maxLevels > MAXP)
+ throw new IllegalArgumentException("maxLen must be [1-"+MAXP+"] but got "+ maxLevels);
+ }
+
+ /** Any more than this and there's no point (double lat & lon are the same). */
+ public static int getMaxLevelsPossible() {
+ return GeohashUtils.MAX_PRECISION;
+ }
+
+ @Override
+ public int getLevelForDistance(double dist) {
+ final int level = GeohashUtils.lookupHashLenForWidthHeight(dist, dist);
+ return Math.max(Math.min(level, maxLevels), 1);
+ }
+
+ @Override
+ public Node getNode(Point p, int level) {
+ return new GhCell(GeohashUtils.encodeLatLon(p.getY(), p.getX(), level));//args are lat,lon (y,x)
+ }
+
+ @Override
+ public Node getNode(String token) {
+ return new GhCell(token);
+ }
+
+ @Override
+ public Node getNode(byte[] bytes, int offset, int len) {
+ return new GhCell(bytes, offset, len);
+ }
+
+ @Override
+ public List getNodes(Shape shape, int detailLevel, boolean inclParents) {
+ return shape instanceof Point ? super.getNodesAltPoint((Point) shape, detailLevel, inclParents) :
+ super.getNodes(shape, detailLevel, inclParents);
+ }
+
+ class GhCell extends Node {
+ GhCell(String token) {
+ super(GeohashPrefixTree.this, token);
+ }
+
+ GhCell(byte[] bytes, int off, int len) {
+ super(GeohashPrefixTree.this, bytes, off, len);
+ }
+
+ @Override
+ public void reset(byte[] bytes, int off, int len) {
+ super.reset(bytes, off, len);
+ shape = null;
+ }
+
+ @Override
+ public Collection getSubCells() {
+ String[] hashes = GeohashUtils.getSubGeohashes(getGeohash());//sorted
+ List cells = new ArrayList(hashes.length);
+ for (String hash : hashes) {
+ cells.add(new GhCell(hash));
+ }
+ return cells;
+ }
+
+ @Override
+ public int getSubCellsSize() {
+ return 32;//8x4
+ }
+
+ @Override
+ public Node getSubCell(Point p) {
+ return GeohashPrefixTree.this.getNode(p,getLevel()+1);//not performant!
+ }
+
+ private Shape shape;//cache
+
+ @Override
+ public Shape getShape() {
+ if (shape == null) {
+ shape = GeohashUtils.decodeBoundary(getGeohash(), ctx);
+ }
+ return shape;
+ }
+
+ @Override
+ public Point getCenter() {
+ return GeohashUtils.decode(getGeohash(), ctx);
+ }
+
+ private String getGeohash() {
+ return getTokenString();
+ }
+
+ }//class GhCell
+
+}
diff --git a/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/Node.java b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/Node.java
new file mode 100644
index 00000000000..c22acf6d5b2
--- /dev/null
+++ b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/Node.java
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.elasticsearch.common.lucene.spatial.prefix.tree;
+
+import com.spatial4j.core.shape.Point;
+import com.spatial4j.core.shape.Shape;
+import com.spatial4j.core.shape.SpatialRelation;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a grid cell. These are not necessarily threadsafe, although new Cell("") (world cell) must be.
+ */
+public abstract class Node implements Comparable {
+ public static final byte LEAF_BYTE = '+';//NOTE: must sort before letters & numbers
+
+ /*
+ Holds a byte[] and/or String representation of the cell. Both are lazy constructed from the other.
+ Neither contains the trailing leaf byte.
+ */
+ private byte[] bytes;
+ private int b_off;
+ private int b_len;
+
+ private String token;//this is the only part of equality
+
+ protected SpatialRelation shapeRel;//set in getSubCells(filter), and via setLeaf().
+ private SpatialPrefixTree spatialPrefixTree;
+
+ protected Node(SpatialPrefixTree spatialPrefixTree, String token) {
+ this.spatialPrefixTree = spatialPrefixTree;
+ this.token = token;
+ if (token.length() > 0 && token.charAt(token.length() - 1) == (char) LEAF_BYTE) {
+ this.token = token.substring(0, token.length() - 1);
+ setLeaf();
+ }
+
+ if (getLevel() == 0)
+ getShape();//ensure any lazy instantiation completes to make this threadsafe
+ }
+
+ protected Node(SpatialPrefixTree spatialPrefixTree, byte[] bytes, int off, int len) {
+ this.spatialPrefixTree = spatialPrefixTree;
+ this.bytes = bytes;
+ this.b_off = off;
+ this.b_len = len;
+ b_fixLeaf();
+ }
+
+ public void reset(byte[] bytes, int off, int len) {
+ assert getLevel() != 0;
+ token = null;
+ shapeRel = null;
+ this.bytes = bytes;
+ this.b_off = off;
+ this.b_len = len;
+ b_fixLeaf();
+ }
+
+ private void b_fixLeaf() {
+ if (bytes[b_off + b_len - 1] == LEAF_BYTE) {
+ b_len--;
+ setLeaf();
+ } else if (getLevel() == spatialPrefixTree.getMaxLevels()) {
+ setLeaf();
+ }
+ }
+
+ public SpatialRelation getShapeRel() {
+ return shapeRel;
+ }
+
+ public boolean isLeaf() {
+ return shapeRel == SpatialRelation.WITHIN;
+ }
+
+ public void setLeaf() {
+ assert getLevel() != 0;
+ shapeRel = SpatialRelation.WITHIN;
+ }
+
+ /**
+ * Note: doesn't contain a trailing leaf byte.
+ */
+ public String getTokenString() {
+ if (token == null) {
+ token = new String(bytes, b_off, b_len, SpatialPrefixTree.UTF8);
+ }
+ return token;
+ }
+
+ /**
+ * Note: doesn't contain a trailing leaf byte.
+ */
+ public byte[] getTokenBytes() {
+ if (bytes != null) {
+ if (b_off != 0 || b_len != bytes.length) {
+ throw new IllegalStateException("Not supported if byte[] needs to be recreated.");
+ }
+ } else {
+ bytes = token.getBytes(SpatialPrefixTree.UTF8);
+ b_off = 0;
+ b_len = bytes.length;
+ }
+ return bytes;
+ }
+
+ public int getLevel() {
+ return token != null ? token.length() : b_len;
+ }
+
+ //TODO add getParent() and update some algorithms to use this?
+ //public Cell getParent();
+
+ /**
+ * Like {@link #getSubCells()} but with the results filtered by a shape. If that shape is a {@link com.spatial4j.core.shape.Point} then it
+ * must call {@link #getSubCell(com.spatial4j.core.shape.Point)};
+ * Precondition: Never called when getLevel() == maxLevel.
+ *
+ * @param shapeFilter an optional filter for the returned cells.
+ * @return A set of cells (no dups), sorted. Not Modifiable.
+ */
+ public Collection getSubCells(Shape shapeFilter) {
+ //Note: Higher-performing subclasses might override to consider the shape filter to generate fewer cells.
+ if (shapeFilter instanceof Point) {
+ return Collections.singleton(getSubCell((Point) shapeFilter));
+ }
+ Collection cells = getSubCells();
+
+ if (shapeFilter == null) {
+ return cells;
+ }
+ List copy = new ArrayList(cells.size());//copy since cells contractually isn't modifiable
+ for (Node cell : cells) {
+ SpatialRelation rel = cell.getShape().relate(shapeFilter, spatialPrefixTree.ctx);
+ if (rel == SpatialRelation.DISJOINT)
+ continue;
+ cell.shapeRel = rel;
+ copy.add(cell);
+ }
+ cells = copy;
+ return cells;
+ }
+
+ /**
+ * Performant implementations are expected to implement this efficiently by considering the current
+ * cell's boundary.
+ * Precondition: Never called when getLevel() == maxLevel.
+ * Precondition: this.getShape().relate(p) != DISJOINT.
+ */
+ public abstract Node getSubCell(Point p);
+
+ //TODO Cell getSubCell(byte b)
+
+ /**
+ * Gets the cells at the next grid cell level that cover this cell.
+ * Precondition: Never called when getLevel() == maxLevel.
+ *
+ * @return A set of cells (no dups), sorted. Not Modifiable.
+ */
+ protected abstract Collection getSubCells();
+
+ /**
+ * {@link #getSubCells()}.size() -- usually a constant. Should be >=2
+ */
+ public abstract int getSubCellsSize();
+
+ public abstract Shape getShape();
+
+ public Point getCenter() {
+ return getShape().getCenter();
+ }
+
+ @Override
+ public int compareTo(Node o) {
+ return getTokenString().compareTo(o.getTokenString());
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return !(obj == null || !(obj instanceof Node)) && getTokenString().equals(((Node) obj).getTokenString());
+ }
+
+ @Override
+ public int hashCode() {
+ return getTokenString().hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return getTokenString() + (isLeaf() ? (char) LEAF_BYTE : "");
+ }
+
+}
diff --git a/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/QuadPrefixTree.java b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/QuadPrefixTree.java
new file mode 100644
index 00000000000..6dc6dcb3367
--- /dev/null
+++ b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/QuadPrefixTree.java
@@ -0,0 +1,284 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.elasticsearch.common.lucene.spatial.prefix.tree;
+
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.shape.Point;
+import com.spatial4j.core.shape.Rectangle;
+import com.spatial4j.core.shape.Shape;
+import com.spatial4j.core.shape.SpatialRelation;
+import com.spatial4j.core.shape.simple.PointImpl;
+
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+
+public class QuadPrefixTree extends SpatialPrefixTree {
+
+ public static final int MAX_LEVELS_POSSIBLE = 50;//not really sure how big this should be
+
+ public static final int DEFAULT_MAX_LEVELS = 12;
+ private final double xmin;
+ private final double xmax;
+ private final double ymin;
+ private final double ymax;
+ private final double xmid;
+ private final double ymid;
+
+ private final double gridW;
+ public final double gridH;
+
+ final double[] levelW;
+ final double[] levelH;
+ final int[] levelS; // side
+ final int[] levelN; // number
+
+ public QuadPrefixTree(
+ SpatialContext ctx, Rectangle bounds, int maxLevels) {
+ super(ctx, maxLevels);
+ this.xmin = bounds.getMinX();
+ this.xmax = bounds.getMaxX();
+ this.ymin = bounds.getMinY();
+ this.ymax = bounds.getMaxY();
+
+ levelW = new double[maxLevels];
+ levelH = new double[maxLevels];
+ levelS = new int[maxLevels];
+ levelN = new int[maxLevels];
+
+ gridW = xmax - xmin;
+ gridH = ymax - ymin;
+ this.xmid = xmin + gridW/2.0;
+ this.ymid = ymin + gridH/2.0;
+ levelW[0] = gridW/2.0;
+ levelH[0] = gridH/2.0;
+ levelS[0] = 2;
+ levelN[0] = 4;
+
+ for (int i = 1; i < levelW.length; i++) {
+ levelW[i] = levelW[i - 1] / 2.0;
+ levelH[i] = levelH[i - 1] / 2.0;
+ levelS[i] = levelS[i - 1] * 2;
+ levelN[i] = levelN[i - 1] * 4;
+ }
+ }
+
+ public QuadPrefixTree(SpatialContext ctx) {
+ this(ctx, DEFAULT_MAX_LEVELS);
+ }
+
+ public QuadPrefixTree(
+ SpatialContext ctx, int maxLevels) {
+ this(ctx, ctx.getWorldBounds(), maxLevels);
+ }
+
+ public void printInfo() {
+ NumberFormat nf = NumberFormat.getNumberInstance();
+ nf.setMaximumFractionDigits(5);
+ nf.setMinimumFractionDigits(5);
+ nf.setMinimumIntegerDigits(3);
+
+ for (int i = 0; i < maxLevels; i++) {
+ System.out.println(i + "]\t" + nf.format(levelW[i]) + "\t" + nf.format(levelH[i]) + "\t" +
+ levelS[i] + "\t" + (levelS[i] * levelS[i]));
+ }
+ }
+
+ @Override
+ public int getLevelForDistance(double dist) {
+ for (int i = 1; i < maxLevels; i++) {
+ //note: level[i] is actually a lookup for level i+1
+ if(dist > levelW[i] || dist > levelH[i]) {
+ return i;
+ }
+ }
+ return maxLevels;
+ }
+
+ @Override
+ public Node getNode(Point p, int level) {
+ List cells = new ArrayList(1);
+ build(xmid, ymid, 0, cells, new StringBuilder(), new PointImpl(p.getX(),p.getY()), level);
+ return cells.get(0);//note cells could be longer if p on edge
+ }
+
+ @Override
+ public Node getNode(String token) {
+ return new QuadCell(token);
+ }
+
+ @Override
+ public Node getNode(byte[] bytes, int offset, int len) {
+ return new QuadCell(bytes, offset, len);
+ }
+
+ @Override //for performance
+ public List getNodes(Shape shape, int detailLevel, boolean inclParents) {
+ if (shape instanceof Point)
+ return super.getNodesAltPoint((Point) shape, detailLevel, inclParents);
+ else
+ return super.getNodes(shape, detailLevel, inclParents);
+ }
+
+ private void build(
+ double x,
+ double y,
+ int level,
+ List matches,
+ StringBuilder str,
+ Shape shape,
+ int maxLevel) {
+ assert str.length() == level;
+ double w = levelW[level] / 2;
+ double h = levelH[level] / 2;
+
+ // Z-Order
+ // http://en.wikipedia.org/wiki/Z-order_%28curve%29
+ checkBattenberg('A', x - w, y + h, level, matches, str, shape, maxLevel);
+ checkBattenberg('B', x + w, y + h, level, matches, str, shape, maxLevel);
+ checkBattenberg('C', x - w, y - h, level, matches, str, shape, maxLevel);
+ checkBattenberg('D', x + w, y - h, level, matches, str, shape, maxLevel);
+
+ // possibly consider hilbert curve
+ // http://en.wikipedia.org/wiki/Hilbert_curve
+ // http://blog.notdot.net/2009/11/Damn-Cool-Algorithms-Spatial-indexing-with-Quadtrees-and-Hilbert-Curves
+ // if we actually use the range property in the query, this could be useful
+ }
+
+ private void checkBattenberg(
+ char c,
+ double cx,
+ double cy,
+ int level,
+ List matches,
+ StringBuilder str,
+ Shape shape,
+ int maxLevel) {
+ assert str.length() == level;
+ double w = levelW[level] / 2;
+ double h = levelH[level] / 2;
+
+ int strlen = str.length();
+ Rectangle rectangle = ctx.makeRect(cx - w, cx + w, cy - h, cy + h);
+ SpatialRelation v = shape.relate(rectangle, ctx);
+ if (SpatialRelation.CONTAINS == v) {
+ str.append(c);
+ //str.append(SpatialPrefixGrid.COVER);
+ matches.add(new QuadCell(str.toString(),v.transpose()));
+ } else if (SpatialRelation.DISJOINT == v) {
+ // nothing
+ } else { // SpatialRelation.WITHIN, SpatialRelation.INTERSECTS
+ str.append(c);
+
+ int nextLevel = level+1;
+ if (nextLevel >= maxLevel) {
+ //str.append(SpatialPrefixGrid.INTERSECTS);
+ matches.add(new QuadCell(str.toString(),v.transpose()));
+ } else {
+ build(cx, cy, nextLevel, matches, str, shape, maxLevel);
+ }
+ }
+ str.setLength(strlen);
+ }
+
+ class QuadCell extends Node {
+
+ public QuadCell(String token) {
+ super(QuadPrefixTree.this, token);
+ }
+
+ public QuadCell(String token, SpatialRelation shapeRel) {
+ super(QuadPrefixTree.this, token);
+ this.shapeRel = shapeRel;
+ }
+
+ QuadCell(byte[] bytes, int off, int len) {
+ super(QuadPrefixTree.this, bytes, off, len);
+ }
+
+ @Override
+ public void reset(byte[] bytes, int off, int len) {
+ super.reset(bytes, off, len);
+ shape = null;
+ }
+
+ @Override
+ public Collection getSubCells() {
+ List cells = new ArrayList(4);
+ cells.add(new QuadCell(getTokenString()+"A"));
+ cells.add(new QuadCell(getTokenString()+"B"));
+ cells.add(new QuadCell(getTokenString()+"C"));
+ cells.add(new QuadCell(getTokenString()+"D"));
+ return cells;
+ }
+
+ @Override
+ public int getSubCellsSize() {
+ return 4;
+ }
+
+ @Override
+ public Node getSubCell(Point p) {
+ return QuadPrefixTree.this.getNode(p,getLevel()+1);//not performant!
+ }
+
+ private Shape shape;//cache
+
+ @Override
+ public Shape getShape() {
+ if (shape == null)
+ shape = makeShape();
+ return shape;
+ }
+
+ private Rectangle makeShape() {
+ String token = getTokenString();
+ double xmin = QuadPrefixTree.this.xmin;
+ double ymin = QuadPrefixTree.this.ymin;
+
+ for (int i = 0; i < token.length(); i++) {
+ char c = token.charAt(i);
+ if ('A' == c || 'a' == c) {
+ ymin += levelH[i];
+ } else if ('B' == c || 'b' == c) {
+ xmin += levelW[i];
+ ymin += levelH[i];
+ } else if ('C' == c || 'c' == c) {
+ // nothing really
+ }
+ else if('D' == c || 'd' == c) {
+ xmin += levelW[i];
+ } else {
+ throw new RuntimeException("unexpected char: " + c);
+ }
+ }
+ int len = token.length();
+ double width, height;
+ if (len > 0) {
+ width = levelW[len-1];
+ height = levelH[len-1];
+ } else {
+ width = gridW;
+ height = gridH;
+ }
+ return ctx.makeRect(xmin, xmin + width, ymin, ymin + height);
+ }
+ }//QuadCell
+}
diff --git a/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/SpatialPrefixTree.java b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/SpatialPrefixTree.java
new file mode 100644
index 00000000000..d72718a1c57
--- /dev/null
+++ b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/SpatialPrefixTree.java
@@ -0,0 +1,246 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.elasticsearch.common.lucene.spatial.prefix.tree;
+
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.shape.Point;
+import com.spatial4j.core.shape.Shape;
+
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A Spatial Prefix Tree, or Trie, which decomposes shapes into prefixed strings at variable lengths corresponding to
+ * variable precision. Each string corresponds to a spatial region.
+ *
+ * Implementations of this class should be thread-safe and immutable once initialized.
+ */
+public abstract class SpatialPrefixTree {
+
+ protected static final Charset UTF8 = Charset.forName("UTF-8");
+
+ protected final int maxLevels;
+
+ protected final SpatialContext ctx;
+
+ public SpatialPrefixTree(SpatialContext ctx, int maxLevels) {
+ assert maxLevels > 0;
+ this.ctx = ctx;
+ this.maxLevels = maxLevels;
+ }
+
+ public SpatialContext getSpatialContext() {
+ return ctx;
+ }
+
+ public int getMaxLevels() {
+ return maxLevels;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "(maxLevels:" + maxLevels + ",ctx:" + ctx + ")";
+ }
+
+ /**
+ * See {@link com.spatial4j.core.query.SpatialArgs#getDistPrecision()}.
+ * A grid level looked up via {@link #getLevelForDistance(double)} is returned.
+ *
+ * @param shape
+ * @param precision 0-0.5
+ * @return 1-maxLevels
+ */
+ public int getMaxLevelForPrecision(Shape shape, double precision) {
+ if (precision < 0 || precision > 0.5) {
+ throw new IllegalArgumentException("Precision " + precision + " must be between [0-0.5]");
+ }
+ if (precision == 0 || shape instanceof Point) {
+ return maxLevels;
+ }
+ double bboxArea = shape.getBoundingBox().getArea();
+ if (bboxArea == 0) {
+ return maxLevels;
+ }
+ double avgSideLenFromCenter = Math.sqrt(bboxArea) / 2;
+ return getLevelForDistance(avgSideLenFromCenter * precision);
+ }
+
+ /**
+ * Returns the level of the smallest grid size with a side length that is greater or equal to the provided
+ * distance.
+ *
+ * @param dist >= 0
+ * @return level [1-maxLevels]
+ */
+ public abstract int getLevelForDistance(double dist);
+
+ //TODO double getDistanceForLevel(int level)
+
+ private transient Node worldNode;//cached
+
+ /**
+ * Returns the level 0 cell which encompasses all spatial data. Equivalent to {@link #getNode(String)} with "".
+ * This cell is threadsafe, just like a spatial prefix grid is, although cells aren't
+ * generally threadsafe.
+ * TODO rename to getTopCell or is this fine?
+ */
+ public Node getWorldNode() {
+ if (worldNode == null) {
+ worldNode = getNode("");
+ }
+ return worldNode;
+ }
+
+ /**
+ * The cell for the specified token. The empty string should be equal to {@link #getWorldNode()}.
+ * Precondition: Never called when token length > maxLevel.
+ */
+ public abstract Node getNode(String token);
+
+ public abstract Node getNode(byte[] bytes, int offset, int len);
+
+ public final Node getNode(byte[] bytes, int offset, int len, Node target) {
+ if (target == null) {
+ return getNode(bytes, offset, len);
+ }
+
+ target.reset(bytes, offset, len);
+ return target;
+ }
+
+ protected Node getNode(Point p, int level) {
+ return getNodes(p, level, false).get(0);
+ }
+
+ /**
+ * Gets the intersecting & including cells for the specified shape, without exceeding detail level.
+ * The result is a set of cells (no dups), sorted. Unmodifiable.
+ *
+ * This implementation checks if shape is a Point and if so uses an implementation that
+ * recursively calls {@link Node#getSubCell(com.spatial4j.core.shape.Point)}. Cell subclasses
+ * ideally implement that method with a quick implementation, otherwise, subclasses should
+ * override this method to invoke {@link #getNodesAltPoint(com.spatial4j.core.shape.Point, int, boolean)}.
+ * TODO consider another approach returning an iterator -- won't build up all cells in memory.
+ */
+ public List getNodes(Shape shape, int detailLevel, boolean inclParents) {
+ if (detailLevel > maxLevels) {
+ throw new IllegalArgumentException("detailLevel > maxLevels");
+ }
+
+ List cells;
+ if (shape instanceof Point) {
+ //optimized point algorithm
+ final int initialCapacity = inclParents ? 1 + detailLevel : 1;
+ cells = new ArrayList(initialCapacity);
+ recursiveGetNodes(getWorldNode(), (Point) shape, detailLevel, true, cells);
+ assert cells.size() == initialCapacity;
+ } else {
+ cells = new ArrayList(inclParents ? 1024 : 512);
+ recursiveGetNodes(getWorldNode(), shape, detailLevel, inclParents, cells);
+ }
+ if (inclParents) {
+ Node c = cells.remove(0);//remove getWorldNode()
+ assert c.getLevel() == 0;
+ }
+ return cells;
+ }
+
+ private void recursiveGetNodes(Node node, Shape shape, int detailLevel, boolean inclParents,
+ Collection result) {
+ if (node.isLeaf()) {//cell is within shape
+ result.add(node);
+ return;
+ }
+ final Collection subCells = node.getSubCells(shape);
+ if (node.getLevel() == detailLevel - 1) {
+ if (subCells.size() < node.getSubCellsSize()) {
+ if (inclParents)
+ result.add(node);
+ for (Node subCell : subCells) {
+ subCell.setLeaf();
+ }
+ result.addAll(subCells);
+ } else {//a bottom level (i.e. detail level) optimization where all boxes intersect, so use parent cell.
+ node.setLeaf();
+ result.add(node);
+ }
+ } else {
+ if (inclParents) {
+ result.add(node);
+ }
+ for (Node subCell : subCells) {
+ recursiveGetNodes(subCell, shape, detailLevel, inclParents, result);//tail call
+ }
+ }
+ }
+
+ private void recursiveGetNodes(Node node, Point point, int detailLevel, boolean inclParents,
+ Collection result) {
+ if (inclParents) {
+ result.add(node);
+ }
+ final Node pCell = node.getSubCell(point);
+ if (node.getLevel() == detailLevel - 1) {
+ pCell.setLeaf();
+ result.add(pCell);
+ } else {
+ recursiveGetNodes(pCell, point, detailLevel, inclParents, result);//tail call
+ }
+ }
+
+ /**
+ * Subclasses might override {@link #getNodes(com.spatial4j.core.shape.Shape, int, boolean)}
+ * and check if the argument is a shape and if so, delegate
+ * to this implementation, which calls {@link #getNode(com.spatial4j.core.shape.Point, int)} and
+ * then calls {@link #getNode(String)} repeatedly if inclParents is true.
+ */
+ protected final List getNodesAltPoint(Point p, int detailLevel, boolean inclParents) {
+ Node cell = getNode(p, detailLevel);
+ if (!inclParents) {
+ return Collections.singletonList(cell);
+ }
+
+ String endToken = cell.getTokenString();
+ assert endToken.length() == detailLevel;
+ List cells = new ArrayList(detailLevel);
+ for (int i = 1; i < detailLevel; i++) {
+ cells.add(getNode(endToken.substring(0, i)));
+ }
+ cells.add(cell);
+ return cells;
+ }
+
+ /**
+ * Will add the trailing leaf byte for leaves. This isn't particularly efficient.
+ */
+ public static List nodesToTokenStrings(Collection nodes) {
+ List tokens = new ArrayList((nodes.size()));
+ for (Node node : nodes) {
+ final String token = node.getTokenString();
+ if (node.isLeaf()) {
+ tokens.add(token + (char) Node.LEAF_BYTE);
+ } else {
+ tokens.add(token);
+ }
+ }
+ return tokens;
+ }
+}
diff --git a/src/main/java/org/elasticsearch/index/mapper/DocumentMapperParser.java b/src/main/java/org/elasticsearch/index/mapper/DocumentMapperParser.java
index 11a998f1d0b..6b303c320f2 100644
--- a/src/main/java/org/elasticsearch/index/mapper/DocumentMapperParser.java
+++ b/src/main/java/org/elasticsearch/index/mapper/DocumentMapperParser.java
@@ -36,6 +36,7 @@ import org.elasticsearch.index.analysis.AnalysisService;
import org.elasticsearch.index.analysis.NamedAnalyzer;
import org.elasticsearch.index.mapper.core.*;
import org.elasticsearch.index.mapper.geo.GeoPointFieldMapper;
+import org.elasticsearch.index.mapper.geo.GeoShapeFieldMapper;
import org.elasticsearch.index.mapper.internal.*;
import org.elasticsearch.index.mapper.ip.IpFieldMapper;
import org.elasticsearch.index.mapper.multifield.MultiFieldMapper;
@@ -85,6 +86,7 @@ public class DocumentMapperParser extends AbstractIndexComponent {
.put(ObjectMapper.NESTED_CONTENT_TYPE, new ObjectMapper.TypeParser())
.put(MultiFieldMapper.CONTENT_TYPE, new MultiFieldMapper.TypeParser())
.put(GeoPointFieldMapper.CONTENT_TYPE, new GeoPointFieldMapper.TypeParser())
+ .put(GeoShapeFieldMapper.CONTENT_TYPE, new GeoShapeFieldMapper.TypeParser())
.immutableMap();
rootTypeParsers = new MapBuilder()
diff --git a/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java b/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java
new file mode 100644
index 00000000000..4d066e64ad2
--- /dev/null
+++ b/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java
@@ -0,0 +1,186 @@
+package org.elasticsearch.index.mapper.geo;
+
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.context.jts.JtsSpatialContext;
+import com.spatial4j.core.distance.DistanceUnits;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.Fieldable;
+import org.elasticsearch.common.lucene.spatial.SpatialStrategy;
+import org.elasticsearch.common.lucene.spatial.prefix.TermQueryPrefixTreeStrategy;
+import org.elasticsearch.common.lucene.spatial.prefix.tree.GeohashPrefixTree;
+import org.elasticsearch.common.lucene.spatial.prefix.tree.QuadPrefixTree;
+import org.elasticsearch.ElasticSearchIllegalArgumentException;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.geo.GeoJSONShapeParser;
+import org.elasticsearch.common.lucene.spatial.prefix.tree.SpatialPrefixTree;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.index.mapper.FieldMapper;
+import org.elasticsearch.index.mapper.Mapper;
+import org.elasticsearch.index.mapper.MapperParsingException;
+import org.elasticsearch.index.mapper.ParseContext;
+import org.elasticsearch.index.mapper.core.AbstractFieldMapper;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * FieldMapper for indexing {@link com.spatial4j.core.shape.Shape}s.
+ *
+ * Currently Shapes can only be indexed and can only be queried using
+ * {@link org.elasticsearch.index.query.GeoShapeFilterParser}, consequently
+ * a lot of behavior in this Mapper is disabled.
+ *
+ * Format supported:
+ *
+ * "field" : {
+ * "type" : "polygon",
+ * "coordinates" : [
+ * [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ]
+ * ]
+ * }
+ */
+public class GeoShapeFieldMapper extends AbstractFieldMapper {
+
+ public static final String CONTENT_TYPE = "geo_shape";
+
+ // TODO: Unsure if the units actually matter since we dont do distance calculations
+ public static final SpatialContext SPATIAL_CONTEXT = new JtsSpatialContext(DistanceUnits.KILOMETERS);
+
+ public static class Names {
+ public static final String TREE = "tree";
+ public static final String TREE_LEVELS = "tree_levels";
+ public static final String GEOHASH = "geohash";
+ public static final String QUADTREE = "quadtree";
+ public static final String DISTANCE_ERROR_PCT = "distance_error_pct";
+ }
+
+ public static class Defaults {
+ public static final String TREE = Names.GEOHASH;
+ public static final int GEOHASH_LEVELS = GeohashPrefixTree.getMaxLevelsPossible();
+ public static final int QUADTREE_LEVELS = QuadPrefixTree.DEFAULT_MAX_LEVELS;
+ public static final double DISTANCE_ERROR_PCT = 0.025d;
+ }
+
+ public static class Builder extends AbstractFieldMapper.Builder {
+
+ private String tree = Defaults.TREE;
+ private int treeLevels;
+ private double distanceErrorPct = Defaults.DISTANCE_ERROR_PCT;
+
+ private SpatialPrefixTree prefixTree;
+
+ public Builder(String name) {
+ super(name);
+ }
+
+ public Builder tree(String tree) {
+ this.tree = tree;
+ return this;
+ }
+
+ public Builder treeLevels(int treeLevels) {
+ this.treeLevels = treeLevels;
+ return this;
+ }
+
+ public Builder distanceErrorPct(double distanceErrorPct) {
+ this.distanceErrorPct = distanceErrorPct;
+ return this;
+ }
+
+ @Override
+ public GeoShapeFieldMapper build(BuilderContext context) {
+ if (tree.equals(Names.GEOHASH)) {
+ int levels = treeLevels != 0 ? treeLevels : Defaults.GEOHASH_LEVELS;
+ prefixTree = new GeohashPrefixTree(SPATIAL_CONTEXT, levels);
+ } else if (tree.equals(Names.QUADTREE)) {
+ int levels = treeLevels != 0 ? treeLevels : Defaults.QUADTREE_LEVELS;
+ prefixTree = new QuadPrefixTree(SPATIAL_CONTEXT, levels);
+ } else {
+ throw new ElasticSearchIllegalArgumentException("Unknown prefix tree type [" + tree + "]");
+ }
+
+ return new GeoShapeFieldMapper(buildNames(context), prefixTree, distanceErrorPct);
+ }
+ }
+
+ public static class TypeParser implements Mapper.TypeParser {
+
+ @Override
+ public Mapper.Builder parse(String name, Map node, ParserContext parserContext) throws MapperParsingException {
+ Builder builder = new Builder(name);
+
+ for (Map.Entry entry : node.entrySet()) {
+ String fieldName = Strings.toUnderscoreCase(entry.getKey());
+ Object fieldNode = entry.getValue();
+ if (Names.TREE.equals(fieldName)) {
+ builder.tree(fieldNode.toString());
+ } else if (Names.TREE_LEVELS.equals(fieldName)) {
+ builder.treeLevels(Integer.parseInt(fieldNode.toString()));
+ } else if (Names.DISTANCE_ERROR_PCT.equals(fieldName)) {
+ builder.distanceErrorPct(Double.parseDouble(fieldNode.toString()));
+ }
+ }
+ return builder;
+ }
+ }
+
+ private final SpatialStrategy spatialStrategy;
+
+ public GeoShapeFieldMapper(FieldMapper.Names names, SpatialPrefixTree prefixTree, double distanceErrorPct) {
+ super(names, Field.Index.NOT_ANALYZED, Field.Store.NO, Field.TermVector.NO, 1, true, true, null, null);
+ this.spatialStrategy = new TermQueryPrefixTreeStrategy(names, prefixTree, distanceErrorPct);
+ }
+
+ @Override
+ protected Fieldable parseCreateField(ParseContext context) throws IOException {
+ return spatialStrategy.createField(GeoJSONShapeParser.parse(context.parser()));
+ }
+
+ @Override
+ protected void doXContentBody(XContentBuilder builder) throws IOException {
+ builder.field("type", contentType());
+
+ // TODO: Come up with a better way to get the name, maybe pass it from builder
+ if (spatialStrategy.getPrefixTree() instanceof GeohashPrefixTree) {
+ // Don't emit the tree name since GeohashPrefixTree is the default
+ // Only emit the tree levels if it isn't the default value
+ if (spatialStrategy.getPrefixTree().getMaxLevels() != Defaults.GEOHASH_LEVELS) {
+ builder.field(Names.TREE_LEVELS, spatialStrategy.getPrefixTree().getMaxLevels());
+ }
+ } else {
+ builder.field(Names.TREE, Names.QUADTREE);
+ if (spatialStrategy.getPrefixTree().getMaxLevels() != Defaults.QUADTREE_LEVELS) {
+ builder.field(Names.TREE_LEVELS, spatialStrategy.getPrefixTree().getMaxLevels());
+ }
+ }
+
+ if (spatialStrategy.getDistanceErrorPct() != Defaults.DISTANCE_ERROR_PCT) {
+ builder.field(Names.DISTANCE_ERROR_PCT, spatialStrategy.getDistanceErrorPct());
+ }
+ }
+
+ @Override
+ protected String contentType() {
+ return CONTENT_TYPE;
+ }
+
+ @Override
+ public String value(Fieldable field) {
+ throw new UnsupportedOperationException("GeoShape fields cannot be converted to String values");
+ }
+
+ @Override
+ public String valueFromString(String value) {
+ throw new UnsupportedOperationException("GeoShape fields cannot be converted to String values");
+ }
+
+ @Override
+ public String valueAsString(Fieldable field) {
+ throw new UnsupportedOperationException("GeoShape fields cannot be converted to String values");
+ }
+
+ public SpatialStrategy spatialStrategy() {
+ return this.spatialStrategy;
+ }
+}
diff --git a/src/main/java/org/elasticsearch/index/query/FilterBuilders.java b/src/main/java/org/elasticsearch/index/query/FilterBuilders.java
index 793cc18f42a..4e6e8f0f5ac 100644
--- a/src/main/java/org/elasticsearch/index/query/FilterBuilders.java
+++ b/src/main/java/org/elasticsearch/index/query/FilterBuilders.java
@@ -19,6 +19,7 @@
package org.elasticsearch.index.query;
+import com.spatial4j.core.shape.Shape;
import org.elasticsearch.common.Nullable;
/**
@@ -338,6 +339,16 @@ public abstract class FilterBuilders {
return new GeoPolygonFilterBuilder(name);
}
+ /**
+ * A filter to filter based on the relationship between a shape and indexed shapes
+ *
+ * @param name The shape field name
+ * @param shape Shape to use in the filter
+ */
+ public static GeoShapeFilterBuilder geoShapeFilter(String name, Shape shape) {
+ return new GeoShapeFilterBuilder(name, shape);
+ }
+
/**
* A filter to filter only documents where a field exists in them.
*
diff --git a/src/main/java/org/elasticsearch/index/query/GeoShapeFilterBuilder.java b/src/main/java/org/elasticsearch/index/query/GeoShapeFilterBuilder.java
new file mode 100644
index 00000000000..ac73fddc3c5
--- /dev/null
+++ b/src/main/java/org/elasticsearch/index/query/GeoShapeFilterBuilder.java
@@ -0,0 +1,108 @@
+package org.elasticsearch.index.query;
+
+import com.spatial4j.core.shape.Shape;
+import org.elasticsearch.common.geo.GeoJSONShapeSerializer;
+import org.elasticsearch.common.geo.ShapeRelation;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+
+/**
+ * {@link FilterBuilder} that builds a GeoShape Filter
+ */
+public class GeoShapeFilterBuilder extends BaseFilterBuilder {
+
+ private final String name;
+
+ private ShapeRelation relation = ShapeRelation.INTERSECTS;
+
+ private final Shape shape;
+
+ private Boolean cache;
+ private String cacheKey;
+
+ private String filterName;
+
+ /**
+ * Creates a new GeoShapeFilterBuilder whose Filter will be against the
+ * given field name
+ *
+ * @param name Name of the field that will be filtered
+ * @param shape Shape used in the filter
+ */
+ public GeoShapeFilterBuilder(String name, Shape shape) {
+ this.name = name;
+ this.shape = shape;
+ }
+
+ /**
+ * Sets the {@link ShapeRelation} that defines how the Shape used in the
+ * Filter must relate to indexed Shapes
+ *
+ * @param relation ShapeRelation used in the filter
+ * @return this
+ */
+ public GeoShapeFilterBuilder relation(ShapeRelation relation) {
+ this.relation = relation;
+ return this;
+ }
+
+ /**
+ * Sets whether the filter will be cached.
+ *
+ * @param cache Whether filter will be cached
+ * @return this
+ */
+ public GeoShapeFilterBuilder cache(boolean cache) {
+ this.cache = cache;
+ return this;
+ }
+
+ /**
+ * Sets the key used for the filter if it is cached
+ *
+ * @param cacheKey Key for the Filter if cached
+ * @return this
+ */
+ public GeoShapeFilterBuilder cacheKey(String cacheKey) {
+ this.cacheKey = cacheKey;
+ return this;
+ }
+
+ /**
+ * Sets the name of the filter
+ *
+ * @param filterName Name of the filter
+ * @return this
+ */
+ public GeoShapeFilterBuilder filterName(String filterName) {
+ this.filterName = filterName;
+ return this;
+ }
+
+ @Override
+ protected void doXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject(GeoShapeFilterParser.NAME);
+
+ builder.startObject(name);
+ builder.field("relation", relation.getRelationName());
+
+ builder.startObject("shape");
+ GeoJSONShapeSerializer.serialize(shape, builder);
+ builder.endObject();
+
+ builder.endObject();
+
+ if (name != null) {
+ builder.field("_name", filterName);
+ }
+ if (cache != null) {
+ builder.field("_cache", cache);
+ }
+ if (cacheKey != null) {
+ builder.field("_cache_key", cacheKey);
+ }
+
+ builder.endObject();
+ }
+}
diff --git a/src/main/java/org/elasticsearch/index/query/GeoShapeFilterParser.java b/src/main/java/org/elasticsearch/index/query/GeoShapeFilterParser.java
new file mode 100644
index 00000000000..62d444583f0
--- /dev/null
+++ b/src/main/java/org/elasticsearch/index/query/GeoShapeFilterParser.java
@@ -0,0 +1,122 @@
+package org.elasticsearch.index.query;
+
+import com.spatial4j.core.shape.Shape;
+import org.apache.lucene.search.Filter;
+import org.elasticsearch.common.geo.ShapeRelation;
+import org.elasticsearch.common.geo.GeoJSONShapeParser;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.index.cache.filter.support.CacheKeyFilter;
+import org.elasticsearch.index.mapper.FieldMapper;
+import org.elasticsearch.index.mapper.MapperService;
+import org.elasticsearch.index.mapper.geo.GeoShapeFieldMapper;
+
+import java.io.IOException;
+
+import static org.elasticsearch.index.query.support.QueryParsers.wrapSmartNameFilter;
+
+/**
+ * {@link FilterParser} for filtering Documents based on {@link Shape}s.
+ *
+ * Only those fields mapped using {@link GeoShapeFieldMapper} can be filtered
+ * using this parser.
+ *
+ * Format supported:
+ *
+ * "field" : {
+ * "relation" : "intersects",
+ * "shape" : {
+ * "type" : "polygon",
+ * "coordinates" : [
+ * [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ]
+ * ]
+ * }
+ * }
+ */
+public class GeoShapeFilterParser implements FilterParser {
+
+ public static final String NAME = "geo_shape";
+
+ @Override
+ public String[] names() {
+ return new String[]{NAME, "geoShape"};
+ }
+
+ @Override
+ public Filter parse(QueryParseContext parseContext) throws IOException, QueryParsingException {
+ XContentParser parser = parseContext.parser();
+
+ String fieldName = null;
+ ShapeRelation shapeRelation = null;
+ Shape shape = null;
+ boolean cache = false;
+ CacheKeyFilter.Key cacheKey = null;
+ String filterName = null;
+
+ XContentParser.Token token;
+ String currentFieldName = null;
+
+ while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+ if (token == XContentParser.Token.FIELD_NAME) {
+ currentFieldName = parser.currentName();
+ } else if (token == XContentParser.Token.START_OBJECT) {
+ fieldName = currentFieldName;
+
+ while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+ if (token == XContentParser.Token.FIELD_NAME) {
+ currentFieldName = parser.currentName();
+
+ token = parser.nextToken();
+ if ("shape".equals(currentFieldName)) {
+ shape = GeoJSONShapeParser.parse(parser);
+ } else if ("relation".equals(currentFieldName)) {
+ shapeRelation = ShapeRelation.getRelationByName(parser.text());
+ if (shapeRelation == null) {
+ throw new QueryParsingException(parseContext.index(), "Unknown shape operation [" + parser.text() + " ]");
+ }
+ }
+ }
+ }
+ } else if (token.isValue()) {
+ if ("_name".equals(currentFieldName)) {
+ filterName = parser.text();
+ } else if ("_cache".equals(currentFieldName)) {
+ cache = parser.booleanValue();
+ } else if ("_cache_key".equals(currentFieldName)) {
+ cacheKey = new CacheKeyFilter.Key(parser.text());
+ }
+ }
+ }
+
+ if (shape == null) {
+ throw new QueryParsingException(parseContext.index(), "No Shape defined");
+ } else if (shapeRelation == null) {
+ throw new QueryParsingException(parseContext.index(), "No Shape Relation defined");
+ }
+
+ MapperService.SmartNameFieldMappers smartNameFieldMappers = parseContext.smartFieldMappers(fieldName);
+ if (smartNameFieldMappers == null || !smartNameFieldMappers.hasMapper()) {
+ throw new QueryParsingException(parseContext.index(), "Failed to find geo_shape field [" + fieldName + "]");
+ }
+
+ FieldMapper fieldMapper = smartNameFieldMappers.mapper();
+ // TODO: This isn't the nicest way to check this
+ if (!(fieldMapper instanceof GeoShapeFieldMapper)) {
+ throw new QueryParsingException(parseContext.index(), "Field [" + fieldName + "] is not a geo_shape");
+ }
+
+ GeoShapeFieldMapper shapeFieldMapper = (GeoShapeFieldMapper) fieldMapper;
+ Filter filter = shapeFieldMapper.spatialStrategy().createFilter(shape, shapeRelation);
+
+ if (cache) {
+ filter = parseContext.cacheFilter(filter, cacheKey);
+ }
+
+ filter = wrapSmartNameFilter(filter, smartNameFieldMappers, parseContext);
+
+ if (filterName != null) {
+ parseContext.addNamedFilter(filterName, filter);
+ }
+
+ return filter;
+ }
+}
diff --git a/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java
new file mode 100644
index 00000000000..27fdf308af7
--- /dev/null
+++ b/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java
@@ -0,0 +1,53 @@
+package org.elasticsearch.index.query;
+
+import com.spatial4j.core.shape.Shape;
+import org.elasticsearch.common.geo.GeoJSONShapeSerializer;
+import org.elasticsearch.common.geo.ShapeRelation;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+
+public class GeoShapeQueryBuilder extends BaseQueryBuilder implements BoostableQueryBuilder {
+
+ private final String name;
+ private final Shape shape;
+
+ private ShapeRelation relation = ShapeRelation.INTERSECTS;
+
+ private float boost = -1;
+
+ public GeoShapeQueryBuilder(String name, Shape shape) {
+ this.name = name;
+ this.shape = shape;
+ }
+
+ public GeoShapeQueryBuilder relation(ShapeRelation relation) {
+ this.relation = relation;
+ return this;
+ }
+
+ @Override
+ public GeoShapeQueryBuilder boost(float boost) {
+ this.boost = boost;
+ return this;
+ }
+
+ @Override
+ protected void doXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject(GeoShapeQueryParser.NAME);
+
+ builder.startObject(name);
+ builder.field("relation", relation.getRelationName());
+
+ builder.startObject("shape");
+ GeoJSONShapeSerializer.serialize(shape, builder);
+ builder.endObject();
+
+ if (boost != -1) {
+ builder.field("boost", boost);
+ }
+
+ builder.endObject();
+ }
+
+}
diff --git a/src/main/java/org/elasticsearch/index/query/GeoShapeQueryParser.java b/src/main/java/org/elasticsearch/index/query/GeoShapeQueryParser.java
new file mode 100644
index 00000000000..7724e1660ff
--- /dev/null
+++ b/src/main/java/org/elasticsearch/index/query/GeoShapeQueryParser.java
@@ -0,0 +1,87 @@
+package org.elasticsearch.index.query;
+
+import com.spatial4j.core.shape.Shape;
+import org.apache.lucene.search.Query;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.geo.GeoJSONShapeParser;
+import org.elasticsearch.common.geo.ShapeRelation;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.index.mapper.FieldMapper;
+import org.elasticsearch.index.mapper.MapperService;
+import org.elasticsearch.index.mapper.geo.GeoShapeFieldMapper;
+
+import java.io.IOException;
+
+public class GeoShapeQueryParser implements QueryParser {
+
+ public static final String NAME = "geo_shape";
+
+ @Override
+ public String[] names() {
+ return new String[]{NAME, Strings.toCamelCase(NAME)};
+ }
+
+ @Override
+ public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException {
+ XContentParser parser = parseContext.parser();
+
+ String fieldName = null;
+ ShapeRelation shapeRelation = null;
+ Shape shape = null;
+
+ XContentParser.Token token;
+ String currentFieldName = null;
+ float boost = 1f;
+
+ while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+ if (token == XContentParser.Token.FIELD_NAME) {
+ currentFieldName = parser.currentName();
+ } else if (token == XContentParser.Token.START_OBJECT) {
+ fieldName = currentFieldName;
+
+ while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+ if (token == XContentParser.Token.FIELD_NAME) {
+ currentFieldName = parser.currentName();
+
+ token = parser.nextToken();
+ if ("shape".equals(currentFieldName)) {
+ shape = GeoJSONShapeParser.parse(parser);
+ } else if ("relation".equals(currentFieldName)) {
+ shapeRelation = ShapeRelation.getRelationByName(parser.text());
+ if (shapeRelation == null) {
+ throw new QueryParsingException(parseContext.index(), "Unknown shape operation [" + parser.text() + " ]");
+ }
+ }
+ }
+ }
+ } else if (token.isValue()) {
+ if ("boost".equals(currentFieldName)) {
+ boost = parser.floatValue();
+ }
+ }
+ }
+
+ if (shape == null) {
+ throw new QueryParsingException(parseContext.index(), "No Shape defined");
+ } else if (shapeRelation == null) {
+ throw new QueryParsingException(parseContext.index(), "No Shape Relation defined");
+ }
+
+ MapperService.SmartNameFieldMappers smartNameFieldMappers = parseContext.smartFieldMappers(fieldName);
+ if (smartNameFieldMappers == null || !smartNameFieldMappers.hasMapper()) {
+ throw new QueryParsingException(parseContext.index(), "Failed to find geo_shape field [" + fieldName + "]");
+ }
+
+ FieldMapper fieldMapper = smartNameFieldMappers.mapper();
+ // TODO: This isn't the nicest way to check this
+ if (!(fieldMapper instanceof GeoShapeFieldMapper)) {
+ throw new QueryParsingException(parseContext.index(), "Field [" + fieldName + "] is not a geo_shape");
+ }
+
+ GeoShapeFieldMapper shapeFieldMapper = (GeoShapeFieldMapper) fieldMapper;
+
+ Query query = shapeFieldMapper.spatialStrategy().createQuery(shape, shapeRelation);
+ query.setBoost(boost);
+ return query;
+ }
+}
diff --git a/src/main/java/org/elasticsearch/index/query/QueryBuilders.java b/src/main/java/org/elasticsearch/index/query/QueryBuilders.java
index 98167f18441..53a323ba7f1 100644
--- a/src/main/java/org/elasticsearch/index/query/QueryBuilders.java
+++ b/src/main/java/org/elasticsearch/index/query/QueryBuilders.java
@@ -19,6 +19,7 @@
package org.elasticsearch.index.query;
+import com.spatial4j.core.shape.Shape;
import org.elasticsearch.common.Nullable;
/**
@@ -700,6 +701,17 @@ public abstract class QueryBuilders {
return new WrapperQueryBuilder(source, offset, length);
}
+ /**
+ * Query that matches Documents based on the relationship between the given shape and
+ * indexed shapes
+ *
+ * @param name The shape field name
+ * @param shape Shape to use in the Query
+ */
+ public static GeoShapeQueryBuilder geoShapeQuery(String name, Shape shape) {
+ return new GeoShapeQueryBuilder(name, shape);
+ }
+
private QueryBuilders() {
}
diff --git a/src/main/java/org/elasticsearch/indices/query/IndicesQueriesRegistry.java b/src/main/java/org/elasticsearch/indices/query/IndicesQueriesRegistry.java
index 85766ea3044..8dd0b4e4ee1 100644
--- a/src/main/java/org/elasticsearch/indices/query/IndicesQueriesRegistry.java
+++ b/src/main/java/org/elasticsearch/indices/query/IndicesQueriesRegistry.java
@@ -74,6 +74,7 @@ public class IndicesQueriesRegistry {
addQueryParser(queryParsers, new FuzzyLikeThisFieldQueryParser());
addQueryParser(queryParsers, new WrapperQueryParser());
addQueryParser(queryParsers, new IndicesQueryParser(clusterService));
+ addQueryParser(queryParsers, new GeoShapeQueryParser());
this.queryParsers = ImmutableMap.copyOf(queryParsers);
Map filterParsers = Maps.newHashMap();
@@ -92,6 +93,7 @@ public class IndicesQueriesRegistry {
addFilterParser(filterParsers, new GeoDistanceRangeFilterParser());
addFilterParser(filterParsers, new GeoBoundingBoxFilterParser());
addFilterParser(filterParsers, new GeoPolygonFilterParser());
+ addFilterParser(filterParsers, new GeoShapeFilterParser());
addFilterParser(filterParsers, new QueryFilterParser());
addFilterParser(filterParsers, new FQueryFilterParser());
addFilterParser(filterParsers, new BoolFilterParser());
diff --git a/src/test/java/org/elasticsearch/test/integration/search/geo/GeoShapeIntegrationTests.java b/src/test/java/org/elasticsearch/test/integration/search/geo/GeoShapeIntegrationTests.java
new file mode 100644
index 00000000000..1c5ca293b1f
--- /dev/null
+++ b/src/test/java/org/elasticsearch/test/integration/search/geo/GeoShapeIntegrationTests.java
@@ -0,0 +1,98 @@
+package org.elasticsearch.test.integration.search.geo;
+
+import com.spatial4j.core.shape.Shape;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.geo.ShapeRelation;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.index.query.GeoShapeFilterParser;
+import org.elasticsearch.test.integration.AbstractNodesTests;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import static org.elasticsearch.common.geo.ShapeBuilder.newRectangle;
+import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
+import static org.elasticsearch.index.query.FilterBuilders.geoShapeFilter;
+import static org.elasticsearch.index.query.QueryBuilders.filteredQuery;
+import static org.elasticsearch.index.query.QueryBuilders.geoShapeQuery;
+import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+
+public class GeoShapeIntegrationTests extends AbstractNodesTests {
+
+ private Client client;
+
+ @BeforeClass
+ public void createNodes() throws Exception {
+ startNode("server1");
+ startNode("server2");
+ client = getClient();
+ }
+
+ @AfterClass
+ public void closeNodes() {
+ client.close();
+ closeAllNodes();
+ }
+
+ protected Client getClient() {
+ return client("server1");
+ }
+
+ @Test
+ public void testIndexPointsFilterRectangle() throws Exception {
+ try {
+ client.admin().indices().prepareDelete("test").execute().actionGet();
+ } catch (Exception e) {
+ // ignore
+ }
+
+ String mapping = XContentFactory.jsonBuilder().startObject().startObject("type1")
+ .startObject("properties").startObject("location")
+ .field("type", "geo_shape")
+ .field("tree", "quadtree")
+ .endObject().endObject()
+ .endObject().endObject().string();
+ client.admin().indices().prepareCreate("test").addMapping("type1", mapping).execute().actionGet();
+ client.admin().cluster().prepareHealth().setWaitForGreenStatus().execute().actionGet();
+
+ client.prepareIndex("test", "type1", "1").setSource(jsonBuilder().startObject()
+ .field("name", "Document 1")
+ .startObject("location")
+ .field("type", "point")
+ .startArray("coordinates").value(-30).value(-30).endArray()
+ .endObject()
+ .endObject()).execute().actionGet();
+
+ client.prepareIndex("test", "type1", "2").setSource(jsonBuilder().startObject()
+ .field("name", "Document 2")
+ .startObject("location")
+ .field("type", "point")
+ .startArray("coordinates").value(-45).value(-50).endArray()
+ .endObject()
+ .endObject()).execute().actionGet();
+
+ client.admin().indices().prepareRefresh().execute().actionGet();
+
+ Shape shape = newRectangle().topLeft(-45, 45).bottomRight(45, -45).build();
+
+ SearchResponse searchResponse = client.prepareSearch()
+ .setQuery(filteredQuery(matchAllQuery(),
+ geoShapeFilter("location", shape).relation(ShapeRelation.INTERSECTS)))
+ .execute().actionGet();
+
+ assertThat(searchResponse.hits().getTotalHits(), equalTo(1l));
+ assertThat(searchResponse.hits().hits().length, equalTo(1));
+ assertThat(searchResponse.hits().getAt(0).id(), equalTo("1"));
+
+ searchResponse = client.prepareSearch()
+ .setQuery(geoShapeQuery("location", shape).relation(ShapeRelation.INTERSECTS))
+ .execute().actionGet();
+
+ assertThat(searchResponse.hits().getTotalHits(), equalTo(1l));
+ assertThat(searchResponse.hits().hits().length, equalTo(1));
+ assertThat(searchResponse.hits().getAt(0).id(), equalTo("1"));
+ }
+}
diff --git a/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeParserTests.java b/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeParserTests.java
new file mode 100644
index 00000000000..dfdcfa670e4
--- /dev/null
+++ b/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeParserTests.java
@@ -0,0 +1,147 @@
+package org.elasticsearch.test.unit.common.geo;
+
+import com.spatial4j.core.shape.Shape;
+import com.spatial4j.core.shape.jts.JtsGeometry;
+import com.spatial4j.core.shape.jts.JtsPoint;
+import com.vividsolutions.jts.geom.*;
+import org.elasticsearch.common.geo.GeoJSONShapeParser;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.json.JsonXContent;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.testng.Assert.assertEquals;
+
+/**
+ * Tests for {@link GeoJSONShapeParser}
+ */
+public class GeoJSONShapeParserTests {
+
+ private final static GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
+
+ @Test
+ public void testParse_simplePoint() throws IOException {
+ String pointGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "Point")
+ .startArray("coordinates").value(100.0).value(0.0).endArray()
+ .endObject().string();
+
+ Point expected = GEOMETRY_FACTORY.createPoint(new Coordinate(100.0, 0.0));
+ assertGeometryEquals(new JtsPoint(expected), pointGeoJson);
+ }
+
+ @Test
+ public void testParse_lineString() throws IOException {
+ String lineGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "LineString")
+ .startArray("coordinates")
+ .startArray().value(100.0).value(0.0).endArray()
+ .startArray().value(101.0).value(1.0).endArray()
+ .endArray()
+ .endObject().string();
+
+ List lineCoordinates = new ArrayList();
+ lineCoordinates.add(new Coordinate(100, 0));
+ lineCoordinates.add(new Coordinate(101, 1));
+
+ LineString expected = GEOMETRY_FACTORY.createLineString(
+ lineCoordinates.toArray(new Coordinate[lineCoordinates.size()]));
+ assertGeometryEquals(new JtsGeometry(expected), lineGeoJson);
+ }
+
+ @Test
+ public void testParse_polygonNoHoles() throws IOException {
+ String polygonGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "Polygon")
+ .startArray("coordinates")
+ .startArray()
+ .startArray().value(100.0).value(0.0).endArray()
+ .startArray().value(101.0).value(0.0).endArray()
+ .startArray().value(101.0).value(1.0).endArray()
+ .startArray().value(100.0).value(1.0).endArray()
+ .startArray().value(100.0).value(0.0).endArray()
+ .endArray()
+ .endArray()
+ .endObject().string();
+
+ List shellCoordinates = new ArrayList();
+ shellCoordinates.add(new Coordinate(100, 0));
+ shellCoordinates.add(new Coordinate(101, 0));
+ shellCoordinates.add(new Coordinate(101, 1));
+ shellCoordinates.add(new Coordinate(100, 1));
+ shellCoordinates.add(new Coordinate(100, 0));
+
+ LinearRing shell = GEOMETRY_FACTORY.createLinearRing(
+ shellCoordinates.toArray(new Coordinate[shellCoordinates.size()]));
+ Polygon expected = GEOMETRY_FACTORY.createPolygon(shell, null);
+ assertGeometryEquals(new JtsGeometry(expected), polygonGeoJson);
+ }
+
+ @Test
+ public void testParse_polygonWithHole() throws IOException {
+ String polygonGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "Polygon")
+ .startArray("coordinates")
+ .startArray()
+ .startArray().value(100.0).value(0.0).endArray()
+ .startArray().value(101.0).value(0.0).endArray()
+ .startArray().value(101.0).value(1.0).endArray()
+ .startArray().value(100.0).value(1.0).endArray()
+ .startArray().value(100.0).value(0.0).endArray()
+ .endArray()
+ .startArray()
+ .startArray().value(100.2).value(0.2).endArray()
+ .startArray().value(100.8).value(0.2).endArray()
+ .startArray().value(100.8).value(0.8).endArray()
+ .startArray().value(100.2).value(0.8).endArray()
+ .startArray().value(100.2).value(0.2).endArray()
+ .endArray()
+ .endArray()
+ .endObject().string();
+
+ List shellCoordinates = new ArrayList();
+ shellCoordinates.add(new Coordinate(100, 0));
+ shellCoordinates.add(new Coordinate(101, 0));
+ shellCoordinates.add(new Coordinate(101, 1));
+ shellCoordinates.add(new Coordinate(100, 1));
+ shellCoordinates.add(new Coordinate(100, 0));
+
+ List holeCoordinates = new ArrayList();
+ holeCoordinates.add(new Coordinate(100.2, 0.2));
+ holeCoordinates.add(new Coordinate(100.8, 0.2));
+ holeCoordinates.add(new Coordinate(100.8, 0.8));
+ holeCoordinates.add(new Coordinate(100.2, 0.8));
+ holeCoordinates.add(new Coordinate(100.2, 0.2));
+
+ LinearRing shell = GEOMETRY_FACTORY.createLinearRing(
+ shellCoordinates.toArray(new Coordinate[shellCoordinates.size()]));
+ LinearRing[] holes = new LinearRing[1];
+ holes[0] = GEOMETRY_FACTORY.createLinearRing(
+ holeCoordinates.toArray(new Coordinate[holeCoordinates.size()]));
+ Polygon expected = GEOMETRY_FACTORY.createPolygon(shell, holes);
+ assertGeometryEquals(new JtsGeometry(expected), polygonGeoJson);
+ }
+
+ @Test
+ public void testParse_multiPoint() throws IOException {
+ String multiPointGeoJson = XContentFactory.jsonBuilder().startObject().field("type", "MultiPoint")
+ .startArray("coordinates")
+ .startArray().value(100.0).value(0.0).endArray()
+ .startArray().value(101.0).value(1.0).endArray()
+ .endArray()
+ .endObject().string();
+
+ List multiPointCoordinates = new ArrayList();
+ multiPointCoordinates.add(new Coordinate(100, 0));
+ multiPointCoordinates.add(new Coordinate(101, 1));
+
+ MultiPoint expected = GEOMETRY_FACTORY.createMultiPoint(
+ multiPointCoordinates.toArray(new Coordinate[multiPointCoordinates.size()]));
+ assertGeometryEquals(new JtsGeometry(expected), multiPointGeoJson);
+ }
+
+ private void assertGeometryEquals(Shape expected, String geoJson) throws IOException {
+ XContentParser parser = JsonXContent.jsonXContent.createParser(geoJson);
+ assertEquals(GeoJSONShapeParser.parse(parser), expected);
+ }
+}
diff --git a/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeSerializerTests.java b/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeSerializerTests.java
new file mode 100644
index 00000000000..007076566bd
--- /dev/null
+++ b/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeSerializerTests.java
@@ -0,0 +1,153 @@
+package org.elasticsearch.test.unit.common.geo;
+
+import com.spatial4j.core.shape.Shape;
+import com.spatial4j.core.shape.jts.JtsGeometry;
+import com.spatial4j.core.shape.jts.JtsPoint;
+import com.vividsolutions.jts.geom.*;
+import org.elasticsearch.common.geo.GeoJSONShapeSerializer;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.testng.Assert.assertEquals;
+
+/**
+ * Tests for {@link GeoJSONShapeSerializer}
+ */
+public class GeoJSONShapeSerializerTests {
+
+ private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
+
+ @Test
+ public void testSerialize_simplePoint() throws IOException {
+ XContentBuilder expected = XContentFactory.jsonBuilder().startObject().field("type", "Point")
+ .startArray("coordinates").value(100.0).value(0.0).endArray()
+ .endObject();
+
+ Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(100.0, 0.0));
+ assertSerializationEquals(expected, new JtsPoint(point));
+ }
+
+ @Test
+ public void testSerialize_lineString() throws IOException {
+ XContentBuilder expected = XContentFactory.jsonBuilder().startObject().field("type", "LineString")
+ .startArray("coordinates")
+ .startArray().value(100.0).value(0.0).endArray()
+ .startArray().value(101.0).value(1.0).endArray()
+ .endArray()
+ .endObject();
+
+ List lineCoordinates = new ArrayList();
+ lineCoordinates.add(new Coordinate(100, 0));
+ lineCoordinates.add(new Coordinate(101, 1));
+
+ LineString lineString = GEOMETRY_FACTORY.createLineString(
+ lineCoordinates.toArray(new Coordinate[lineCoordinates.size()]));
+
+ assertSerializationEquals(expected, new JtsGeometry(lineString));
+ }
+
+ @Test
+ public void testSerialize_polygonNoHoles() throws IOException {
+ XContentBuilder expected = XContentFactory.jsonBuilder().startObject().field("type", "Polygon")
+ .startArray("coordinates")
+ .startArray()
+ .startArray().value(100.0).value(0.0).endArray()
+ .startArray().value(101.0).value(0.0).endArray()
+ .startArray().value(101.0).value(1.0).endArray()
+ .startArray().value(100.0).value(1.0).endArray()
+ .startArray().value(100.0).value(0.0).endArray()
+ .endArray()
+ .endArray()
+ .endObject();
+
+ List shellCoordinates = new ArrayList();
+ shellCoordinates.add(new Coordinate(100, 0));
+ shellCoordinates.add(new Coordinate(101, 0));
+ shellCoordinates.add(new Coordinate(101, 1));
+ shellCoordinates.add(new Coordinate(100, 1));
+ shellCoordinates.add(new Coordinate(100, 0));
+
+ LinearRing shell = GEOMETRY_FACTORY.createLinearRing(
+ shellCoordinates.toArray(new Coordinate[shellCoordinates.size()]));
+ Polygon polygon = GEOMETRY_FACTORY.createPolygon(shell, null);
+
+ assertSerializationEquals(expected, new JtsGeometry(polygon));
+ }
+
+ @Test
+ public void testSerialize_polygonWithHole() throws IOException {
+ XContentBuilder expected = XContentFactory.jsonBuilder().startObject().field("type", "Polygon")
+ .startArray("coordinates")
+ .startArray()
+ .startArray().value(100.0).value(0.0).endArray()
+ .startArray().value(101.0).value(0.0).endArray()
+ .startArray().value(101.0).value(1.0).endArray()
+ .startArray().value(100.0).value(1.0).endArray()
+ .startArray().value(100.0).value(0.0).endArray()
+ .endArray()
+ .startArray()
+ .startArray().value(100.2).value(0.2).endArray()
+ .startArray().value(100.8).value(0.2).endArray()
+ .startArray().value(100.8).value(0.8).endArray()
+ .startArray().value(100.2).value(0.8).endArray()
+ .startArray().value(100.2).value(0.2).endArray()
+ .endArray()
+ .endArray()
+ .endObject();
+
+ List shellCoordinates = new ArrayList();
+ shellCoordinates.add(new Coordinate(100, 0));
+ shellCoordinates.add(new Coordinate(101, 0));
+ shellCoordinates.add(new Coordinate(101, 1));
+ shellCoordinates.add(new Coordinate(100, 1));
+ shellCoordinates.add(new Coordinate(100, 0));
+
+ List holeCoordinates = new ArrayList();
+ holeCoordinates.add(new Coordinate(100.2, 0.2));
+ holeCoordinates.add(new Coordinate(100.8, 0.2));
+ holeCoordinates.add(new Coordinate(100.8, 0.8));
+ holeCoordinates.add(new Coordinate(100.2, 0.8));
+ holeCoordinates.add(new Coordinate(100.2, 0.2));
+
+ LinearRing shell = GEOMETRY_FACTORY.createLinearRing(
+ shellCoordinates.toArray(new Coordinate[shellCoordinates.size()]));
+ LinearRing[] holes = new LinearRing[1];
+ holes[0] = GEOMETRY_FACTORY.createLinearRing(
+ holeCoordinates.toArray(new Coordinate[holeCoordinates.size()]));
+ Polygon polygon = GEOMETRY_FACTORY.createPolygon(shell, holes);
+
+ assertSerializationEquals(expected, new JtsGeometry(polygon));
+ }
+
+ @Test
+ public void testSerialize_multiPoint() throws IOException {
+ XContentBuilder expected = XContentFactory.jsonBuilder().startObject().field("type", "MultiPoint")
+ .startArray("coordinates")
+ .startArray().value(100.0).value(0.0).endArray()
+ .startArray().value(101.0).value(1.0).endArray()
+ .endArray()
+ .endObject();
+
+ List multiPointCoordinates = new ArrayList();
+ multiPointCoordinates.add(new Coordinate(100, 0));
+ multiPointCoordinates.add(new Coordinate(101, 1));
+
+ MultiPoint multiPoint = GEOMETRY_FACTORY.createMultiPoint(
+ multiPointCoordinates.toArray(new Coordinate[multiPointCoordinates.size()]));
+
+ assertSerializationEquals(expected, new JtsGeometry(multiPoint));
+ }
+
+ private void assertSerializationEquals(XContentBuilder expected, Shape shape) throws IOException {
+ XContentBuilder builder = XContentFactory.jsonBuilder();
+ builder.startObject();
+ GeoJSONShapeSerializer.serialize(shape, builder);
+ builder.endObject();
+ assertEquals(expected.string(), builder.string());
+ }
+}
diff --git a/src/test/java/org/elasticsearch/test/unit/common/geo/ShapeBuilderTests.java b/src/test/java/org/elasticsearch/test/unit/common/geo/ShapeBuilderTests.java
new file mode 100644
index 00000000000..f9f18d8258b
--- /dev/null
+++ b/src/test/java/org/elasticsearch/test/unit/common/geo/ShapeBuilderTests.java
@@ -0,0 +1,73 @@
+package org.elasticsearch.test.unit.common.geo;
+
+import com.spatial4j.core.shape.Point;
+import com.spatial4j.core.shape.Rectangle;
+import com.spatial4j.core.shape.Shape;
+import com.vividsolutions.jts.geom.Coordinate;
+import com.vividsolutions.jts.geom.Geometry;
+import com.vividsolutions.jts.geom.LineString;
+import com.vividsolutions.jts.geom.Polygon;
+import org.elasticsearch.common.geo.ShapeBuilder;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+
+/**
+ * Tests for {@link ShapeBuilder}
+ */
+public class ShapeBuilderTests {
+
+ @Test
+ public void testNewPoint() {
+ Point point = ShapeBuilder.newPoint(-100, 45);
+ assertEquals(-100D, point.getX());
+ assertEquals(45D, point.getY());
+ }
+
+ @Test
+ public void testNewRectangle() {
+ Rectangle rectangle = ShapeBuilder.newRectangle().topLeft(-45, 30).bottomRight(45, -30).build();
+ assertEquals(-45D, rectangle.getMinX());
+ assertEquals(-30D, rectangle.getMinY());
+ assertEquals(45D, rectangle.getMaxX());
+ assertEquals(30D, rectangle.getMaxY());
+ }
+
+ @Test
+ public void testNewPolygon() {
+ Polygon polygon = ShapeBuilder.newPolygon()
+ .point(-45, 30)
+ .point(45, 30)
+ .point(45, -30)
+ .point(-45, -30)
+ .point(-45, 30).toPolygon();
+
+ LineString exterior = polygon.getExteriorRing();
+ assertEquals(exterior.getCoordinateN(0), new Coordinate(-45, 30));
+ assertEquals(exterior.getCoordinateN(1), new Coordinate(45, 30));
+ assertEquals(exterior.getCoordinateN(2), new Coordinate(45, -30));
+ assertEquals(exterior.getCoordinateN(3), new Coordinate(-45, -30));
+ }
+
+ @Test
+ public void testToJTSGeometry() {
+ ShapeBuilder.PolygonBuilder polygonBuilder = ShapeBuilder.newPolygon()
+ .point(-45, 30)
+ .point(45, 30)
+ .point(45, -30)
+ .point(-45, -30)
+ .point(-45, 30);
+
+ Shape polygon = polygonBuilder.build();
+ Geometry polygonGeometry = ShapeBuilder.toJTSGeometry(polygon);
+ assertEquals(polygonBuilder.toPolygon(), polygonGeometry);
+
+ Rectangle rectangle = ShapeBuilder.newRectangle().topLeft(-45, 30).bottomRight(45, -30).build();
+ Geometry rectangleGeometry = ShapeBuilder.toJTSGeometry(rectangle);
+ assertEquals(rectangleGeometry, polygonGeometry);
+
+ Point point = ShapeBuilder.newPoint(-45, 30);
+ Geometry pointGeometry = ShapeBuilder.toJTSGeometry(point);
+ assertEquals(pointGeometry.getCoordinate(), new Coordinate(-45, 30));
+ }
+}
diff --git a/src/test/java/org/elasticsearch/test/unit/common/lucene/spatial/prefix/TermQueryPrefixTreeStrategyTests.java b/src/test/java/org/elasticsearch/test/unit/common/lucene/spatial/prefix/TermQueryPrefixTreeStrategyTests.java
new file mode 100644
index 00000000000..34e0ade9d5a
--- /dev/null
+++ b/src/test/java/org/elasticsearch/test/unit/common/lucene/spatial/prefix/TermQueryPrefixTreeStrategyTests.java
@@ -0,0 +1,165 @@
+package org.elasticsearch.test.unit.common.lucene.spatial.prefix;
+
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.context.jts.JtsSpatialContext;
+import com.spatial4j.core.distance.DistanceUnits;
+import com.spatial4j.core.shape.Rectangle;
+import com.spatial4j.core.shape.Shape;
+import org.apache.lucene.analysis.KeywordAnalyzer;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.search.*;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.RAMDirectory;
+import org.apache.lucene.util.IOUtils;
+import org.apache.lucene.util.Version;
+import org.elasticsearch.common.lucene.spatial.prefix.TermQueryPrefixTreeStrategy;
+import org.elasticsearch.common.lucene.spatial.prefix.tree.GeohashPrefixTree;
+import org.elasticsearch.common.lucene.spatial.prefix.tree.QuadPrefixTree;
+import org.elasticsearch.common.lucene.spatial.prefix.tree.SpatialPrefixTree;
+import org.elasticsearch.index.mapper.FieldMapper;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.elasticsearch.common.geo.ShapeBuilder.*;
+import static org.testng.Assert.assertTrue;
+
+/**
+ * Tests for {@link TermQueryPrefixTreeStrategy}
+ */
+public class TermQueryPrefixTreeStrategyTests {
+
+ private static final SpatialContext SPATIAL_CONTEXT = new JtsSpatialContext(DistanceUnits.KILOMETERS);
+
+ // TODO: Randomize the implementation choice
+ private static final SpatialPrefixTree QUAD_PREFIX_TREE =
+ new QuadPrefixTree(SPATIAL_CONTEXT, QuadPrefixTree.DEFAULT_MAX_LEVELS);
+ private static final SpatialPrefixTree GEOHASH_PREFIX_TREE
+ = new GeohashPrefixTree(SPATIAL_CONTEXT, GeohashPrefixTree.getMaxLevelsPossible());
+
+ private static final TermQueryPrefixTreeStrategy STRATEGY = new TermQueryPrefixTreeStrategy(new FieldMapper.Names("shape"), GEOHASH_PREFIX_TREE, 0.025);
+
+ private Directory directory;
+ private IndexReader indexReader;
+ private IndexSearcher indexSearcher;
+
+ @BeforeTest
+ public void setUp() throws IOException {
+ directory = new RAMDirectory();
+ IndexWriter writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_36, new KeywordAnalyzer()));
+
+ writer.addDocument(newDocument("1", newPoint(-30, -30)));
+ writer.addDocument(newDocument("2", newPoint(-45, -45)));
+ writer.addDocument(newDocument("3", newPoint(-45, 50)));
+ writer.addDocument(newDocument("4", newRectangle().topLeft(-50, 50).bottomRight(-38, 38).build()));
+
+ indexReader = IndexReader.open(writer, true);
+ indexSearcher = new IndexSearcher(indexReader);
+ }
+
+ private Document newDocument(String id, Shape shape) {
+ Document document = new Document();
+ document.add(new Field("id", id, Field.Store.YES, Field.Index.NOT_ANALYZED));
+ document.add(STRATEGY.createField(shape));
+ return document;
+ }
+
+ private void assertTopDocs(TopDocs topDocs, String... ids) throws IOException {
+ assertTrue(ids.length <= topDocs.totalHits, "Query has more hits than expected");
+
+ Set foundIDs = new HashSet();
+ for (ScoreDoc doc : topDocs.scoreDocs) {
+ Document foundDocument = indexSearcher.doc(doc.doc);
+ foundIDs.add(foundDocument.getFieldable("id").stringValue());
+ }
+
+ for (String id : ids) {
+ assertTrue(foundIDs.contains(id), "ID [" + id + "] was not found in query results");
+ }
+ }
+
+ @Test
+ public void testIntersectionRelation() throws IOException {
+ Rectangle rectangle = newRectangle().topLeft(-45, 45).bottomRight(45, -45).build();
+
+ Filter filter = STRATEGY.createIntersectsFilter(rectangle);
+ assertTopDocs(indexSearcher.search(new MatchAllDocsQuery(), filter, 10), "1", "2", "4");
+
+ Query query = STRATEGY.createIntersectsQuery(rectangle);
+ assertTopDocs(indexSearcher.search(query, 10), "1", "2", "4");
+
+ Shape polygon = newPolygon()
+ .point(-45, 45)
+ .point(45, 45)
+ .point(45, -45)
+ .point(-45, -45)
+ .point(-45, 45).build();
+
+ filter = STRATEGY.createIntersectsFilter(polygon);
+ assertTopDocs(indexSearcher.search(new MatchAllDocsQuery(), filter, 10), "1", "2", "4");
+
+ query = STRATEGY.createIntersectsQuery(polygon);
+ assertTopDocs(indexSearcher.search(query, 10), "1", "2", "4");
+ }
+
+ @Test
+ public void testDisjointRelation() throws IOException {
+ Rectangle rectangle = newRectangle().topLeft(-45, 45).bottomRight(45, -45).build();
+
+ Filter filter = STRATEGY.createDisjointFilter(rectangle);
+ assertTopDocs(indexSearcher.search(new MatchAllDocsQuery(), filter, 10), "3");
+
+ Query query = STRATEGY.createDisjointQuery(rectangle);
+ assertTopDocs(indexSearcher.search(query, 10), "3");
+
+ Shape polygon = newPolygon()
+ .point(-45, 45)
+ .point(45, 45)
+ .point(45, -45)
+ .point(-45, -45)
+ .point(-45, 45).build();
+
+ filter = STRATEGY.createDisjointFilter(polygon);
+ assertTopDocs(indexSearcher.search(new MatchAllDocsQuery(), filter, 10), "3");
+
+ query = STRATEGY.createDisjointQuery(polygon);
+ assertTopDocs(indexSearcher.search(query, 10), "3");
+ }
+
+ @Test
+ public void testContainsRelation() throws IOException {
+ Rectangle rectangle = newRectangle().topLeft(-45, 45).bottomRight(45, -45).build();
+
+ Filter filter = STRATEGY.createContainsFilter(rectangle);
+ assertTopDocs(indexSearcher.search(new MatchAllDocsQuery(), filter, 10), "1");
+
+ Query query = STRATEGY.createContainsQuery(rectangle);
+ assertTopDocs(indexSearcher.search(query, 10), "1");
+
+ Shape polygon = newPolygon()
+ .point(-45, 45)
+ .point(45, 45)
+ .point(45, -45)
+ .point(-45, -45)
+ .point(-45, 45).build();
+
+ filter = STRATEGY.createContainsFilter(polygon);
+ assertTopDocs(indexSearcher.search(new MatchAllDocsQuery(), filter, 10), "1");
+
+ query = STRATEGY.createContainsQuery(polygon);
+ assertTopDocs(indexSearcher.search(query, 10), "1");
+ }
+
+ @AfterTest
+ public void tearDown() throws IOException {
+ IOUtils.close(indexSearcher, indexReader, directory);
+ }
+}
diff --git a/src/test/java/org/elasticsearch/test/unit/index/mapper/geo/GeoShapeFieldMapperTests.java b/src/test/java/org/elasticsearch/test/unit/index/mapper/geo/GeoShapeFieldMapperTests.java
new file mode 100644
index 00000000000..2a3ca864bbf
--- /dev/null
+++ b/src/test/java/org/elasticsearch/test/unit/index/mapper/geo/GeoShapeFieldMapperTests.java
@@ -0,0 +1,86 @@
+package org.elasticsearch.test.unit.index.mapper.geo;
+
+import org.elasticsearch.common.lucene.spatial.SpatialStrategy;
+import org.elasticsearch.common.lucene.spatial.prefix.tree.GeohashPrefixTree;
+import org.elasticsearch.common.lucene.spatial.prefix.tree.QuadPrefixTree;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.index.mapper.DocumentMapper;
+import org.elasticsearch.index.mapper.FieldMapper;
+import org.elasticsearch.index.mapper.geo.GeoShapeFieldMapper;
+import org.elasticsearch.test.unit.index.mapper.MapperTests;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.instanceOf;
+
+public class GeoShapeFieldMapperTests {
+
+ @Test
+ public void testDefaultConfiguration() throws IOException {
+ String mapping = XContentFactory.jsonBuilder().startObject().startObject("type1")
+ .startObject("properties").startObject("location")
+ .field("type", "geo_shape")
+ .endObject().endObject()
+ .endObject().endObject().string();
+
+ DocumentMapper defaultMapper = MapperTests.newParser().parse(mapping);
+ FieldMapper fieldMapper = defaultMapper.mappers().name("location").mapper();
+ assertThat(fieldMapper, instanceOf(GeoShapeFieldMapper.class));
+
+ GeoShapeFieldMapper geoShapeFieldMapper = (GeoShapeFieldMapper) fieldMapper;
+ SpatialStrategy strategy = geoShapeFieldMapper.spatialStrategy();
+
+ assertThat(strategy.getDistanceErrorPct(), equalTo(GeoShapeFieldMapper.Defaults.DISTANCE_ERROR_PCT));
+ assertThat(strategy.getPrefixTree(), instanceOf(GeohashPrefixTree.class));
+ assertThat(strategy.getPrefixTree().getMaxLevels(), equalTo(GeoShapeFieldMapper.Defaults.GEOHASH_LEVELS));
+ }
+
+ @Test
+ public void testGeohashConfiguration() throws IOException {
+ String mapping = XContentFactory.jsonBuilder().startObject().startObject("type1")
+ .startObject("properties").startObject("location")
+ .field("type", "geo_shape")
+ .field("tree", "geohash")
+ .field("tree_levels", "4")
+ .field("distance_error_pct", "0.1")
+ .endObject().endObject()
+ .endObject().endObject().string();
+
+ DocumentMapper defaultMapper = MapperTests.newParser().parse(mapping);
+ FieldMapper fieldMapper = defaultMapper.mappers().name("location").mapper();
+ assertThat(fieldMapper, instanceOf(GeoShapeFieldMapper.class));
+
+ GeoShapeFieldMapper geoShapeFieldMapper = (GeoShapeFieldMapper) fieldMapper;
+ SpatialStrategy strategy = geoShapeFieldMapper.spatialStrategy();
+
+ assertThat(strategy.getDistanceErrorPct(), equalTo(0.1));
+ assertThat(strategy.getPrefixTree(), instanceOf(GeohashPrefixTree.class));
+ assertThat(strategy.getPrefixTree().getMaxLevels(), equalTo(4));
+ }
+
+ @Test
+ public void testQuadtreeConfiguration() throws IOException {
+ String mapping = XContentFactory.jsonBuilder().startObject().startObject("type1")
+ .startObject("properties").startObject("location")
+ .field("type", "geo_shape")
+ .field("tree", "quadtree")
+ .field("tree_levels", "6")
+ .field("distance_error_pct", "0.5")
+ .endObject().endObject()
+ .endObject().endObject().string();
+
+ DocumentMapper defaultMapper = MapperTests.newParser().parse(mapping);
+ FieldMapper fieldMapper = defaultMapper.mappers().name("location").mapper();
+ assertThat(fieldMapper, instanceOf(GeoShapeFieldMapper.class));
+
+ GeoShapeFieldMapper geoShapeFieldMapper = (GeoShapeFieldMapper) fieldMapper;
+ SpatialStrategy strategy = geoShapeFieldMapper.spatialStrategy();
+
+ assertThat(strategy.getDistanceErrorPct(), equalTo(0.5));
+ assertThat(strategy.getPrefixTree(), instanceOf(QuadPrefixTree.class));
+ assertThat(strategy.getPrefixTree().getMaxLevels(), equalTo(6));
+ }
+}
diff --git a/src/test/java/org/elasticsearch/test/unit/index/mapper/geopoint/GeohashMappingGeoPointTests.java b/src/test/java/org/elasticsearch/test/unit/index/mapper/geo/GeohashMappingGeoPointTests.java
similarity index 98%
rename from src/test/java/org/elasticsearch/test/unit/index/mapper/geopoint/GeohashMappingGeoPointTests.java
rename to src/test/java/org/elasticsearch/test/unit/index/mapper/geo/GeohashMappingGeoPointTests.java
index 60dae6f9eee..64feebec0b1 100644
--- a/src/test/java/org/elasticsearch/test/unit/index/mapper/geopoint/GeohashMappingGeoPointTests.java
+++ b/src/test/java/org/elasticsearch/test/unit/index/mapper/geo/GeohashMappingGeoPointTests.java
@@ -17,7 +17,7 @@
* under the License.
*/
-package org.elasticsearch.test.unit.index.mapper.geopoint;
+package org.elasticsearch.test.unit.index.mapper.geo;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.index.mapper.DocumentMapper;
diff --git a/src/test/java/org/elasticsearch/test/unit/index/mapper/geopoint/LatLonAndGeohashMappingGeoPointTests.java b/src/test/java/org/elasticsearch/test/unit/index/mapper/geo/LatLonAndGeohashMappingGeoPointTests.java
similarity index 98%
rename from src/test/java/org/elasticsearch/test/unit/index/mapper/geopoint/LatLonAndGeohashMappingGeoPointTests.java
rename to src/test/java/org/elasticsearch/test/unit/index/mapper/geo/LatLonAndGeohashMappingGeoPointTests.java
index 3a739c334e3..66418c25780 100644
--- a/src/test/java/org/elasticsearch/test/unit/index/mapper/geopoint/LatLonAndGeohashMappingGeoPointTests.java
+++ b/src/test/java/org/elasticsearch/test/unit/index/mapper/geo/LatLonAndGeohashMappingGeoPointTests.java
@@ -17,7 +17,7 @@
* under the License.
*/
-package org.elasticsearch.test.unit.index.mapper.geopoint;
+package org.elasticsearch.test.unit.index.mapper.geo;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.index.mapper.DocumentMapper;
diff --git a/src/test/java/org/elasticsearch/test/unit/index/mapper/geopoint/LatLonMappingGeoPointTests.java b/src/test/java/org/elasticsearch/test/unit/index/mapper/geo/LatLonMappingGeoPointTests.java
similarity index 99%
rename from src/test/java/org/elasticsearch/test/unit/index/mapper/geopoint/LatLonMappingGeoPointTests.java
rename to src/test/java/org/elasticsearch/test/unit/index/mapper/geo/LatLonMappingGeoPointTests.java
index 9e74dfa426b..0e926803326 100644
--- a/src/test/java/org/elasticsearch/test/unit/index/mapper/geopoint/LatLonMappingGeoPointTests.java
+++ b/src/test/java/org/elasticsearch/test/unit/index/mapper/geo/LatLonMappingGeoPointTests.java
@@ -17,7 +17,7 @@
* under the License.
*/
-package org.elasticsearch.test.unit.index.mapper.geopoint;
+package org.elasticsearch.test.unit.index.mapper.geo;
import org.elasticsearch.ElasticSearchIllegalArgumentException;
import org.elasticsearch.common.Numbers;
diff --git a/src/test/java/org/elasticsearch/test/unit/index/query/SimpleIndexQueryParserTests.java b/src/test/java/org/elasticsearch/test/unit/index/query/SimpleIndexQueryParserTests.java
index 3e5844bc969..313821f7dfe 100644
--- a/src/test/java/org/elasticsearch/test/unit/index/query/SimpleIndexQueryParserTests.java
+++ b/src/test/java/org/elasticsearch/test/unit/index/query/SimpleIndexQueryParserTests.java
@@ -1822,4 +1822,27 @@ public class SimpleIndexQueryParserTests {
assertThat(filter.points()[2].lat, closeTo(20, 0.00001));
assertThat(filter.points()[2].lon, closeTo(-90, 0.00001));
}
+
+ @Test
+ public void testGeoShapeFilter() throws IOException {
+ IndexQueryParserService queryParser = queryParser();
+ String query = copyToStringFromClasspath("/org/elasticsearch/test/unit/index/query/geoShape-filter.json");
+ Query parsedQuery = queryParser.parse(query).query();
+ assertThat(parsedQuery, instanceOf(DeletionAwareConstantScoreQuery.class));
+ DeletionAwareConstantScoreQuery constantScoreQuery = (DeletionAwareConstantScoreQuery) parsedQuery;
+ XTermsFilter filter = (XTermsFilter) constantScoreQuery.getFilter();
+ Term exampleTerm = filter.getTerms()[0];
+ assertThat(exampleTerm.field(), equalTo("country"));
+ }
+
+ @Test
+ public void testGeoShapeQuery() throws IOException {
+ IndexQueryParserService queryParser = queryParser();
+ String query = copyToStringFromClasspath("/org/elasticsearch/test/unit/index/query/geoShape-query.json");
+ Query parsedQuery = queryParser.parse(query).query();
+ assertThat(parsedQuery, instanceOf(BooleanQuery.class));
+ BooleanQuery booleanQuery = (BooleanQuery) parsedQuery;
+ TermQuery termQuery = (TermQuery) booleanQuery.getClauses()[0].getQuery();
+ assertThat(termQuery.getTerm().field(), equalTo("country"));
+ }
}
diff --git a/src/test/java/org/elasticsearch/test/unit/index/query/geoShape-filter.json b/src/test/java/org/elasticsearch/test/unit/index/query/geoShape-filter.json
new file mode 100644
index 00000000000..a4392ae3465
--- /dev/null
+++ b/src/test/java/org/elasticsearch/test/unit/index/query/geoShape-filter.json
@@ -0,0 +1,21 @@
+{
+ "filtered" : {
+ "query" : {
+ "match_all" : {}
+ },
+ "filter" : {
+ "geo_shape" : {
+ "country" : {
+ "shape" : {
+ "type" : "Envelope",
+ "coordinates" : [
+ [-45, 45],
+ [45, -45]
+ ]
+ },
+ "relation" : "intersects"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/elasticsearch/test/unit/index/query/geoShape-query.json b/src/test/java/org/elasticsearch/test/unit/index/query/geoShape-query.json
new file mode 100644
index 00000000000..e0af8278a53
--- /dev/null
+++ b/src/test/java/org/elasticsearch/test/unit/index/query/geoShape-query.json
@@ -0,0 +1,14 @@
+{
+ "geo_shape" : {
+ "country" : {
+ "shape" : {
+ "type" : "Envelope",
+ "coordinates" : [
+ [-45, 45],
+ [45, -45]
+ ]
+ },
+ "relation" : "intersects"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/elasticsearch/test/unit/index/query/mapping.json b/src/test/java/org/elasticsearch/test/unit/index/query/mapping.json
index 8d21cc4409f..2a21760f194 100644
--- a/src/test/java/org/elasticsearch/test/unit/index/query/mapping.json
+++ b/src/test/java/org/elasticsearch/test/unit/index/query/mapping.json
@@ -3,6 +3,9 @@
"properties":{
"location":{
"type":"geo_point"
+ },
+ "country" : {
+ "type" : "geo_shape"
}
}
}