Skip to content

Commit c00fc36

Browse files
committed
Add support for distance queries on shape queries (elastic#53468)
With the upgrade to Lucene 8.5, XYShape field has support for distance queries. This change implements this new feature and removes the limitation.
1 parent b0884ba commit c00fc36

File tree

5 files changed

+186
-117
lines changed

5 files changed

+186
-117
lines changed

x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/ShapeIndexer.java

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
package org.elasticsearch.xpack.spatial.index.mapper;
77

88
import org.apache.lucene.document.XYShape;
9-
import org.apache.lucene.geo.XYLine;
10-
import org.apache.lucene.geo.XYPolygon;
119
import org.apache.lucene.index.IndexableField;
1210
import org.elasticsearch.geometry.Circle;
1311
import org.elasticsearch.geometry.Geometry;
@@ -75,8 +73,7 @@ public Void visit(GeometryCollection<?> collection) {
7573

7674
@Override
7775
public Void visit(Line line) {
78-
float[][] vertices = lineToFloatArray(line.getX(), line.getY());
79-
addFields(XYShape.createIndexableFields(name, new XYLine(vertices[0], vertices[1])));
76+
addFields(XYShape.createIndexableFields(name, ShapeUtils.toLuceneXYLine(line)));
8077
return null;
8178
}
8279

@@ -111,50 +108,24 @@ public Void visit(MultiPolygon multiPolygon) {
111108

112109
@Override
113110
public Void visit(Point point) {
114-
addFields(XYShape.createIndexableFields(name, (float)point.getX(), (float)point.getY()));
111+
addFields(XYShape.createIndexableFields(name, (float) point.getX(), (float) point.getY()));
115112
return null;
116113
}
117114

118115
@Override
119116
public Void visit(Polygon polygon) {
120-
addFields(XYShape.createIndexableFields(name, toLucenePolygon(polygon)));
117+
addFields(XYShape.createIndexableFields(name, ShapeUtils.toLuceneXYPolygon(polygon)));
121118
return null;
122119
}
123120

124121
@Override
125122
public Void visit(Rectangle r) {
126-
XYPolygon p = new XYPolygon(
127-
new float[]{(float)r.getMinX(), (float)r.getMaxX(), (float)r.getMaxX(), (float)r.getMinX(), (float)r.getMinX()},
128-
new float[]{(float)r.getMinY(), (float)r.getMinY(), (float)r.getMaxY(), (float)r.getMaxY(), (float)r.getMinY()});
129-
addFields(XYShape.createIndexableFields(name, p));
123+
addFields(XYShape.createIndexableFields(name, ShapeUtils.toLuceneXYPolygon(r)));
130124
return null;
131125
}
132126

133127
private void addFields(IndexableField[] fields) {
134128
this.fields.addAll(Arrays.asList(fields));
135129
}
136130
}
137-
138-
public static XYPolygon toLucenePolygon(Polygon polygon) {
139-
XYPolygon[] holes = new XYPolygon[polygon.getNumberOfHoles()];
140-
LinearRing ring;
141-
float[][] vertices;
142-
for(int i = 0; i<holes.length; i++) {
143-
ring = polygon.getHole(i);
144-
vertices = lineToFloatArray(ring.getX(), ring.getY());
145-
holes[i] = new XYPolygon(vertices[0], vertices[1]);
146-
}
147-
ring = polygon.getPolygon();
148-
vertices = lineToFloatArray(ring.getX(), ring.getY());
149-
return new XYPolygon(vertices[0], vertices[1], holes);
150-
}
151-
152-
private static float[][] lineToFloatArray(double[] x, double[] y) {
153-
float[][] result = new float[2][x.length];
154-
for (int i = 0; i < x.length; ++i) {
155-
result[0][i] = (float)x[i];
156-
result[1][i] = (float)y[i];
157-
}
158-
return result;
159-
}
160131
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.spatial.index.mapper;
7+
8+
import org.elasticsearch.geometry.Circle;
9+
import org.elasticsearch.geometry.Line;
10+
import org.elasticsearch.geometry.Point;
11+
import org.elasticsearch.geometry.Polygon;
12+
import org.elasticsearch.geometry.Rectangle;
13+
14+
15+
/**
16+
* Utility class that transforms Elasticsearch geometry objects to the Lucene representation
17+
*/
18+
public class ShapeUtils {
19+
20+
public static org.apache.lucene.geo.XYPolygon toLuceneXYPolygon(Polygon polygon) {
21+
org.apache.lucene.geo.XYPolygon[] holes = new org.apache.lucene.geo.XYPolygon[polygon.getNumberOfHoles()];
22+
for(int i = 0; i<holes.length; i++) {
23+
holes[i] = new org.apache.lucene.geo.XYPolygon(
24+
doubleArrayToFloatArray(polygon.getHole(i).getX()),
25+
doubleArrayToFloatArray(polygon.getHole(i).getY()));
26+
}
27+
return new org.apache.lucene.geo.XYPolygon(
28+
doubleArrayToFloatArray(polygon.getPolygon().getX()),
29+
doubleArrayToFloatArray(polygon.getPolygon().getY()), holes);
30+
}
31+
32+
public static org.apache.lucene.geo.XYPolygon toLuceneXYPolygon(Rectangle r) {
33+
return new org.apache.lucene.geo.XYPolygon(
34+
new float[]{(float) r.getMinX(), (float) r.getMaxX(), (float) r.getMaxX(), (float) r.getMinX(), (float) r.getMinX()},
35+
new float[]{(float) r.getMinY(), (float) r.getMinY(), (float) r.getMaxY(), (float) r.getMaxY(), (float) r.getMinY()});
36+
}
37+
38+
public static org.apache.lucene.geo.XYRectangle toLuceneXYRectangle(Rectangle r) {
39+
return new org.apache.lucene.geo.XYRectangle((float) r.getMinX(), (float) r.getMaxX(),
40+
(float) r.getMinY(), (float) r.getMaxY());
41+
}
42+
43+
public static org.apache.lucene.geo.XYPoint toLuceneXYPoint(Point point) {
44+
return new org.apache.lucene.geo.XYPoint((float) point.getX(), (float) point.getY());
45+
}
46+
47+
public static org.apache.lucene.geo.XYLine toLuceneXYLine(Line line) {
48+
return new org.apache.lucene.geo.XYLine(
49+
doubleArrayToFloatArray(line.getX()),
50+
doubleArrayToFloatArray(line.getY()));
51+
}
52+
53+
public static org.apache.lucene.geo.XYCircle toLuceneXYCircle(Circle circle) {
54+
return new org.apache.lucene.geo.XYCircle((float) circle.getX(), (float) circle.getY(), (float) circle.getRadiusMeters());
55+
}
56+
57+
private ShapeUtils() {
58+
}
59+
60+
private static float[] doubleArrayToFloatArray(double[] array) {
61+
float[] result = new float[array.length];
62+
for (int i = 0; i < array.length; ++i) {
63+
result[i] = (float) array[i];
64+
}
65+
return result;
66+
}
67+
68+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package org.elasticsearch.xpack.spatial.index.query;
77

88
import org.apache.logging.log4j.LogManager;
9+
import org.apache.lucene.search.ConstantScoreQuery;
910
import org.apache.lucene.search.Query;
1011
import org.elasticsearch.common.Nullable;
1112
import org.elasticsearch.common.geo.builders.ShapeBuilder;
@@ -138,7 +139,7 @@ public Query buildShapeQuery(QueryShardContext context, MappedFieldType fieldTyp
138139
}
139140

140141
final AbstractGeometryFieldMapper.AbstractGeometryFieldType ft = (AbstractGeometryFieldMapper.AbstractGeometryFieldType) fieldType;
141-
return ft.geometryQueryBuilder().process(shape, ft.name(), relation, context);
142+
return new ConstantScoreQuery(ft.geometryQueryBuilder().process(shape, ft.name(), relation, context));
142143
}
143144

144145
@Override

x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java

Lines changed: 72 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,8 @@
55
*/
66
package org.elasticsearch.xpack.spatial.index.query;
77

8-
import org.apache.lucene.document.ShapeField;
98
import org.apache.lucene.document.XYShape;
10-
import org.apache.lucene.geo.XYLine;
11-
import org.apache.lucene.geo.XYPolygon;
12-
import org.apache.lucene.search.BooleanClause;
13-
import org.apache.lucene.search.BooleanQuery;
14-
import org.apache.lucene.search.ConstantScoreQuery;
9+
import org.apache.lucene.geo.XYGeometry;
1510
import org.apache.lucene.search.MatchNoDocsQuery;
1611
import org.apache.lucene.search.Query;
1712
import org.elasticsearch.Version;
@@ -33,24 +28,26 @@
3328
import org.elasticsearch.index.query.QueryShardContext;
3429
import org.elasticsearch.index.query.QueryShardException;
3530
import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper;
31+
import org.elasticsearch.xpack.spatial.index.mapper.ShapeUtils;
32+
33+
import java.util.ArrayList;
34+
import java.util.List;
3635

37-
import static org.elasticsearch.xpack.spatial.index.mapper.ShapeIndexer.toLucenePolygon;
3836

3937
public class ShapeQueryProcessor implements AbstractSearchableGeometryFieldType.QueryProcessor {
4038

4139
@Override
4240
public Query process(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context) {
4341
validateIsShapeFieldType(fieldName, context);
44-
if (shape == null) {
45-
return new MatchNoDocsQuery();
46-
}
4742
// CONTAINS queries are not supported by VECTOR strategy for indices created before version 7.5.0 (Lucene 8.3.0);
4843
if (relation == ShapeRelation.CONTAINS && context.indexVersionCreated().before(Version.V_7_5_0)) {
4944
throw new QueryShardException(context,
5045
ShapeRelation.CONTAINS + " query relation not supported for Field [" + fieldName + "].");
5146
}
52-
// wrap geometry Query as a ConstantScoreQuery
53-
return new ConstantScoreQuery(shape.visit(new ShapeVisitor(context, fieldName, relation)));
47+
if (shape == null) {
48+
return new MatchNoDocsQuery();
49+
}
50+
return getVectorQueryFromShape(shape, fieldName, relation, context);
5451
}
5552

5653
private void validateIsShapeFieldType(String fieldName, QueryShardContext context) {
@@ -61,115 +58,107 @@ private void validateIsShapeFieldType(String fieldName, QueryShardContext contex
6158
}
6259
}
6360

64-
private class ShapeVisitor implements GeometryVisitor<Query, RuntimeException> {
65-
QueryShardContext context;
66-
String fieldName;
67-
ShapeRelation relation;
61+
private Query getVectorQueryFromShape(Geometry queryShape, String fieldName, ShapeRelation relation, QueryShardContext context) {
62+
final LuceneGeometryCollector visitor = new LuceneGeometryCollector(fieldName, context);
63+
queryShape.visit(visitor);
64+
final List<XYGeometry> geometries = visitor.geometries();
65+
if (geometries.size() == 0) {
66+
return new MatchNoDocsQuery();
67+
}
68+
return XYShape.newGeometryQuery(fieldName, relation.getLuceneRelation(),
69+
geometries.toArray(new XYGeometry[geometries.size()]));
70+
}
71+
72+
private static class LuceneGeometryCollector implements GeometryVisitor<Void, RuntimeException> {
73+
private final List<XYGeometry> geometries = new ArrayList<>();
74+
private final String name;
75+
private final QueryShardContext context;
6876

69-
ShapeVisitor(QueryShardContext context, String fieldName, ShapeRelation relation) {
77+
private LuceneGeometryCollector(String name, QueryShardContext context) {
78+
this.name = name;
7079
this.context = context;
71-
this.fieldName = fieldName;
72-
this.relation = relation;
7380
}
7481

75-
@Override
76-
public Query visit(Circle circle) {
77-
throw new QueryShardException(context, "Field [" + fieldName + "] found and unknown shape Circle");
82+
List<XYGeometry> geometries() {
83+
return geometries;
7884
}
7985

8086
@Override
81-
public Query visit(GeometryCollection<?> collection) {
82-
BooleanQuery.Builder bqb = new BooleanQuery.Builder();
83-
visit(bqb, collection);
84-
return bqb.build();
85-
}
86-
87-
private void visit(BooleanQuery.Builder bqb, GeometryCollection<?> collection) {
88-
BooleanClause.Occur occur;
89-
if (relation == ShapeRelation.CONTAINS || relation == ShapeRelation.DISJOINT) {
90-
// all shapes must be disjoint / must be contained in relation to the indexed shape.
91-
occur = BooleanClause.Occur.MUST;
92-
} else {
93-
// at least one shape must intersect / contain the indexed shape.
94-
occur = BooleanClause.Occur.SHOULD;
87+
public Void visit(Circle circle) {
88+
if (circle.isEmpty() == false) {
89+
geometries.add(ShapeUtils.toLuceneXYCircle(circle));
9590
}
91+
return null;
92+
}
93+
94+
@Override
95+
public Void visit(GeometryCollection<?> collection) {
9696
for (Geometry shape : collection) {
97-
bqb.add(shape.visit(this), occur);
97+
shape.visit(this);
9898
}
99+
return null;
99100
}
100101

101102
@Override
102-
public Query visit(Line line) {
103-
return XYShape.newLineQuery(fieldName, relation.getLuceneRelation(),
104-
new XYLine(doubleArrayToFloatArray(line.getX()), doubleArrayToFloatArray(line.getY())));
103+
public Void visit(Line line) {
104+
if (line.isEmpty() == false) {
105+
geometries.add(ShapeUtils.toLuceneXYLine(line));
106+
}
107+
return null;
105108
}
106109

107110
@Override
108-
public Query visit(LinearRing ring) {
109-
throw new QueryShardException(context, "Field [" + fieldName + "] found and unsupported shape LinearRing");
111+
public Void visit(LinearRing ring) {
112+
throw new QueryShardException(context, "Field [" + name + "] found and unsupported shape LinearRing");
110113
}
111114

112115
@Override
113-
public Query visit(MultiLine multiLine) {
114-
XYLine[] lines = new XYLine[multiLine.size()];
115-
for (int i=0; i<multiLine.size(); i++) {
116-
lines[i] = new XYLine(doubleArrayToFloatArray(multiLine.get(i).getX()),
117-
doubleArrayToFloatArray(multiLine.get(i).getY()));
116+
public Void visit(MultiLine multiLine) {
117+
for (Line line : multiLine) {
118+
visit(line);
118119
}
119-
return XYShape.newLineQuery(fieldName, relation.getLuceneRelation(), lines);
120+
return null;
120121
}
121122

122123
@Override
123-
public Query visit(MultiPoint multiPoint) {
124-
float[][] points = new float[multiPoint.size()][2];
125-
for (int i = 0; i < multiPoint.size(); i++) {
126-
points[i] = new float[] {(float) multiPoint.get(i).getX(), (float) multiPoint.get(i).getY()};
124+
public Void visit(MultiPoint multiPoint) {
125+
for (Point point : multiPoint) {
126+
visit(point);
127127
}
128-
return XYShape.newPointQuery(fieldName, relation.getLuceneRelation(), points);
128+
return null;
129129
}
130130

131131
@Override
132-
public Query visit(MultiPolygon multiPolygon) {
133-
XYPolygon[] polygons = new XYPolygon[multiPolygon.size()];
134-
for (int i=0; i<multiPolygon.size(); i++) {
135-
polygons[i] = toLucenePolygon(multiPolygon.get(i));
132+
public Void visit(MultiPolygon multiPolygon) {
133+
for (Polygon polygon : multiPolygon) {
134+
visit(polygon);
136135
}
137-
return visitMultiPolygon(polygons);
138-
}
139-
140-
private Query visitMultiPolygon(XYPolygon... polygons) {
141-
return XYShape.newPolygonQuery(fieldName, relation.getLuceneRelation(), polygons);
136+
return null;
142137
}
143138

144139
@Override
145-
public Query visit(Point point) {
146-
ShapeField.QueryRelation luceneRelation = relation.getLuceneRelation();
147-
if (luceneRelation == ShapeField.QueryRelation.CONTAINS) {
148-
// contains and intersects are equivalent but the implementation of
149-
// intersects is more efficient.
150-
luceneRelation = ShapeField.QueryRelation.INTERSECTS;
140+
public Void visit(Point point) {
141+
if (point.isEmpty() == false) {
142+
geometries.add(ShapeUtils.toLuceneXYPoint(point));
151143
}
152-
float[][] pointArray = new float[][] {{(float)point.getX(), (float)point.getY()}};
153-
return XYShape.newPointQuery(fieldName, luceneRelation, pointArray);
154-
}
144+
return null;
155145

156-
@Override
157-
public Query visit(Polygon polygon) {
158-
return XYShape.newPolygonQuery(fieldName, relation.getLuceneRelation(), toLucenePolygon(polygon));
159146
}
160147

161148
@Override
162-
public Query visit(Rectangle r) {
163-
return XYShape.newBoxQuery(fieldName, relation.getLuceneRelation(),
164-
(float)r.getMinX(), (float)r.getMaxX(), (float)r.getMinY(), (float)r.getMaxY());
149+
public Void visit(Polygon polygon) {
150+
if (polygon.isEmpty() == false) {
151+
geometries.add(ShapeUtils.toLuceneXYPolygon(polygon));
152+
}
153+
return null;
165154
}
166-
}
167155

168-
private static float[] doubleArrayToFloatArray(double[] array) {
169-
float[] result = new float[array.length];
170-
for (int i = 0; i < array.length; ++i) {
171-
result[i] = (float) array[i];
156+
@Override
157+
public Void visit(Rectangle r) {
158+
if (r.isEmpty() == false) {
159+
geometries.add(ShapeUtils.toLuceneXYRectangle(r));
160+
}
161+
return null;
172162
}
173-
return result;
174163
}
175164
}

0 commit comments

Comments
 (0)