Add support for distance queries on shape queries (#53468) (#53796)

With the upgrade to Lucene 8.5, XYShape field has support for distance queries. This change implements this new feature and removes the limitation.
This commit is contained in:
Ignacio Vera 2020-03-19 15:32:09 +01:00 committed by GitHub
parent 4f1b2fd2b1
commit dfc1d79ddf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 221 additions and 152 deletions

View File

@ -6,8 +6,6 @@
package org.elasticsearch.xpack.spatial.index.mapper;
import org.apache.lucene.document.XYShape;
import org.apache.lucene.geo.XYLine;
import org.apache.lucene.geo.XYPolygon;
import org.apache.lucene.index.IndexableField;
import org.elasticsearch.geometry.Circle;
import org.elasticsearch.geometry.Geometry;
@ -75,8 +73,7 @@ public class ShapeIndexer implements AbstractGeometryFieldMapper.Indexer<Geometr
@Override
public Void visit(Line line) {
float[][] vertices = lineToFloatArray(line.getX(), line.getY());
addFields(XYShape.createIndexableFields(name, new XYLine(vertices[0], vertices[1])));
addFields(XYShape.createIndexableFields(name, ShapeUtils.toLuceneXYLine(line)));
return null;
}
@ -111,22 +108,19 @@ public class ShapeIndexer implements AbstractGeometryFieldMapper.Indexer<Geometr
@Override
public Void visit(Point point) {
addFields(XYShape.createIndexableFields(name, (float)point.getX(), (float)point.getY()));
addFields(XYShape.createIndexableFields(name, (float) point.getX(), (float) point.getY()));
return null;
}
@Override
public Void visit(Polygon polygon) {
addFields(XYShape.createIndexableFields(name, toLucenePolygon(polygon)));
addFields(XYShape.createIndexableFields(name, ShapeUtils.toLuceneXYPolygon(polygon)));
return null;
}
@Override
public Void visit(Rectangle r) {
XYPolygon p = new XYPolygon(
new float[]{(float)r.getMinX(), (float)r.getMaxX(), (float)r.getMaxX(), (float)r.getMinX(), (float)r.getMinX()},
new float[]{(float)r.getMinY(), (float)r.getMinY(), (float)r.getMaxY(), (float)r.getMaxY(), (float)r.getMinY()});
addFields(XYShape.createIndexableFields(name, p));
addFields(XYShape.createIndexableFields(name, ShapeUtils.toLuceneXYPolygon(r)));
return null;
}
@ -134,27 +128,4 @@ public class ShapeIndexer implements AbstractGeometryFieldMapper.Indexer<Geometr
this.fields.addAll(Arrays.asList(fields));
}
}
public static XYPolygon toLucenePolygon(Polygon polygon) {
XYPolygon[] holes = new XYPolygon[polygon.getNumberOfHoles()];
LinearRing ring;
float[][] vertices;
for(int i = 0; i<holes.length; i++) {
ring = polygon.getHole(i);
vertices = lineToFloatArray(ring.getX(), ring.getY());
holes[i] = new XYPolygon(vertices[0], vertices[1]);
}
ring = polygon.getPolygon();
vertices = lineToFloatArray(ring.getX(), ring.getY());
return new XYPolygon(vertices[0], vertices[1], holes);
}
private static float[][] lineToFloatArray(double[] x, double[] y) {
float[][] result = new float[2][x.length];
for (int i = 0; i < x.length; ++i) {
result[0][i] = (float)x[i];
result[1][i] = (float)y[i];
}
return result;
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.spatial.index.mapper;
import org.elasticsearch.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 ShapeUtils {
public static org.apache.lucene.geo.XYPolygon toLuceneXYPolygon(Polygon polygon) {
org.apache.lucene.geo.XYPolygon[] holes = new org.apache.lucene.geo.XYPolygon[polygon.getNumberOfHoles()];
for(int i = 0; i<holes.length; i++) {
holes[i] = new org.apache.lucene.geo.XYPolygon(
doubleArrayToFloatArray(polygon.getHole(i).getX()),
doubleArrayToFloatArray(polygon.getHole(i).getY()));
}
return new org.apache.lucene.geo.XYPolygon(
doubleArrayToFloatArray(polygon.getPolygon().getX()),
doubleArrayToFloatArray(polygon.getPolygon().getY()), holes);
}
public static org.apache.lucene.geo.XYPolygon toLuceneXYPolygon(Rectangle r) {
return new org.apache.lucene.geo.XYPolygon(
new float[]{(float) r.getMinX(), (float) r.getMaxX(), (float) r.getMaxX(), (float) r.getMinX(), (float) r.getMinX()},
new float[]{(float) r.getMinY(), (float) r.getMinY(), (float) r.getMaxY(), (float) r.getMaxY(), (float) r.getMinY()});
}
public static org.apache.lucene.geo.XYRectangle toLuceneXYRectangle(Rectangle r) {
return new org.apache.lucene.geo.XYRectangle((float) r.getMinX(), (float) r.getMaxX(),
(float) r.getMinY(), (float) r.getMaxY());
}
public static org.apache.lucene.geo.XYPoint toLuceneXYPoint(Point point) {
return new org.apache.lucene.geo.XYPoint((float) point.getX(), (float) point.getY());
}
public static org.apache.lucene.geo.XYLine toLuceneXYLine(Line line) {
return new org.apache.lucene.geo.XYLine(
doubleArrayToFloatArray(line.getX()),
doubleArrayToFloatArray(line.getY()));
}
public static org.apache.lucene.geo.XYCircle toLuceneXYCircle(Circle circle) {
return new org.apache.lucene.geo.XYCircle((float) circle.getX(), (float) circle.getY(), (float) circle.getRadiusMeters());
}
private ShapeUtils() {
}
private static float[] doubleArrayToFloatArray(double[] array) {
float[] result = new float[array.length];
for (int i = 0; i < array.length; ++i) {
result[i] = (float) array[i];
}
return result;
}
}

View File

@ -6,6 +6,7 @@
package org.elasticsearch.xpack.spatial.index.query;
import org.apache.logging.log4j.LogManager;
import org.apache.lucene.search.ConstantScoreQuery;
import org.apache.lucene.search.Query;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.geo.builders.ShapeBuilder;
@ -138,7 +139,7 @@ public class ShapeQueryBuilder extends AbstractGeometryQueryBuilder<ShapeQueryBu
}
final AbstractGeometryFieldMapper.AbstractGeometryFieldType ft = (AbstractGeometryFieldMapper.AbstractGeometryFieldType) fieldType;
return ft.geometryQueryBuilder().process(shape, ft.name(), relation, context);
return new ConstantScoreQuery(ft.geometryQueryBuilder().process(shape, ft.name(), relation, context));
}
@Override

