Skip to content

Commit 4f1b2fd

Browse files
authored
Add support for distance queries on geo_shape queries (#53466) (#53795)
With the upgrade to Lucene 8.5, LatLonShape field has support for distance queries. This change implements this new feature and removes the limitation.
1 parent b0884ba commit 4f1b2fd

File tree

5 files changed

+197
-101
lines changed

5 files changed

+197
-101
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.common.geo;
20+
21+
import org.elasticsearch.geometry.Circle;
22+
import org.elasticsearch.geometry.Line;
23+
import org.elasticsearch.geometry.Point;
24+
import org.elasticsearch.geometry.Polygon;
25+
import org.elasticsearch.geometry.Rectangle;
26+
27+
28+
/**
29+
* Utility class that transforms Elasticsearch geometry objects to the Lucene representation
30+
*/
31+
public class GeoShapeUtils {
32+
33+
public static org.apache.lucene.geo.Polygon toLucenePolygon(Polygon polygon) {
34+
org.apache.lucene.geo.Polygon[] holes = new org.apache.lucene.geo.Polygon[polygon.getNumberOfHoles()];
35+
for(int i = 0; i<holes.length; i++) {
36+
holes[i] = new org.apache.lucene.geo.Polygon(polygon.getHole(i).getY(), polygon.getHole(i).getX());
37+
}
38+
return new org.apache.lucene.geo.Polygon(polygon.getPolygon().getY(), polygon.getPolygon().getX(), holes);
39+
}
40+
41+
public static org.apache.lucene.geo.Polygon toLucenePolygon(Rectangle r) {
42+
return new org.apache.lucene.geo.Polygon(
43+
new double[]{r.getMinLat(), r.getMinLat(), r.getMaxLat(), r.getMaxLat(), r.getMinLat()},
44+
new double[]{r.getMinLon(), r.getMaxLon(), r.getMaxLon(), r.getMinLon(), r.getMinLon()});
45+
}
46+
47+
public static org.apache.lucene.geo.Rectangle toLuceneRectangle(Rectangle r) {
48+
return new org.apache.lucene.geo.Rectangle(r.getMinLat(), r.getMaxLat(), r.getMinLon(), r.getMaxLon());
49+
}
50+
51+
public static org.apache.lucene.geo.Point toLucenePoint(Point point) {
52+
return new org.apache.lucene.geo.Point(point.getLat(), point.getLon());
53+
}
54+
55+
public static org.apache.lucene.geo.Line toLuceneLine(Line line) {
56+
return new org.apache.lucene.geo.Line(line.getLats(), line.getLons());
57+
}
58+
59+
public static org.apache.lucene.geo.Circle toLuceneCircle(Circle circle) {
60+
return new org.apache.lucene.geo.Circle(circle.getLat(), circle.getLon(), circle.getRadiusMeters());
61+
}
62+
63+
private GeoShapeUtils() {
64+
}
65+
66+
}

server/src/main/java/org/elasticsearch/index/mapper/GeoShapeIndexer.java

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.apache.lucene.index.IndexableField;
2525
import org.elasticsearch.common.geo.GeoLineDecomposer;
2626
import org.elasticsearch.common.geo.GeoPolygonDecomposer;
27+
import org.elasticsearch.common.geo.GeoShapeUtils;
2728
import org.elasticsearch.common.geo.GeoShapeType;
2829
import org.elasticsearch.geometry.Circle;
2930
import org.elasticsearch.geometry.Geometry;
@@ -213,7 +214,7 @@ public Void visit(GeometryCollection<?> collection) {
213214

214215
@Override
215216
public Void visit(Line line) {
216-
addFields(LatLonShape.createIndexableFields(name, new org.apache.lucene.geo.Line(line.getY(), line.getX())));
217+
addFields(LatLonShape.createIndexableFields(name, GeoShapeUtils.toLuceneLine(line)));
217218
return null;
218219
}
219220

@@ -254,16 +255,13 @@ public Void visit(Point point) {
254255

255256
@Override
256257
public Void visit(Polygon polygon) {
257-
addFields(LatLonShape.createIndexableFields(name, toLucenePolygon(polygon)));
258+
addFields(LatLonShape.createIndexableFields(name, GeoShapeUtils.toLucenePolygon(polygon)));
258259
return null;
259260
}
260261

261262
@Override
262263
public Void visit(Rectangle r) {
263-
org.apache.lucene.geo.Polygon p = new org.apache.lucene.geo.Polygon(
264-
new double[]{r.getMinY(), r.getMinY(), r.getMaxY(), r.getMaxY(), r.getMinY()},
265-
new double[]{r.getMinX(), r.getMaxX(), r.getMaxX(), r.getMinX(), r.getMinX()});
266-
addFields(LatLonShape.createIndexableFields(name, p));
264+
addFields(LatLonShape.createIndexableFields(name, GeoShapeUtils.toLucenePolygon(r)));
267265
return null;
268266
}
269267

@@ -272,11 +270,4 @@ private void addFields(IndexableField[] fields) {
272270
}
273271
}
274272

275-
public static org.apache.lucene.geo.Polygon toLucenePolygon(Polygon polygon) {
276-
org.apache.lucene.geo.Polygon[] holes = new org.apache.lucene.geo.Polygon[polygon.getNumberOfHoles()];
277-
for(int i = 0; i<holes.length; i++) {
278-
holes[i] = new org.apache.lucene.geo.Polygon(polygon.getHole(i).getY(), polygon.getHole(i).getX());
279-
}
280-
return new org.apache.lucene.geo.Polygon(polygon.getPolygon().getY(), polygon.getPolygon().getX(), holes);
281-
}
282273
}

server/src/main/java/org/elasticsearch/index/query/VectorGeoPointShapeQueryProcessor.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.apache.lucene.search.Query;
2828
import org.elasticsearch.common.geo.GeoPolygonDecomposer;
2929
import org.elasticsearch.common.geo.GeoShapeType;
30+
import org.elasticsearch.common.geo.GeoShapeUtils;
3031
import org.elasticsearch.common.geo.ShapeRelation;
3132
import org.elasticsearch.geometry.Circle;
3233
import org.elasticsearch.geometry.Geometry;
@@ -46,8 +47,6 @@
4647

4748
import java.util.ArrayList;
4849

49-
import static org.elasticsearch.index.mapper.GeoShapeIndexer.toLucenePolygon;
50-
5150
public class VectorGeoPointShapeQueryProcessor implements AbstractSearchableGeometryFieldType.QueryProcessor {
5251

5352
@Override
@@ -145,7 +144,7 @@ private Query visit(ArrayList<Polygon> collector) {
145144
org.apache.lucene.geo.Polygon[] lucenePolygons =
146145
new org.apache.lucene.geo.Polygon[collector.size()];
147146
for (int i = 0; i < collector.size(); i++) {
148-
lucenePolygons[i] = toLucenePolygon(collector.get(i));
147+
lucenePolygons[i] = GeoShapeUtils.toLucenePolygon(collector.get(i));
149148
}
150149
Query query = LatLonPoint.newPolygonQuery(fieldName, lucenePolygons);
151150
if (fieldType.hasDocValues()) {

server/src/main/java/org/elasticsearch/index/query/VectorGeoShapeQueryProcessor.java

Lines changed: 85 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,32 @@
2020
package org.elasticsearch.index.query;
2121

2222
import org.apache.lucene.document.LatLonShape;
23-
import org.apache.lucene.document.ShapeField;
24-
import org.apache.lucene.geo.Line;
25-
import org.apache.lucene.geo.Polygon;
26-
import org.apache.lucene.search.BooleanClause;
27-
import org.apache.lucene.search.BooleanQuery;
23+
import org.apache.lucene.geo.LatLonGeometry;
2824
import org.apache.lucene.search.MatchNoDocsQuery;
2925
import org.apache.lucene.search.Query;
3026
import org.elasticsearch.Version;
27+
import org.elasticsearch.common.geo.GeoLineDecomposer;
28+
import org.elasticsearch.common.geo.GeoPolygonDecomposer;
29+
import org.elasticsearch.common.geo.GeoShapeUtils;
3130
import org.elasticsearch.common.geo.ShapeRelation;
3231
import org.elasticsearch.geometry.Circle;
3332
import org.elasticsearch.geometry.Geometry;
3433
import org.elasticsearch.geometry.GeometryCollection;
3534
import org.elasticsearch.geometry.GeometryVisitor;
35+
import org.elasticsearch.geometry.Line;
3636
import org.elasticsearch.geometry.LinearRing;
3737
import org.elasticsearch.geometry.MultiLine;
3838
import org.elasticsearch.geometry.MultiPoint;
3939
import org.elasticsearch.geometry.MultiPolygon;
4040
import org.elasticsearch.geometry.Point;
41+
import org.elasticsearch.geometry.Polygon;
4142
import org.elasticsearch.geometry.Rectangle;
4243
import org.elasticsearch.index.mapper.AbstractSearchableGeometryFieldType;
43-
import org.elasticsearch.index.mapper.GeoShapeFieldMapper;
44-
import org.elasticsearch.index.mapper.GeoShapeIndexer;
45-
import org.elasticsearch.index.mapper.MappedFieldType;
4644

47-
import static org.elasticsearch.index.mapper.GeoShapeIndexer.toLucenePolygon;
45+
46+
import java.util.ArrayList;
47+
import java.util.List;
48+
4849

4950
public class VectorGeoShapeQueryProcessor implements AbstractSearchableGeometryFieldType.QueryProcessor {
5051

@@ -59,127 +60,126 @@ public Query process(Geometry shape, String fieldName, ShapeRelation relation, Q
5960
return getVectorQueryFromShape(shape, fieldName, relation, context);
6061
}
6162

62-
protected Query getVectorQueryFromShape(
63-
Geometry queryShape, String fieldName, ShapeRelation relation, QueryShardContext context) {
64-
GeoShapeIndexer geometryIndexer = new GeoShapeIndexer(true, fieldName);
65-
66-
Geometry processedShape = geometryIndexer.prepareForIndexing(queryShape);
67-
68-
if (processedShape == null) {
63+
private Query getVectorQueryFromShape(Geometry queryShape, String fieldName, ShapeRelation relation, QueryShardContext context) {
64+
final LuceneGeometryCollector visitor = new LuceneGeometryCollector(fieldName, context);
65+
queryShape.visit(visitor);
66+
final List<LatLonGeometry> geometries = visitor.geometries();
67+
if (geometries.size() == 0) {
6968
return new MatchNoDocsQuery();
7069
}
71-
return processedShape.visit(new ShapeVisitor(context, fieldName, relation));
70+
return LatLonShape.newGeometryQuery(fieldName, relation.getLuceneRelation(),
71+
geometries.toArray(new LatLonGeometry[geometries.size()]));
7272
}
7373

74-
private class ShapeVisitor implements GeometryVisitor<Query, RuntimeException> {
75-
QueryShardContext context;
76-
MappedFieldType fieldType;
77-
String fieldName;
78-
ShapeRelation relation;
74+
private static class LuceneGeometryCollector implements GeometryVisitor<Void, RuntimeException> {
75+
private final List<LatLonGeometry> geometries = new ArrayList<>();
76+
private final String name;
77+
private final QueryShardContext context;
7978

80-
ShapeVisitor(QueryShardContext context, String fieldName, ShapeRelation relation) {
79+
private LuceneGeometryCollector(String name, QueryShardContext context) {
80+
this.name = name;
8181
this.context = context;
82-
this.fieldType = context.fieldMapper(fieldName);
83-
this.fieldName = fieldName;
84-
this.relation = relation;
8582
}
8683

87-
@Override
88-
public Query visit(Circle circle) {
89-
throw new QueryShardException(context, "Field [" + fieldName + "] found an unknown shape Circle");
84+
List<LatLonGeometry> geometries() {
85+
return geometries;
9086
}
9187

9288
@Override
93-
public Query visit(GeometryCollection<?> collection) {
94-
BooleanQuery.Builder bqb = new BooleanQuery.Builder();
95-
visit(bqb, collection);
96-
return bqb.build();
97-
}
98-
99-
private void visit(BooleanQuery.Builder bqb, GeometryCollection<?> collection) {
100-
BooleanClause.Occur occur;
101-
if (relation == ShapeRelation.CONTAINS || relation == ShapeRelation.DISJOINT) {
102-
// all shapes must be disjoint / must be contained in relation to the indexed shape.
103-
occur = BooleanClause.Occur.MUST;
104-
} else {
105-
// at least one shape must intersect / contain the indexed shape.
106-
occur = BooleanClause.Occur.SHOULD;
89+
public Void visit(Circle circle) {
90+
if (circle.isEmpty() == false) {
91+
geometries.add(GeoShapeUtils.toLuceneCircle(circle));
10792
}
93+
return null;
94+
}
95+
96+
@Override
97+
public Void visit(GeometryCollection<?> collection) {
10898
for (Geometry shape : collection) {
109-
bqb.add(shape.visit(this), occur);
99+
shape.visit(this);
110100
}
101+
return null;
111102
}
112103

113104
@Override
114-
public Query visit(org.elasticsearch.geometry.Line line) {
115-
validateIsGeoShapeFieldType();
116-
return LatLonShape.newLineQuery(fieldName, relation.getLuceneRelation(), new Line(line.getY(), line.getX()));
105+
public Void visit(org.elasticsearch.geometry.Line line) {
106+
if (line.isEmpty() == false) {
107+
List<org.elasticsearch.geometry.Line> collector = new ArrayList<>();
108+
GeoLineDecomposer.decomposeLine(line, collector);
109+
collectLines(collector);
110+
}
111+
return null;
117112
}
118113

119114
@Override
120-
public Query visit(LinearRing ring) {
121-
throw new QueryShardException(context, "Field [" + fieldName + "] found an unsupported shape LinearRing");
115+
public Void visit(LinearRing ring) {
116+
throw new QueryShardException(context, "Field [" + name + "] found and unsupported shape LinearRing");
122117
}
123118

124119
@Override
125-
public Query visit(MultiLine multiLine) {
126-
validateIsGeoShapeFieldType();
127-
Line[] lines = new Line[multiLine.size()];
128-
for (int i = 0; i < multiLine.size(); i++) {
129-
lines[i] = new Line(multiLine.get(i).getY(), multiLine.get(i).getX());
130-
}
131-
return LatLonShape.newLineQuery(fieldName, relation.getLuceneRelation(), lines);
120+
public Void visit(MultiLine multiLine) {
121+
List<org.elasticsearch.geometry.Line> collector = new ArrayList<>();
122+
GeoLineDecomposer.decomposeMultiLine(multiLine, collector);
123+
collectLines(collector);
124+
return null;
132125
}
133126

134127
@Override
135-
public Query visit(MultiPoint multiPoint) {
136-
double[][] points = new double[multiPoint.size()][2];
137-
for (int i = 0; i < multiPoint.size(); i++) {
138-
points[i] = new double[] {multiPoint.get(i).getLat(), multiPoint.get(i).getLon()};
128+
public Void visit(MultiPoint multiPoint) {
129+
for (Point point : multiPoint) {
130+
visit(point);
139131
}
140-
return LatLonShape.newPointQuery(fieldName, relation.getLuceneRelation(), points);
132+
return null;
141133
}
142134

143135
@Override
144-
public Query visit(MultiPolygon multiPolygon) {
145-
Polygon[] polygons = new Polygon[multiPolygon.size()];
146-
for (int i = 0; i < multiPolygon.size(); i++) {
147-
polygons[i] = toLucenePolygon(multiPolygon.get(i));
136+
public Void visit(MultiPolygon multiPolygon) {
137+
if (multiPolygon.isEmpty() == false) {
138+
List<org.elasticsearch.geometry.Polygon> collector = new ArrayList<>();
139+
GeoPolygonDecomposer.decomposeMultiPolygon(multiPolygon, true, collector);
140+
collectPolygons(collector);
148141
}
149-
return LatLonShape.newPolygonQuery(fieldName, relation.getLuceneRelation(), polygons);
142+
return null;
150143
}
151144

152145
@Override
153-
public Query visit(Point point) {
154-
validateIsGeoShapeFieldType();
155-
ShapeField.QueryRelation luceneRelation = relation.getLuceneRelation();
156-
if (luceneRelation == ShapeField.QueryRelation.CONTAINS) {
157-
// contains and intersects are equivalent but the implementation of
158-
// intersects is more efficient.
159-
luceneRelation = ShapeField.QueryRelation.INTERSECTS;
146+
public Void visit(Point point) {
147+
if (point.isEmpty() == false) {
148+
geometries.add(GeoShapeUtils.toLucenePoint(point));
160149
}
161-
return LatLonShape.newPointQuery(fieldName, luceneRelation,
162-
new double[] {point.getY(), point.getX()});
150+
return null;
151+
163152
}
164153

165154
@Override
166-
public Query visit(org.elasticsearch.geometry.Polygon polygon) {
167-
return LatLonShape.newPolygonQuery(fieldName, relation.getLuceneRelation(), toLucenePolygon(polygon));
155+
public Void visit(org.elasticsearch.geometry.Polygon polygon) {
156+
if (polygon.isEmpty() == false) {
157+
List<org.elasticsearch.geometry.Polygon> collector = new ArrayList<>();
158+
GeoPolygonDecomposer.decomposePolygon(polygon, true, collector);
159+
collectPolygons(collector);
160+
}
161+
return null;
168162
}
169163

170164
@Override
171-
public Query visit(Rectangle r) {
172-
return LatLonShape.newBoxQuery(fieldName, relation.getLuceneRelation(),
173-
r.getMinY(), r.getMaxY(), r.getMinX(), r.getMaxX());
165+
public Void visit(Rectangle r) {
166+
if (r.isEmpty() == false) {
167+
geometries.add(GeoShapeUtils.toLuceneRectangle(r));
168+
}
169+
return null;
174170
}
175171

176-
private void validateIsGeoShapeFieldType() {
177-
if (fieldType instanceof GeoShapeFieldMapper.GeoShapeFieldType == false) {
178-
throw new QueryShardException(context, "Expected " + GeoShapeFieldMapper.CONTENT_TYPE
179-
+ " field type for Field [" + fieldName + "] but found " + fieldType.typeName());
172+
private void collectLines(List<org.elasticsearch.geometry.Line> geometryLines) {
173+
for (Line line: geometryLines) {
174+
geometries.add(GeoShapeUtils.toLuceneLine(line));
180175
}
181176
}
182-
}
183177

178+
private void collectPolygons(List<org.elasticsearch.geometry.Polygon> geometryPolygons) {
179+
for (Polygon polygon : geometryPolygons) {
180+
geometries.add(GeoShapeUtils.toLucenePolygon(polygon));
181+
}
182+
}
183+
}
184184
}
185185

0 commit comments

Comments
 (0)