diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeoShapeUtils.java b/server/src/main/java/org/elasticsearch/common/geo/GeoShapeUtils.java new file mode 100644 index 00000000000..7e23385f2ad --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/geo/GeoShapeUtils.java @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.common.geo; + +import org.elasticsearch.geometry.Circle; +import org.elasticsearch.geometry.Line; +import org.elasticsearch.geometry.Point; +import org.elasticsearch.geometry.Polygon; +import org.elasticsearch.geometry.Rectangle; + + +/** + * Utility class that transforms Elasticsearch geometry objects to the Lucene representation + */ +public class GeoShapeUtils { + + public static org.apache.lucene.geo.Polygon toLucenePolygon(Polygon polygon) { + org.apache.lucene.geo.Polygon[] holes = new org.apache.lucene.geo.Polygon[polygon.getNumberOfHoles()]; + for(int i = 0; i geometries = visitor.geometries(); + if (geometries.size() == 0) { return new MatchNoDocsQuery(); } - return processedShape.visit(new ShapeVisitor(context, fieldName, relation)); + return LatLonShape.newGeometryQuery(fieldName, relation.getLuceneRelation(), + geometries.toArray(new LatLonGeometry[geometries.size()])); } - private class ShapeVisitor implements GeometryVisitor { - QueryShardContext context; - MappedFieldType fieldType; - String fieldName; - ShapeRelation relation; + private static class LuceneGeometryCollector implements GeometryVisitor { + private final List geometries = new ArrayList<>(); + private final String name; + private final QueryShardContext context; - ShapeVisitor(QueryShardContext context, String fieldName, ShapeRelation relation) { + private LuceneGeometryCollector(String name, QueryShardContext context) { + this.name = name; this.context = context; - this.fieldType = context.fieldMapper(fieldName); - this.fieldName = fieldName; - this.relation = relation; + } + + List geometries() { + return geometries; } @Override - public Query visit(Circle circle) { - throw new QueryShardException(context, "Field [" + fieldName + "] found an unknown shape Circle"); - } - - @Override - public Query visit(GeometryCollection collection) { - BooleanQuery.Builder bqb = new BooleanQuery.Builder(); - visit(bqb, collection); - return bqb.build(); - } - - private void visit(BooleanQuery.Builder bqb, GeometryCollection collection) { - BooleanClause.Occur occur; - if (relation == ShapeRelation.CONTAINS || relation == ShapeRelation.DISJOINT) { - // all shapes must be disjoint / must be contained in relation to the indexed shape. - occur = BooleanClause.Occur.MUST; - } else { - // at least one shape must intersect / contain the indexed shape. - occur = BooleanClause.Occur.SHOULD; + public Void visit(Circle circle) { + if (circle.isEmpty() == false) { + geometries.add(GeoShapeUtils.toLuceneCircle(circle)); } + return null; + } + + @Override + public Void visit(GeometryCollection collection) { for (Geometry shape : collection) { - bqb.add(shape.visit(this), occur); + shape.visit(this); + } + return null; + } + + @Override + public Void visit(org.elasticsearch.geometry.Line line) { + if (line.isEmpty() == false) { + List collector = new ArrayList<>(); + GeoLineDecomposer.decomposeLine(line, collector); + collectLines(collector); + } + return null; + } + + @Override + public Void visit(LinearRing ring) { + throw new QueryShardException(context, "Field [" + name + "] found and unsupported shape LinearRing"); + } + + @Override + public Void visit(MultiLine multiLine) { + List collector = new ArrayList<>(); + GeoLineDecomposer.decomposeMultiLine(multiLine, collector); + collectLines(collector); + return null; + } + + @Override + public Void visit(MultiPoint multiPoint) { + for (Point point : multiPoint) { + visit(point); + } + return null; + } + + @Override + public Void visit(MultiPolygon multiPolygon) { + if (multiPolygon.isEmpty() == false) { + List collector = new ArrayList<>(); + GeoPolygonDecomposer.decomposeMultiPolygon(multiPolygon, true, collector); + collectPolygons(collector); + } + return null; + } + + @Override + public Void visit(Point point) { + if (point.isEmpty() == false) { + geometries.add(GeoShapeUtils.toLucenePoint(point)); + } + return null; + + } + + @Override + public Void visit(org.elasticsearch.geometry.Polygon polygon) { + if (polygon.isEmpty() == false) { + List collector = new ArrayList<>(); + GeoPolygonDecomposer.decomposePolygon(polygon, true, collector); + collectPolygons(collector); + } + return null; + } + + @Override + public Void visit(Rectangle r) { + if (r.isEmpty() == false) { + geometries.add(GeoShapeUtils.toLuceneRectangle(r)); + } + return null; + } + + private void collectLines(List geometryLines) { + for (Line line: geometryLines) { + geometries.add(GeoShapeUtils.toLuceneLine(line)); } } - @Override - public Query visit(org.elasticsearch.geometry.Line line) { - validateIsGeoShapeFieldType(); - return LatLonShape.newLineQuery(fieldName, relation.getLuceneRelation(), new Line(line.getY(), line.getX())); - } - - @Override - public Query visit(LinearRing ring) { - throw new QueryShardException(context, "Field [" + fieldName + "] found an unsupported shape LinearRing"); - } - - @Override - public Query visit(MultiLine multiLine) { - validateIsGeoShapeFieldType(); - Line[] lines = new Line[multiLine.size()]; - for (int i = 0; i < multiLine.size(); i++) { - lines[i] = new Line(multiLine.get(i).getY(), multiLine.get(i).getX()); - } - return LatLonShape.newLineQuery(fieldName, relation.getLuceneRelation(), lines); - } - - @Override - public Query visit(MultiPoint multiPoint) { - double[][] points = new double[multiPoint.size()][2]; - for (int i = 0; i < multiPoint.size(); i++) { - points[i] = new double[] {multiPoint.get(i).getLat(), multiPoint.get(i).getLon()}; - } - return LatLonShape.newPointQuery(fieldName, relation.getLuceneRelation(), points); - } - - @Override - public Query visit(MultiPolygon multiPolygon) { - Polygon[] polygons = new Polygon[multiPolygon.size()]; - for (int i = 0; i < multiPolygon.size(); i++) { - polygons[i] = toLucenePolygon(multiPolygon.get(i)); - } - return LatLonShape.newPolygonQuery(fieldName, relation.getLuceneRelation(), polygons); - } - - @Override - public Query visit(Point point) { - validateIsGeoShapeFieldType(); - ShapeField.QueryRelation luceneRelation = relation.getLuceneRelation(); - if (luceneRelation == ShapeField.QueryRelation.CONTAINS) { - // contains and intersects are equivalent but the implementation of - // intersects is more efficient. - luceneRelation = ShapeField.QueryRelation.INTERSECTS; - } - return LatLonShape.newPointQuery(fieldName, luceneRelation, - new double[] {point.getY(), point.getX()}); - } - - @Override - public Query visit(org.elasticsearch.geometry.Polygon polygon) { - return LatLonShape.newPolygonQuery(fieldName, relation.getLuceneRelation(), toLucenePolygon(polygon)); - } - - @Override - public Query visit(Rectangle r) { - return LatLonShape.newBoxQuery(fieldName, relation.getLuceneRelation(), - r.getMinY(), r.getMaxY(), r.getMinX(), r.getMaxX()); - } - - private void validateIsGeoShapeFieldType() { - if (fieldType instanceof GeoShapeFieldMapper.GeoShapeFieldType == false) { - throw new QueryShardException(context, "Expected " + GeoShapeFieldMapper.CONTENT_TYPE - + " field type for Field [" + fieldName + "] but found " + fieldType.typeName()); + private void collectPolygons(List geometryPolygons) { + for (Polygon polygon : geometryPolygons) { + geometries.add(GeoShapeUtils.toLucenePolygon(polygon)); } } } - } diff --git a/server/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java b/server/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java index 6b78db8b8e0..893fc96f40d 100644 --- a/server/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java +++ b/server/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java @@ -27,6 +27,7 @@ import org.elasticsearch.common.CheckedSupplier; import org.elasticsearch.common.Strings; import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.geo.SpatialStrategy; +import org.elasticsearch.common.geo.builders.CircleBuilder; import org.elasticsearch.common.geo.builders.CoordinatesBuilder; import org.elasticsearch.common.geo.builders.EnvelopeBuilder; import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder; @@ -36,6 +37,7 @@ import org.elasticsearch.common.geo.builders.PointBuilder; import org.elasticsearch.common.geo.builders.PolygonBuilder; import org.elasticsearch.common.geo.builders.ShapeBuilder; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.DistanceUnit; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentParser; @@ -759,4 +761,42 @@ public class GeoShapeQueryTests extends GeoQueryTests { assertHitCount(result, 0); } + public void testDistanceQuery() throws Exception { + String mapping = Strings.toString(createRandomMapping()); + client().admin().indices().prepareCreate("test_distance").addMapping("type1", mapping, XContentType.JSON).get(); + ensureGreen(); + + CircleBuilder circleBuilder = new CircleBuilder().center(new Coordinate(1, 0)).radius(350, DistanceUnit.KILOMETERS); + + client().index(new IndexRequest("test_distance") + .source(jsonBuilder().startObject().field("geo", new PointBuilder(2, 2)).endObject()) + .setRefreshPolicy(IMMEDIATE)).actionGet(); + client().index(new IndexRequest("test_distance") + .source(jsonBuilder().startObject().field("geo", new PointBuilder(3, 1)).endObject()) + .setRefreshPolicy(IMMEDIATE)).actionGet(); + client().index(new IndexRequest("test_distance") + .source(jsonBuilder().startObject().field("geo", new PointBuilder(-20, -30)).endObject()) + .setRefreshPolicy(IMMEDIATE)).actionGet(); + client().index(new IndexRequest("test_distance") + .source(jsonBuilder().startObject().field("geo", new PointBuilder(20, 30)).endObject()) + .setRefreshPolicy(IMMEDIATE)).actionGet(); + + SearchResponse response = client().prepareSearch("test_distance") + .setQuery(QueryBuilders.geoShapeQuery("geo", circleBuilder.buildGeometry()).relation(ShapeRelation.WITHIN)) + .get(); + assertEquals(2, response.getHits().getTotalHits().value); + response = client().prepareSearch("test_distance") + .setQuery(QueryBuilders.geoShapeQuery("geo", circleBuilder.buildGeometry()).relation(ShapeRelation.INTERSECTS)) + .get(); + assertEquals(2, response.getHits().getTotalHits().value); + response = client().prepareSearch("test_distance") + .setQuery(QueryBuilders.geoShapeQuery("geo", circleBuilder.buildGeometry()).relation(ShapeRelation.DISJOINT)) + .get(); + assertEquals(2, response.getHits().getTotalHits().value); + response = client().prepareSearch("test_distance") + .setQuery(QueryBuilders.geoShapeQuery("geo", circleBuilder.buildGeometry()).relation(ShapeRelation.CONTAINS)) + .get(); + assertEquals(0, response.getHits().getTotalHits().value); + } + }