Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support geo label position as runtime field #86154

Merged
5 changes: 5 additions & 0 deletions docs/changelog/86154.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 86154
summary: Support geo label position as runtime field
area: Geo
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class org.elasticsearch.index.fielddata.ScriptDocValues$Geometry {
int getDimensionalType()
org.elasticsearch.common.geo.GeoPoint getCentroid()
org.elasticsearch.common.geo.GeoBoundingBox getBoundingBox()
org.elasticsearch.common.geo.GeoPoint getLabelPosition()
double getMercatorWidth()
double getMercatorHeight()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,18 @@ setup:
- match: { hits.hits.0.fields.bbox.0.bottom_right.lat: 41.1199999647215 }
- match: { hits.hits.0.fields.bbox.0.bottom_right.lon: -71.34000004269183 }

- do:
search:
rest_total_hits_as_int: true
body:
query: { term: { _id: "1" } }
script_fields:
label_position:
script:
source: "doc['geo_point'].getLabelPosition()"
- match: { hits.hits.0.fields.label_position.0.lat: 41.1199999647215 }
- match: { hits.hits.0.fields.label_position.0.lon: -71.34000004269183 }

- do:
search:
rest_total_hits_as_int: true
Expand All @@ -661,9 +673,9 @@ setup:
body:
query: { term: { _id: "1" } }
script_fields:
type:
script:
source: "doc['geo_point'].getDimensionalType()"
type:
script:
source: "doc['geo_point'].getDimensionalType()"
- match: { hits.hits.0.fields.type.0: 0 }

- do:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.document.DocumentField;
import org.elasticsearch.common.geo.GeoBoundingBox;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.geo.GeometryTestUtils;
import org.elasticsearch.index.fielddata.ScriptDocValues;
import org.elasticsearch.plugins.Plugin;
Expand All @@ -21,6 +22,8 @@
import org.elasticsearch.test.ESSingleNodeTestCase;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentFactory;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matchers;
import org.junit.Before;

Expand All @@ -36,6 +39,8 @@
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse;
import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.oneOf;

public class GeoPointScriptDocValuesIT extends ESSingleNodeTestCase {

Expand All @@ -54,6 +59,8 @@ protected Map<String, Function<Map<String, Object>, Object>> pluginScripts() {
scripts.put("lon", this::scriptLon);
scripts.put("height", this::scriptHeight);
scripts.put("width", this::scriptWidth);
scripts.put("label_lat", this::scriptLabelLat);
scripts.put("label_lon", this::scriptLabelLon);
return scripts;
}

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

private double scriptLabelLat(Map<String, Object> vars) {
Map<?, ?> doc = (Map<?, ?>) vars.get("doc");
ScriptDocValues.Geometry<?> geometry = assertGeometry(doc);
return geometry.size() == 0 ? Double.NaN : geometry.getLabelPosition().lat();
}

private double scriptLabelLon(Map<String, Object> vars) {
Map<?, ?> doc = (Map<?, ?>) vars.get("doc");
ScriptDocValues.Geometry<?> geometry = assertGeometry(doc);
return geometry.size() == 0 ? Double.NaN : geometry.getLabelPosition().lon();
}

private ScriptDocValues.Geometry<?> assertGeometry(Map<?, ?> doc) {
ScriptDocValues.Geometry<?> geometry = (ScriptDocValues.Geometry<?>) doc.get("location");
if (geometry.size() == 0) {
assertThat(geometry.getBoundingBox(), Matchers.nullValue());
assertThat(geometry.getCentroid(), Matchers.nullValue());
assertThat(geometry.getLabelPosition(), Matchers.nullValue());
assertThat(geometry.getDimensionalType(), equalTo(-1));
} else {
assertThat(geometry.getBoundingBox(), Matchers.notNullValue());
assertThat(geometry.getCentroid(), Matchers.notNullValue());
assertThat(geometry.getLabelPosition(), Matchers.notNullValue());
assertThat(geometry.getDimensionalType(), equalTo(0));
}
return geometry;
Expand Down Expand Up @@ -140,6 +161,8 @@ public void testRandomPoint() throws Exception {
.addScriptField("lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "lon", Collections.emptyMap()))
.addScriptField("height", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "height", Collections.emptyMap()))
.addScriptField("width", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "width", Collections.emptyMap()))
.addScriptField("label_lat", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "label_lat", Collections.emptyMap()))
.addScriptField("label_lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "label_lon", Collections.emptyMap()))
.get();
assertSearchResponse(searchResponse);

Expand All @@ -151,6 +174,10 @@ public void testRandomPoint() throws Exception {
assertThat(fields.get("lon").getValue(), equalTo(qLon));
assertThat(fields.get("height").getValue(), equalTo(0d));
assertThat(fields.get("width").getValue(), equalTo(0d));

// Check label position is the same point
assertThat(fields.get("label_lon").getValue(), equalTo(qLon));
assertThat(fields.get("label_lat").getValue(), equalTo(qLat));
}

public void testRandomMultiPoint() throws Exception {
Expand Down Expand Up @@ -178,6 +205,8 @@ public void testRandomMultiPoint() throws Exception {
.addScriptField("lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "lon", Collections.emptyMap()))
.addScriptField("height", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "height", Collections.emptyMap()))
.addScriptField("width", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "width", Collections.emptyMap()))
.addScriptField("label_lat", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "label_lat", Collections.emptyMap()))
.addScriptField("label_lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "label_lon", Collections.emptyMap()))
.get();
assertSearchResponse(searchResponse);

Expand All @@ -196,6 +225,11 @@ public void testRandomMultiPoint() throws Exception {
assertThat(fields.get("lon").getValue(), equalTo(centroidLon));
assertThat(fields.get("height").getValue(), equalTo(height));
assertThat(fields.get("width").getValue(), equalTo(width));

// Check label position is one of the incoming points
double labelLat = fields.get("label_lat").getValue();
double labelLon = fields.get("label_lon").getValue();
assertThat("Label should be one of the points", new GeoPoint(labelLat, labelLon), isMultiPointLabelPosition(lats, lons));
}

public void testNullPoint() throws Exception {
Expand All @@ -221,4 +255,29 @@ public void testNullPoint() throws Exception {
assertThat(fields.get("height").getValue(), equalTo(Double.NaN));
assertThat(fields.get("width").getValue(), equalTo(Double.NaN));
}

private static MultiPointLabelPosition isMultiPointLabelPosition(double[] lats, double[] lons) {
return new MultiPointLabelPosition(lats, lons);
}

private static class MultiPointLabelPosition extends BaseMatcher<GeoPoint> {
private final GeoPoint[] points;

private MultiPointLabelPosition(double[] lats, double[] lons) {
points = new GeoPoint[lats.length];
for (int i = 0; i < lats.length; i++) {
points[i] = new GeoPoint(lats[i], lons[i]);
}
}

@Override
public boolean matches(Object actual) {
return is(oneOf(points)).matches(actual);
}

@Override
public void describeTo(Description description) {
description.appendText("is(oneOf(" + Arrays.toString(points) + ")");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@ public Geometry(Supplier<T> supplier) {
/** Returns the bounding box of this geometry */
public abstract GeoBoundingBox getBoundingBox();

/** Returns the suggested label position */
public abstract GeoPoint getLabelPosition();

/** Returns the centroid of this geometry */
public abstract GeoPoint getCentroid();

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

GeoBoundingBox getInternalBoundingBox();

GeoPoint getInternalLabelPosition();
}

public static class GeoPoints extends Geometry<GeoPoint> {
Expand Down Expand Up @@ -363,6 +368,11 @@ public double getMercatorHeight() {
public GeoBoundingBox getBoundingBox() {
return size() == 0 ? null : geometrySupplier.getInternalBoundingBox();
}

@Override
public GeoPoint getLabelPosition() {
return size() == 0 ? null : geometrySupplier.getInternalLabelPosition();
}
}

public static class Booleans extends ScriptDocValues<Boolean> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ public interface GeoShapeQueryable {
Query geoShapeQuery(SearchExecutionContext context, String fieldName, ShapeRelation relation, LatLonGeometry... luceneGeometries);

default Query geoShapeQuery(SearchExecutionContext context, String fieldName, ShapeRelation relation, Geometry shape) {
final LatLonGeometry[] luceneGeometries = toQuantizeLuceneGeometry(fieldName, context, shape, relation);
final LatLonGeometry[] luceneGeometries;
try {
luceneGeometries = toQuantizeLuceneGeometry(shape, relation);
} catch (IllegalArgumentException e) {
throw new QueryShardException(context, "Exception creating query on Field [" + fieldName + "] " + e.getMessage(), e);
}
if (luceneGeometries.length == 0) {
return new MatchNoDocsQuery();
}
Expand Down Expand Up @@ -82,12 +87,7 @@ private static double[] quantizeLons(double[] lons) {
* transforms an Elasticsearch {@link Geometry} into a lucene {@link LatLonGeometry} and quantize
* the latitude and longitude values to match the values on the index.
*/
private static LatLonGeometry[] toQuantizeLuceneGeometry(
String name,
SearchExecutionContext context,
Geometry geometry,
ShapeRelation relation
) {
static LatLonGeometry[] toQuantizeLuceneGeometry(Geometry geometry, ShapeRelation relation) {
if (geometry == null) {
return new LatLonGeometry[0];
}
Expand Down Expand Up @@ -130,7 +130,7 @@ public Void visit(org.elasticsearch.geometry.Line line) {
if (relation == ShapeRelation.WITHIN) {
// Line geometries and WITHIN relation is not supported by Lucene. Throw an error here
// to have same behavior for runtime fields.
throw new QueryShardException(context, "Field [" + name + "] found an unsupported shape Line");
throw new IllegalArgumentException("found an unsupported shape Line");
}
geometries.add(new org.apache.lucene.geo.Line(quantizeLats(line.getLats()), quantizeLons(line.getLons())));
}
Expand All @@ -139,7 +139,7 @@ public Void visit(org.elasticsearch.geometry.Line line) {

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

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.apache.lucene.util.ArrayUtil;
import org.elasticsearch.common.geo.GeoBoundingBox;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.GeoUtils;
import org.elasticsearch.index.fielddata.MultiGeoPointValues;
import org.elasticsearch.index.fielddata.ScriptDocValues;

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

public GeoPointDocValuesField(MultiGeoPointValues input, String name) {
this.input = input;
Expand Down Expand Up @@ -71,11 +73,13 @@ private void setSingleValue() throws IOException {
centroid.reset(point.lat(), point.lon());
boundingBox.topLeft().reset(point.lat(), point.lon());
boundingBox.bottomRight().reset(point.lat(), point.lon());
labelIndex = 0;
}

private void setMultiValue() throws IOException {
double centroidLat = 0;
double centroidLon = 0;
labelIndex = 0;
double maxLon = Double.NEGATIVE_INFINITY;
double minLon = Double.POSITIVE_INFINITY;
double maxLat = Double.NEGATIVE_INFINITY;
Expand All @@ -89,12 +93,22 @@ private void setMultiValue() throws IOException {
minLon = Math.min(minLon, values[i].getLon());
maxLat = Math.max(maxLat, values[i].getLat());
minLat = Math.min(minLat, values[i].getLat());
labelIndex = closestPoint(labelIndex, i, (minLat + maxLat) / 2, (minLon + maxLon) / 2);
}
centroid.reset(centroidLat / count, centroidLon / count);
boundingBox.topLeft().reset(maxLat, minLon);
boundingBox.bottomRight().reset(minLat, maxLon);
}

private int closestPoint(int a, int b, double lat, double lon) {
if (a == b) {
return a;
}
double distA = GeoUtils.planeDistance(lat, lon, values[a].lat(), values[a].lon());
double distB = GeoUtils.planeDistance(lat, lon, values[b].lat(), values[b].lon());
return distA < distB ? a : b;
}

@Override
public ScriptDocValues<GeoPoint> toScriptDocValues() {
if (geoPoints == null) {
Expand All @@ -121,6 +135,11 @@ public GeoBoundingBox getInternalBoundingBox() {
return boundingBox;
}

@Override
public GeoPoint getInternalLabelPosition() {
return values[labelIndex];
}

@Override
public String getName() {
return name;
Expand Down
Loading