Skip to content

Commit 36fd640

Browse files
Support geo label position as runtime field (#86154)
We do this differently for each geometry type: * For points we return the point * For multipoint the centroid is unlikely to be one of the points, so we choose a point closest to the middle of the bounding box. * For linestring we choose the first line-segment in the triangle tree, and return its center. * For polygons we choose the centroid, but check if it is contained within the polygon. If not, we choose the first triangle in the triangle tree and return its center (average point) The use of the first entry in the triangle tree is a technique to get a likely approximate center, while also being high performance.
1 parent 65d9098 commit 36fd640

File tree

18 files changed

+1093
-15
lines changed

18 files changed

+1093
-15
lines changed

docs/changelog/86154.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 86154
2+
summary: Support geo label position as runtime field
3+
area: Geo
4+
type: enhancement
5+
issues: []

modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class org.elasticsearch.index.fielddata.ScriptDocValues$Geometry {
8585
int getDimensionalType()
8686
org.elasticsearch.common.geo.GeoPoint getCentroid()
8787
org.elasticsearch.common.geo.GeoBoundingBox getBoundingBox()
88+
org.elasticsearch.common.geo.GeoPoint getLabelPosition()
8889
double getMercatorWidth()
8990
double getMercatorHeight()
9091
}

modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,18 @@ setup:
638638
- match: { hits.hits.0.fields.bbox.0.bottom_right.lat: 41.1199999647215 }
639639
- match: { hits.hits.0.fields.bbox.0.bottom_right.lon: -71.34000004269183 }
640640

641+
- do:
642+
search:
643+
rest_total_hits_as_int: true
644+
body:
645+
query: { term: { _id: "1" } }
646+
script_fields:
647+
label_position:
648+
script:
649+
source: "doc['geo_point'].getLabelPosition()"
650+
- match: { hits.hits.0.fields.label_position.0.lat: 41.1199999647215 }
651+
- match: { hits.hits.0.fields.label_position.0.lon: -71.34000004269183 }
652+
641653
- do:
642654
search:
643655
rest_total_hits_as_int: true
@@ -661,9 +673,9 @@ setup:
661673
body:
662674
query: { term: { _id: "1" } }
663675
script_fields:
664-
type:
665-
script:
666-
source: "doc['geo_point'].getDimensionalType()"
676+
type:
677+
script:
678+
source: "doc['geo_point'].getDimensionalType()"
667679
- match: { hits.hits.0.fields.type.0: 0 }
668680

669681
- do:

server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoPointScriptDocValuesIT.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.elasticsearch.action.search.SearchResponse;
1313
import org.elasticsearch.common.document.DocumentField;
1414
import org.elasticsearch.common.geo.GeoBoundingBox;
15+
import org.elasticsearch.common.geo.GeoPoint;
1516
import org.elasticsearch.geo.GeometryTestUtils;
1617
import org.elasticsearch.index.fielddata.ScriptDocValues;
1718
import org.elasticsearch.plugins.Plugin;
@@ -21,6 +22,8 @@
2122
import org.elasticsearch.test.ESSingleNodeTestCase;
2223
import org.elasticsearch.xcontent.XContentBuilder;
2324
import org.elasticsearch.xcontent.XContentFactory;
25+
import org.hamcrest.BaseMatcher;
26+
import org.hamcrest.Description;
2427
import org.hamcrest.Matchers;
2528
import org.junit.Before;
2629

@@ -36,6 +39,8 @@
3639
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse;
3740
import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder;
3841
import static org.hamcrest.Matchers.equalTo;
42+
import static org.hamcrest.Matchers.is;
43+
import static org.hamcrest.Matchers.oneOf;
3944

4045
public class GeoPointScriptDocValuesIT extends ESSingleNodeTestCase {
4146

@@ -54,6 +59,8 @@ protected Map<String, Function<Map<String, Object>, Object>> pluginScripts() {
5459
scripts.put("lon", this::scriptLon);
5560
scripts.put("height", this::scriptHeight);
5661
scripts.put("width", this::scriptWidth);
62+
scripts.put("label_lat", this::scriptLabelLat);
63+
scripts.put("label_lon", this::scriptLabelLon);
5764
return scripts;
5865
}
5966

@@ -91,15 +98,29 @@ private double scriptLon(Map<String, Object> vars) {
9198
return geometry.size() == 0 ? Double.NaN : geometry.getCentroid().lon();
9299
}
93100

101+
private double scriptLabelLat(Map<String, Object> vars) {
102+
Map<?, ?> doc = (Map<?, ?>) vars.get("doc");
103+
ScriptDocValues.Geometry<?> geometry = assertGeometry(doc);
104+
return geometry.size() == 0 ? Double.NaN : geometry.getLabelPosition().lat();
105+
}
106+
107+
private double scriptLabelLon(Map<String, Object> vars) {
108+
Map<?, ?> doc = (Map<?, ?>) vars.get("doc");
109+
ScriptDocValues.Geometry<?> geometry = assertGeometry(doc);
110+
return geometry.size() == 0 ? Double.NaN : geometry.getLabelPosition().lon();
111+
}
112+
94113
private ScriptDocValues.Geometry<?> assertGeometry(Map<?, ?> doc) {
95114
ScriptDocValues.Geometry<?> geometry = (ScriptDocValues.Geometry<?>) doc.get("location");
96115
if (geometry.size() == 0) {
97116
assertThat(geometry.getBoundingBox(), Matchers.nullValue());
98117
assertThat(geometry.getCentroid(), Matchers.nullValue());
118+
assertThat(geometry.getLabelPosition(), Matchers.nullValue());
99119
assertThat(geometry.getDimensionalType(), equalTo(-1));
100120
} else {
101121
assertThat(geometry.getBoundingBox(), Matchers.notNullValue());
102122
assertThat(geometry.getCentroid(), Matchers.notNullValue());
123+
assertThat(geometry.getLabelPosition(), Matchers.notNullValue());
103124
assertThat(geometry.getDimensionalType(), equalTo(0));
104125
}
105126
return geometry;
@@ -140,6 +161,8 @@ public void testRandomPoint() throws Exception {
140161
.addScriptField("lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "lon", Collections.emptyMap()))
141162
.addScriptField("height", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "height", Collections.emptyMap()))
142163
.addScriptField("width", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "width", Collections.emptyMap()))
164+
.addScriptField("label_lat", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "label_lat", Collections.emptyMap()))
165+
.addScriptField("label_lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "label_lon", Collections.emptyMap()))
143166
.get();
144167
assertSearchResponse(searchResponse);
145168

@@ -151,6 +174,10 @@ public void testRandomPoint() throws Exception {
151174
assertThat(fields.get("lon").getValue(), equalTo(qLon));
152175
assertThat(fields.get("height").getValue(), equalTo(0d));
153176
assertThat(fields.get("width").getValue(), equalTo(0d));
177+
178+
// Check label position is the same point
179+
assertThat(fields.get("label_lon").getValue(), equalTo(qLon));
180+
assertThat(fields.get("label_lat").getValue(), equalTo(qLat));
154181
}
155182

156183
public void testRandomMultiPoint() throws Exception {
@@ -178,6 +205,8 @@ public void testRandomMultiPoint() throws Exception {
178205
.addScriptField("lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "lon", Collections.emptyMap()))
179206
.addScriptField("height", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "height", Collections.emptyMap()))
180207
.addScriptField("width", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "width", Collections.emptyMap()))
208+
.addScriptField("label_lat", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "label_lat", Collections.emptyMap()))
209+
.addScriptField("label_lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "label_lon", Collections.emptyMap()))
181210
.get();
182211
assertSearchResponse(searchResponse);
183212

@@ -196,6 +225,11 @@ public void testRandomMultiPoint() throws Exception {
196225
assertThat(fields.get("lon").getValue(), equalTo(centroidLon));
197226
assertThat(fields.get("height").getValue(), equalTo(height));
198227
assertThat(fields.get("width").getValue(), equalTo(width));
228+
229+
// Check label position is one of the incoming points
230+
double labelLat = fields.get("label_lat").getValue();
231+
double labelLon = fields.get("label_lon").getValue();
232+
assertThat("Label should be one of the points", new GeoPoint(labelLat, labelLon), isMultiPointLabelPosition(lats, lons));
199233
}
200234

201235
public void testNullPoint() throws Exception {
@@ -221,4 +255,29 @@ public void testNullPoint() throws Exception {
221255
assertThat(fields.get("height").getValue(), equalTo(Double.NaN));
222256
assertThat(fields.get("width").getValue(), equalTo(Double.NaN));
223257
}
258+
259+
private static MultiPointLabelPosition isMultiPointLabelPosition(double[] lats, double[] lons) {
260+
return new MultiPointLabelPosition(lats, lons);
261+
}
262+
263+
private static class MultiPointLabelPosition extends BaseMatcher<GeoPoint> {
264+
private final GeoPoint[] points;
265+
266+
private MultiPointLabelPosition(double[] lats, double[] lons) {
267+
points = new GeoPoint[lats.length];
268+
for (int i = 0; i < lats.length; i++) {
269+
points[i] = new GeoPoint(lats[i], lons[i]);
270+
}
271+
}
272+
273+
@Override
274+
public boolean matches(Object actual) {
275+
return is(oneOf(points)).matches(actual);
276+
}
277+
278+
@Override
279+
public void describeTo(Description description) {
280+
description.appendText("is(oneOf(" + Arrays.toString(points) + ")");
281+
}
282+
}
224283
}

server/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,9 @@ public Geometry(Supplier<T> supplier) {
232232
/** Returns the bounding box of this geometry */
233233
public abstract GeoBoundingBox getBoundingBox();
234234

235+
/** Returns the suggested label position */
236+
public abstract GeoPoint getLabelPosition();
237+
235238
/** Returns the centroid of this geometry */
236239
public abstract GeoPoint getCentroid();
237240

@@ -247,6 +250,8 @@ public interface GeometrySupplier<T> extends Supplier<T> {
247250
GeoPoint getInternalCentroid();
248251

249252
GeoBoundingBox getInternalBoundingBox();
253+
254+
GeoPoint getInternalLabelPosition();
250255
}
251256

252257
public static class GeoPoints extends Geometry<GeoPoint> {
@@ -363,6 +368,11 @@ public double getMercatorHeight() {
363368
public GeoBoundingBox getBoundingBox() {
364369
return size() == 0 ? null : geometrySupplier.getInternalBoundingBox();
365370
}
371+
372+
@Override
373+
public GeoPoint getLabelPosition() {
374+
return size() == 0 ? null : geometrySupplier.getInternalLabelPosition();
375+
}
366376
}
367377

368378
public static class Booleans extends ScriptDocValues<Boolean> {

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ public interface GeoShapeQueryable {
4444
Query geoShapeQuery(SearchExecutionContext context, String fieldName, ShapeRelation relation, LatLonGeometry... luceneGeometries);
4545

4646
default Query geoShapeQuery(SearchExecutionContext context, String fieldName, ShapeRelation relation, Geometry shape) {
47-
final LatLonGeometry[] luceneGeometries = toQuantizeLuceneGeometry(fieldName, context, shape, relation);
47+
final LatLonGeometry[] luceneGeometries;
48+
try {
49+
luceneGeometries = toQuantizeLuceneGeometry(shape, relation);
50+
} catch (IllegalArgumentException e) {
51+
throw new QueryShardException(context, "Exception creating query on Field [" + fieldName + "] " + e.getMessage(), e);
52+
}
4853
if (luceneGeometries.length == 0) {
4954
return new MatchNoDocsQuery();
5055
}
@@ -82,12 +87,7 @@ private static double[] quantizeLons(double[] lons) {
8287
* transforms an Elasticsearch {@link Geometry} into a lucene {@link LatLonGeometry} and quantize
8388
* the latitude and longitude values to match the values on the index.
8489
*/
85-
private static LatLonGeometry[] toQuantizeLuceneGeometry(
86-
String name,
87-
SearchExecutionContext context,
88-
Geometry geometry,
89-
ShapeRelation relation
90-
) {
90+
static LatLonGeometry[] toQuantizeLuceneGeometry(Geometry geometry, ShapeRelation relation) {
9191
if (geometry == null) {
9292
return new LatLonGeometry[0];
9393
}
@@ -130,7 +130,7 @@ public Void visit(org.elasticsearch.geometry.Line line) {
130130
if (relation == ShapeRelation.WITHIN) {
131131
// Line geometries and WITHIN relation is not supported by Lucene. Throw an error here
132132
// to have same behavior for runtime fields.
133-
throw new QueryShardException(context, "Field [" + name + "] found an unsupported shape Line");
133+
throw new IllegalArgumentException("found an unsupported shape Line");
134134
}
135135
geometries.add(new org.apache.lucene.geo.Line(quantizeLats(line.getLats()), quantizeLons(line.getLons())));
136136
}
@@ -139,7 +139,7 @@ public Void visit(org.elasticsearch.geometry.Line line) {
139139

140140
@Override
141141
public Void visit(LinearRing ring) {
142-
throw new QueryShardException(context, "Field [" + name + "] found an unsupported shape LinearRing");
142+
throw new IllegalArgumentException("Found an unsupported shape LinearRing");
143143
}
144144

145145
@Override

server/src/main/java/org/elasticsearch/script/field/GeoPointDocValuesField.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.apache.lucene.util.ArrayUtil;
1212
import org.elasticsearch.common.geo.GeoBoundingBox;
1313
import org.elasticsearch.common.geo.GeoPoint;
14+
import org.elasticsearch.common.geo.GeoUtils;
1415
import org.elasticsearch.index.fielddata.MultiGeoPointValues;
1516
import org.elasticsearch.index.fielddata.ScriptDocValues;
1617

@@ -34,6 +35,7 @@ public class GeoPointDocValuesField extends AbstractScriptFieldFactory<GeoPoint>
3435
private ScriptDocValues.GeoPoints geoPoints = null;
3536
private final GeoPoint centroid = new GeoPoint();
3637
private final GeoBoundingBox boundingBox = new GeoBoundingBox(new GeoPoint(), new GeoPoint());
38+
private int labelIndex = 0;
3739

3840
public GeoPointDocValuesField(MultiGeoPointValues input, String name) {
3941
this.input = input;
@@ -71,11 +73,13 @@ private void setSingleValue() throws IOException {
7173
centroid.reset(point.lat(), point.lon());
7274
boundingBox.topLeft().reset(point.lat(), point.lon());
7375
boundingBox.bottomRight().reset(point.lat(), point.lon());
76+
labelIndex = 0;
7477
}
7578

7679
private void setMultiValue() throws IOException {
7780
double centroidLat = 0;
7881
double centroidLon = 0;
82+
labelIndex = 0;
7983
double maxLon = Double.NEGATIVE_INFINITY;
8084
double minLon = Double.POSITIVE_INFINITY;
8185
double maxLat = Double.NEGATIVE_INFINITY;
@@ -89,12 +93,22 @@ private void setMultiValue() throws IOException {
8993
minLon = Math.min(minLon, values[i].getLon());
9094
maxLat = Math.max(maxLat, values[i].getLat());
9195
minLat = Math.min(minLat, values[i].getLat());
96+
labelIndex = closestPoint(labelIndex, i, (minLat + maxLat) / 2, (minLon + maxLon) / 2);
9297
}
9398
centroid.reset(centroidLat / count, centroidLon / count);
9499
boundingBox.topLeft().reset(maxLat, minLon);
95100
boundingBox.bottomRight().reset(minLat, maxLon);
96101
}
97102

103+
private int closestPoint(int a, int b, double lat, double lon) {
104+
if (a == b) {
105+
return a;
106+
}
107+
double distA = GeoUtils.planeDistance(lat, lon, values[a].lat(), values[a].lon());
108+
double distB = GeoUtils.planeDistance(lat, lon, values[b].lat(), values[b].lon());
109+
return distA < distB ? a : b;
110+
}
111+
98112
@Override
99113
public ScriptDocValues<GeoPoint> toScriptDocValues() {
100114
if (geoPoints == null) {
@@ -121,6 +135,11 @@ public GeoBoundingBox getInternalBoundingBox() {
121135
return boundingBox;
122136
}
123137

138+
@Override
139+
public GeoPoint getInternalLabelPosition() {
140+
return values[labelIndex];
141+
}
142+
124143
@Override
125144
public String getName() {
126145
return name;

0 commit comments

Comments
 (0)