View File

@ -5,13 +5,8 @@
*/
package org.elasticsearch.xpack.spatial.index.query;
import org.apache.lucene.document.ShapeField;
import org.apache.lucene.document.XYShape;
import org.apache.lucene.geo.XYLine;
import org.apache.lucene.geo.XYPolygon;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.ConstantScoreQuery;
import org.apache.lucene.geo.XYGeometry;
import org.apache.lucene.search.MatchNoDocsQuery;
import org.apache.lucene.search.Query;
import org.elasticsearch.Version;
@ -33,24 +28,26 @@ import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.index.query.QueryShardContext;
import org.elasticsearch.index.query.QueryShardException;
import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper;
import org.elasticsearch.xpack.spatial.index.mapper.ShapeUtils;
import java.util.ArrayList;
import java.util.List;
import static org.elasticsearch.xpack.spatial.index.mapper.ShapeIndexer.toLucenePolygon;
public class ShapeQueryProcessor implements AbstractSearchableGeometryFieldType.QueryProcessor {
@Override
public Query process(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context) {
validateIsShapeFieldType(fieldName, context);
if (shape == null) {
return new MatchNoDocsQuery();
}
// CONTAINS queries are not supported by VECTOR strategy for indices created before version 7.5.0 (Lucene 8.3.0);
if (relation == ShapeRelation.CONTAINS && context.indexVersionCreated().before(Version.V_7_5_0)) {
throw new QueryShardException(context,
ShapeRelation.CONTAINS + " query relation not supported for Field [" + fieldName + "].");
}
// wrap geometry Query as a ConstantScoreQuery
return new ConstantScoreQuery(shape.visit(new ShapeVisitor(context, fieldName, relation)));
if (shape == null) {
return new MatchNoDocsQuery();
}
return getVectorQueryFromShape(shape, fieldName, relation, context);
}
private void validateIsShapeFieldType(String fieldName, QueryShardContext context) {
@ -61,115 +58,107 @@ public class ShapeQueryProcessor implements AbstractSearchableGeometryFieldType.
}
}
private class ShapeVisitor implements GeometryVisitor<Query, RuntimeException> {
QueryShardContext context;
String fieldName;
ShapeRelation relation;
ShapeVisitor(QueryShardContext context, String fieldName, ShapeRelation relation) {
this.context = context;
this.fieldName = fieldName;
this.relation = relation;
}
@Override
public Query visit(Circle circle) {
throw new QueryShardException(context, "Field [" + fieldName + "] found and 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;
}
for (Geometry shape : collection) {
bqb.add(shape.visit(this), occur);
}
}
@Override
public Query visit(Line line) {
return XYShape.newLineQuery(fieldName, relation.getLuceneRelation(),
new XYLine(doubleArrayToFloatArray(line.getX()), doubleArrayToFloatArray(line.getY())));
}
@Override
public Query visit(LinearRing ring) {
throw new QueryShardException(context, "Field [" + fieldName + "] found and unsupported shape LinearRing");
}
@Override
public Query visit(MultiLine multiLine) {
XYLine[] lines = new XYLine[multiLine.size()];
for (int i=0; i<multiLine.size(); i++) {
lines[i] = new XYLine(doubleArrayToFloatArray(multiLine.get(i).getX()),
doubleArrayToFloatArray(multiLine.get(i).getY()));
}
return XYShape.newLineQuery(fieldName, relation.getLuceneRelation(), lines);
}
@Override
public Query visit(MultiPoint multiPoint) {
float[][] points = new float[multiPoint.size()][2];
for (int i = 0; i < multiPoint.size(); i++) {
points[i] = new float[] {(float) multiPoint.get(i).getX(), (float) multiPoint.get(i).getY()};
}
return XYShape.newPointQuery(fieldName, relation.getLuceneRelation(), points);
}
@Override
public Query visit(MultiPolygon multiPolygon) {
XYPolygon[] polygons = new XYPolygon[multiPolygon.size()];
for (int i=0; i<multiPolygon.size(); i++) {
polygons[i] = toLucenePolygon(multiPolygon.get(i));
}
return visitMultiPolygon(polygons);
}
private Query visitMultiPolygon(XYPolygon... polygons) {
return XYShape.newPolygonQuery(fieldName, relation.getLuceneRelation(), polygons);
}
@Override
public Query visit(Point point) {
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;
}
float[][] pointArray = new float[][] {{(float)point.getX(), (float)point.getY()}};
return XYShape.newPointQuery(fieldName, luceneRelation, pointArray);
}
@Override
public Query visit(Polygon polygon) {
return XYShape.newPolygonQuery(fieldName, relation.getLuceneRelation(), toLucenePolygon(polygon));
}
@Override
public Query visit(Rectangle r) {
return XYShape.newBoxQuery(fieldName, relation.getLuceneRelation(),
(float)r.getMinX(), (float)r.getMaxX(), (float)r.getMinY(), (float)r.getMaxY());
private Query getVectorQueryFromShape(Geometry queryShape, String fieldName, ShapeRelation relation, QueryShardContext context) {
final LuceneGeometryCollector visitor = new LuceneGeometryCollector(fieldName, context);
queryShape.visit(visitor);
final List<XYGeometry> geometries = visitor.geometries();
if (geometries.size() == 0) {
return new MatchNoDocsQuery();
}
return XYShape.newGeometryQuery(fieldName, relation.getLuceneRelation(),
geometries.toArray(new XYGeometry[geometries.size()]));
}
private static float[] doubleArrayToFloatArray(double[] array) {
float[] result = new float[array.length];
for (int i = 0; i < array.length; ++i) {
result[i] = (float) array[i];
private static class LuceneGeometryCollector implements GeometryVisitor<Void, RuntimeException> {
private final List<XYGeometry> geometries = new ArrayList<>();
private final String name;
private final QueryShardContext context;
private LuceneGeometryCollector(String name, QueryShardContext context) {
this.name = name;
this.context = context;
}
List<XYGeometry> geometries() {
return geometries;
}
@Override
public Void visit(Circle circle) {
if (circle.isEmpty() == false) {
geometries.add(ShapeUtils.toLuceneXYCircle(circle));
}
return null;
}
@Override
public Void visit(GeometryCollection<?> collection) {
for (Geometry shape : collection) {
shape.visit(this);
}
return null;
}
@Override
public Void visit(Line line) {
if (line.isEmpty() == false) {
geometries.add(ShapeUtils.toLuceneXYLine(line));
}
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) {
for (Line line : multiLine) {
visit(line);
}
return null;
}
@Override
public Void visit(MultiPoint multiPoint) {
for (Point point : multiPoint) {
visit(point);
}
return null;
}
@Override
public Void visit(MultiPolygon multiPolygon) {
for (Polygon polygon : multiPolygon) {
visit(polygon);
}
return null;
}
@Override
public Void visit(Point point) {
if (point.isEmpty() == false) {
geometries.add(ShapeUtils.toLuceneXYPoint(point));
}
return null;
}
@Override
public Void visit(Polygon polygon) {
if (polygon.isEmpty() == false) {
geometries.add(ShapeUtils.toLuceneXYPolygon(polygon));
}
return null;
}
@Override
public Void visit(Rectangle r) {
if (r.isEmpty() == false) {
geometries.add(ShapeUtils.toLuceneXYRectangle(r));
}
return null;
}
return result;
}
}

View File

@ -10,11 +10,13 @@ import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.geo.GeoJson;
import org.elasticsearch.common.geo.ShapeRelation;
import org.elasticsearch.common.geo.builders.CircleBuilder;
import org.elasticsearch.common.geo.builders.EnvelopeBuilder;
import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder;
import org.elasticsearch.common.geo.builders.MultiPointBuilder;
import org.elasticsearch.common.geo.builders.PointBuilder;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
@ -337,4 +339,42 @@ public class ShapeQueryTests extends ESSingleNodeTestCase {
assertEquals(1, response.getHits().getTotalHits().value);
}
}
public void testDistanceQuery() throws Exception {
client().admin().indices().prepareCreate("test_distance").addMapping("type", "location", "type=shape")
.execute().actionGet();
ensureGreen();
CircleBuilder circleBuilder = new CircleBuilder().center(new Coordinate(1, 0)).radius(10, DistanceUnit.METERS);
client().index(new IndexRequest("test_distance")
.source(jsonBuilder().startObject().field("location", new PointBuilder(2, 2)).endObject())
.setRefreshPolicy(IMMEDIATE)).actionGet();
client().index(new IndexRequest("test_distance")
.source(jsonBuilder().startObject().field("location", new PointBuilder(3, 1)).endObject())
.setRefreshPolicy(IMMEDIATE)).actionGet();
client().index(new IndexRequest("test_distance")
.source(jsonBuilder().startObject().field("location", new PointBuilder(-20, -30)).endObject())
.setRefreshPolicy(IMMEDIATE)).actionGet();
client().index(new IndexRequest("test_distance")
.source(jsonBuilder().startObject().field("location", new PointBuilder(20, 30)).endObject())
.setRefreshPolicy(IMMEDIATE)).actionGet();
SearchResponse response = client().prepareSearch("test_distance")
.setQuery(new ShapeQueryBuilder("location", circleBuilder.buildGeometry()).relation(ShapeRelation.WITHIN))
.get();
assertEquals(2, response.getHits().getTotalHits().value);
response = client().prepareSearch("test_distance")
.setQuery(new ShapeQueryBuilder("location", circleBuilder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
.get();
assertEquals(2, response.getHits().getTotalHits().value);
response = client().prepareSearch("test_distance")
.setQuery(new ShapeQueryBuilder("location", circleBuilder.buildGeometry()).relation(ShapeRelation.DISJOINT))
.get();
assertEquals(2, response.getHits().getTotalHits().value);
response = client().prepareSearch("test_distance")
.setQuery(new ShapeQueryBuilder("location", circleBuilder.buildGeometry()).relation(ShapeRelation.CONTAINS))
.get();
assertEquals(0, response.getHits().getTotalHits().value);
}
}