Added GeoShape indexing and querying support

This commit is contained in:
Chris Male 2012-06-26 23:29:08 +12:00 committed by Shay Banon
parent ad0e916fb7
commit bea4346f3a
35 changed files with 3227 additions and 3 deletions

14
pom.xml
View File

@ -152,6 +152,20 @@
</dependency>
<!-- END: dependencies that are shaded -->
<dependency>
<groupId>com.spatial4j</groupId>
<artifactId>spatial4j</artifactId>
<version>0.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.vividsolutions</groupId>
<artifactId>jts</artifactId>
<version>1.12</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>

View File

@ -8,6 +8,8 @@
<include>log4j:log4j</include>
<include>net.java.dev.jna:jna</include>
<include>org.xerial.snappy:snappy-java</include>
<include>com.spatial4j:spatial4j</include>
<include>com.vividsolutions:jts</include>
</includes>
</dependencySet>
<dependencySet>

View File

@ -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<CoordinateNode> nodes = new ArrayList<CoordinateNode>();
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<CoordinateNode> 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<CoordinateNode> children) {
this.children = children;
}
}
}

View File

@ -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();
}
}

View File

@ -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<Point> points = new ArrayList<Point>();
/**
* 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);
}
}
}

View File

@ -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;
}
}

View File

@ -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> nodeTokenStream = new ThreadLocal<NodeTokenStream>() {
@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<Node> 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;
}
}

View File

@ -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<Node> nodes;
private Iterator<Node> 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<Node> nodes) {
this.nodes = nodes;
}
}

View File

@ -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<Node> 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<Node> 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<Node> 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<Node> 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);
}
}

View File

@ -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<Node> 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<Node> getSubCells() {
String[] hashes = GeohashUtils.getSubGeohashes(getGeohash());//sorted
List<Node> cells = new ArrayList<Node>(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
}

View File

@ -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<Node> {
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<Node> 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<Node> cells = getSubCells();
if (shapeFilter == null) {
return cells;
}
List<Node> copy = new ArrayList<Node>(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<Node> 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 : "");
}
}

View File

@ -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<Node> cells = new ArrayList<Node>(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<Node> 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<Node> 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<Node> 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<Node> getSubCells() {
List<Node> cells = new ArrayList<Node>(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
}

View File

@ -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.
* <p/>
* 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<Node> getNodes(Shape shape, int detailLevel, boolean inclParents) {
if (detailLevel > maxLevels) {
throw new IllegalArgumentException("detailLevel > maxLevels");
}
List<Node> cells;
if (shape instanceof Point) {
//optimized point algorithm
final int initialCapacity = inclParents ? 1 + detailLevel : 1;
cells = new ArrayList<Node>(initialCapacity);
recursiveGetNodes(getWorldNode(), (Point) shape, detailLevel, true, cells);
assert cells.size() == initialCapacity;
} else {
cells = new ArrayList<Node>(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<Node> result) {
if (node.isLeaf()) {//cell is within shape
result.add(node);
return;
}
final Collection<Node> 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<Node> 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<Node> 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<Node> cells = new ArrayList<Node>(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<String> nodesToTokenStrings(Collection<Node> nodes) {
List<String> tokens = new ArrayList<String>((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;
}
}

View File

@ -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<String, Mapper.TypeParser>()

View File

@ -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<String> {
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<Builder, GeoShapeFieldMapper> {
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<String, Object> node, ParserContext parserContext) throws MapperParsingException {
Builder builder = new Builder(name);
for (Map.Entry<String, Object> 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;
}
}

View File

@ -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.
*

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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<GeoShapeQueryBuilder> {
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();
}
}

View File

@ -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;
}
}

View File

@ -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() {
}

View File

@ -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<String, FilterParser> 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());

View File

@ -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"));
}
}

View File

@ -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<Coordinate> lineCoordinates = new ArrayList<Coordinate>();
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<Coordinate> shellCoordinates = new ArrayList<Coordinate>();
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<Coordinate> shellCoordinates = new ArrayList<Coordinate>();
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<Coordinate> holeCoordinates = new ArrayList<Coordinate>();
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<Coordinate> multiPointCoordinates = new ArrayList<Coordinate>();
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);
}
}

View File

@ -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<Coordinate> lineCoordinates = new ArrayList<Coordinate>();
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<Coordinate> shellCoordinates = new ArrayList<Coordinate>();
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<Coordinate> shellCoordinates = new ArrayList<Coordinate>();
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<Coordinate> holeCoordinates = new ArrayList<Coordinate>();
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<Coordinate> multiPointCoordinates = new ArrayList<Coordinate>();
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());
}
}

View File

@ -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));
}
}

View File

@ -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<String> foundIDs = new HashSet<String>();
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);
}
}

View File

@ -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));
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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"));
}
}

View File

@ -0,0 +1,21 @@
{
"filtered" : {
"query" : {
"match_all" : {}
},
"filter" : {
"geo_shape" : {
"country" : {
"shape" : {
"type" : "Envelope",
"coordinates" : [
[-45, 45],
[45, -45]
]
},
"relation" : "intersects"
}
}
}
}
}

View File

@ -0,0 +1,14 @@
{
"geo_shape" : {
"country" : {
"shape" : {
"type" : "Envelope",
"coordinates" : [
[-45, 45],
[45, -45]
]
},
"relation" : "intersects"
}
}
}

View File

@ -3,6 +3,9 @@
"properties":{
"location":{
"type":"geo_point"
},
"country" : {
"type" : "geo_shape"
}
}
}