diff --git a/docs/changelog/85120.yaml b/docs/changelog/85120.yaml new file mode 100644 index 0000000000000..4d23584325159 --- /dev/null +++ b/docs/changelog/85120.yaml @@ -0,0 +1,5 @@ +pr: 85120 +summary: Support GeoJSON for `geo_point` +area: Geo +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeoUtils.java b/server/src/main/java/org/elasticsearch/common/geo/GeoUtils.java index 78c5ae2f00c58..fb7c6ec634aca 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/GeoUtils.java +++ b/server/src/main/java/org/elasticsearch/common/geo/GeoUtils.java @@ -26,7 +26,9 @@ import org.elasticsearch.xcontent.support.MapXContentParser; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; +import java.util.Locale; public class GeoUtils { @@ -42,6 +44,8 @@ public class GeoUtils { public static final String LATITUDE = "lat"; public static final String LONGITUDE = "lon"; public static final String GEOHASH = "geohash"; + public static final String COORDINATES = "coordinates"; + public static final String TYPE = "type"; /** Earth ellipsoid major axis defined by WGS 84 in meters */ public static final double EARTH_SEMI_MAJOR_AXIS = 6378137.0; // meters (WGS 84) @@ -437,75 +441,81 @@ public static GeoPoint parseGeoPoint(XContentParser parser, GeoPoint point, fina double lat = Double.NaN; double lon = Double.NaN; String geohash = null; - NumberFormatException numberFormatException = null; + String geojsonType = null; + ArrayList coordinates = null; if (parser.currentToken() == Token.START_OBJECT) { try (XContentSubParser subParser = new XContentSubParser(parser)) { while (subParser.nextToken() != Token.END_OBJECT) { if (subParser.currentToken() == Token.FIELD_NAME) { String field = subParser.currentName(); + subParser.nextToken(); if (LATITUDE.equals(field)) { - subParser.nextToken(); - switch (subParser.currentToken()) { - case VALUE_NUMBER: - case VALUE_STRING: - try { - lat = subParser.doubleValue(true); - } catch (NumberFormatException e) { - numberFormatException = e; - } - break; - default: - throw new ElasticsearchParseException("latitude must be a number"); - } + lat = parseValidDouble(subParser, "latitude"); } else if (LONGITUDE.equals(field)) { - subParser.nextToken(); - switch (subParser.currentToken()) { - case VALUE_NUMBER: - case VALUE_STRING: - try { - lon = subParser.doubleValue(true); - } catch (NumberFormatException e) { - numberFormatException = e; - } - break; - default: - throw new ElasticsearchParseException("longitude must be a number"); - } + lon = parseValidDouble(subParser, "longitude"); } else if (GEOHASH.equals(field)) { - if (subParser.nextToken() == Token.VALUE_STRING) { + if (subParser.currentToken() == Token.VALUE_STRING) { geohash = subParser.text(); } else { throw new ElasticsearchParseException("geohash must be a string"); } + } else if (COORDINATES.equals(field)) { + if (subParser.currentToken() == Token.START_ARRAY) { + coordinates = new ArrayList<>(); + while (subParser.nextToken() != Token.END_ARRAY) { + coordinates.add(parseValidDouble(subParser, field)); + } + } else { + throw new ElasticsearchParseException("GeoJSON 'coordinates' must be an array"); + } + } else if (TYPE.equals(field)) { + if (subParser.currentToken() == Token.VALUE_STRING) { + geojsonType = subParser.text(); + } else { + throw new ElasticsearchParseException("GeoJSON 'type' must be a string"); + } } else { - throw new ElasticsearchParseException("field must be either [{}], [{}] or [{}]", LATITUDE, LONGITUDE, GEOHASH); + throw new ElasticsearchParseException( + "field must be either [{}], [{}], [{}], [{}] or [{}]", + LATITUDE, + LONGITUDE, + GEOHASH, + COORDINATES, + TYPE + ); } } else { throw new ElasticsearchParseException("token [{}] not allowed", subParser.currentToken()); } } } + assertOnlyOneFormat( + geohash != null, + Double.isNaN(lat) == false, + Double.isNaN(lon) == false, + coordinates != null, + geojsonType != null + ); if (geohash != null) { - if (Double.isNaN(lat) == false || Double.isNaN(lon) == false) { - throw new ElasticsearchParseException("field must be either lat/lon or geohash"); - } else { - return point.parseGeoHash(geohash, effectivePoint); + return point.parseGeoHash(geohash, effectivePoint); + } + if (coordinates != null) { + if (geojsonType == null || geojsonType.toLowerCase(Locale.ROOT).equals("point") == false) { + throw new ElasticsearchParseException("GeoJSON 'type' for geo_point can only be 'Point'"); } - } else if (numberFormatException != null) { - throw new ElasticsearchParseException( - "[{}] and [{}] must be valid double values", - numberFormatException, - LATITUDE, - LONGITUDE - ); - } else if (Double.isNaN(lat)) { - throw new ElasticsearchParseException("field [{}] missing", LATITUDE); - } else if (Double.isNaN(lon)) { - throw new ElasticsearchParseException("field [{}] missing", LONGITUDE); - } else { - return point.reset(lat, lon); + if (coordinates.size() < 2) { + throw new ElasticsearchParseException("GeoJSON 'coordinates' must contain at least two values"); + } + if (coordinates.size() == 3) { + GeoPoint.assertZValue(ignoreZValue, coordinates.get(2)); + } + if (coordinates.size() > 3) { + throw new ElasticsearchParseException("[geo_point] field type does not accept > 3 dimensions"); + } + return point.reset(coordinates.get(1), coordinates.get(0)); } + return point.reset(lat, lon); } else if (parser.currentToken() == Token.START_ARRAY) { try (XContentSubParser subParser = new XContentSubParser(parser)) { @@ -536,6 +546,46 @@ public static GeoPoint parseGeoPoint(XContentParser parser, GeoPoint point, fina } } + private static double parseValidDouble(XContentSubParser subParser, String field) throws IOException { + try { + return switch (subParser.currentToken()) { + case VALUE_NUMBER, VALUE_STRING -> subParser.doubleValue(true); + default -> throw new ElasticsearchParseException("{} must be a number", field); + }; + } catch (NumberFormatException e) { + throw new ElasticsearchParseException("[{}] must be a valid double value", e, field); + } + } + + private static void assertOnlyOneFormat(boolean geohash, boolean lat, boolean lon, boolean coordinates, boolean type) { + String invalidFieldsMessage = "field must be either lat/lon, geohash string or type/coordinates"; + boolean latlon = lat && lon; + boolean geojson = coordinates && type; + var found = new ArrayList(); + if (geohash) found.add("geohash"); + if (latlon) found.add("lat/lon"); + if (geojson) found.add("GeoJSON"); + if (found.size() > 1) { + throw new ElasticsearchParseException("fields matching more than one point format found: {}", found); + } else if (geohash) { + if (lat || lon || type || coordinates) { + throw new ElasticsearchParseException(invalidFieldsMessage); + } + } else if (found.size() == 0) { + if (lat) { + throw new ElasticsearchParseException("field [{}] missing", LONGITUDE); + } else if (lon) { + throw new ElasticsearchParseException("field [{}] missing", LATITUDE); + } else if (coordinates) { + throw new ElasticsearchParseException("field [{}] missing", TYPE); + } else if (type) { + throw new ElasticsearchParseException("field [{}] missing", COORDINATES); + } else { + throw new ElasticsearchParseException(invalidFieldsMessage); + } + } + } + /** * Parse a {@link GeoPoint} from a string. The string must have one of the following forms: * diff --git a/server/src/test/java/org/elasticsearch/index/mapper/GeoPointFieldTypeTests.java b/server/src/test/java/org/elasticsearch/index/mapper/GeoPointFieldTypeTests.java index c38ffd0c61902..3d0fe92a0f532 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/GeoPointFieldTypeTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/GeoPointFieldTypeTests.java @@ -71,6 +71,16 @@ public void testFetchSourceValue() throws IOException { assertEquals(List.of(), fetchSourceValue(mapper, sourceValue, null)); assertEquals(List.of(), fetchSourceValue(mapper, sourceValue, "wkt")); } + + // test single point in GeoJSON format + sourceValue = jsonPoint; + assertEquals(List.of(jsonPoint), fetchSourceValue(mapper, sourceValue, null)); + assertEquals(List.of(wktPoint), fetchSourceValue(mapper, sourceValue, "wkt")); + + // Test a list of points in GeoJSON format + sourceValue = List.of(jsonPoint, otherJsonPoint); + assertEquals(List.of(jsonPoint, otherJsonPoint), fetchSourceValue(mapper, sourceValue, null)); + assertEquals(List.of(wktPoint, otherWktPoint), fetchSourceValue(mapper, sourceValue, "wkt")); } public void testFetchVectorTile() throws IOException { diff --git a/server/src/test/java/org/elasticsearch/index/search/geo/GeoPointParsingTests.java b/server/src/test/java/org/elasticsearch/index/search/geo/GeoPointParsingTests.java index 216e643ecfca4..4378e0184a775 100644 --- a/server/src/test/java/org/elasticsearch/index/search/geo/GeoPointParsingTests.java +++ b/server/src/test/java/org/elasticsearch/index/search/geo/GeoPointParsingTests.java @@ -10,8 +10,10 @@ import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.geo.GeoJson; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.GeoUtils; +import org.elasticsearch.geometry.Point; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.geo.RandomGeoGenerator; import org.elasticsearch.xcontent.XContentBuilder; @@ -19,6 +21,7 @@ import org.elasticsearch.xcontent.json.JsonXContent; import java.io.IOException; +import java.util.HashMap; import java.util.function.DoubleSupplier; import static org.elasticsearch.geometry.utils.Geohash.stringEncode; @@ -84,25 +87,28 @@ public void testGeoPointParsing() throws IOException { GeoPoint point = GeoUtils.parseGeoPoint(objectLatLon(randomPt.lat(), randomPt.lon())); assertPointsEqual(point, randomPt); - GeoUtils.parseGeoPoint(toObject(objectLatLon(randomPt.lat(), randomPt.lon())), randomBoolean()); + point = GeoUtils.parseGeoPoint(toObject(objectLatLon(randomPt.lat(), randomPt.lon())), randomBoolean()); assertPointsEqual(point, randomPt); GeoUtils.parseGeoPoint(arrayLatLon(randomPt.lat(), randomPt.lon()), point); assertPointsEqual(point, randomPt); - GeoUtils.parseGeoPoint(toObject(arrayLatLon(randomPt.lat(), randomPt.lon())), randomBoolean()); + point = GeoUtils.parseGeoPoint(toObject(arrayLatLon(randomPt.lat(), randomPt.lon())), randomBoolean()); assertPointsEqual(point, randomPt); GeoUtils.parseGeoPoint(geohash(randomPt.lat(), randomPt.lon()), point); assertCloseTo(point, randomPt.lat(), randomPt.lon()); - GeoUtils.parseGeoPoint(toObject(geohash(randomPt.lat(), randomPt.lon())), randomBoolean()); + point = GeoUtils.parseGeoPoint(toObject(geohash(randomPt.lat(), randomPt.lon())), randomBoolean()); assertCloseTo(point, randomPt.lat(), randomPt.lon()); GeoUtils.parseGeoPoint(stringLatLon(randomPt.lat(), randomPt.lon()), point); assertCloseTo(point, randomPt.lat(), randomPt.lon()); - GeoUtils.parseGeoPoint(toObject(stringLatLon(randomPt.lat(), randomPt.lon())), randomBoolean()); + point = GeoUtils.parseGeoPoint(toObject(stringLatLon(randomPt.lat(), randomPt.lon())), randomBoolean()); + assertCloseTo(point, randomPt.lat(), randomPt.lon()); + + point = GeoUtils.parseGeoPoint(GeoJson.toMap(new Point(randomPt.lon(), randomPt.lat())), randomBoolean()); assertCloseTo(point, randomPt.lat(), randomPt.lon()); } @@ -118,49 +124,40 @@ public void testInvalidPointEmbeddedObject() throws IOException { try (XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content))) { parser.nextToken(); Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); - assertThat(e.getMessage(), is("field must be either [lat], [lon] or [geohash]")); - } - try (XContentParser parser2 = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content))) { - parser2.nextToken(); - Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(toObject(parser2), randomBoolean())); - assertThat(e.getMessage(), is("field must be either [lat], [lon] or [geohash]")); - } - } - - public void testInvalidPointLatHashMix() throws IOException { - XContentBuilder content = JsonXContent.contentBuilder(); - content.startObject(); - content.field("lat", 0).field("geohash", stringEncode(0d, 0d)); - content.endObject(); - - try (XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content))) { - parser.nextToken(); - Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); - assertThat(e.getMessage(), is("field must be either lat/lon or geohash")); + assertThat(e.getMessage(), is("field must be either [lat], [lon], [geohash], [coordinates] or [type]")); } try (XContentParser parser2 = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content))) { parser2.nextToken(); Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(toObject(parser2), randomBoolean())); - assertThat(e.getMessage(), is("field must be either lat/lon or geohash")); + assertThat(e.getMessage(), is("field must be either [lat], [lon], [geohash], [coordinates] or [type]")); } } - public void testInvalidPointLonHashMix() throws IOException { - XContentBuilder content = JsonXContent.contentBuilder(); - content.startObject(); - content.field("lon", 0).field("geohash", stringEncode(0d, 0d)); - content.endObject(); - - try (XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content))) { - parser.nextToken(); - - Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); - assertThat(e.getMessage(), is("field must be either lat/lon or geohash")); - } - try (XContentParser parser2 = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content))) { - parser2.nextToken(); - Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(toObject(parser2), randomBoolean())); - assertThat(e.getMessage(), is("field must be either lat/lon or geohash")); + public void testInvalidPointHashMix() throws IOException { + HashMap otherFields = new HashMap<>(); + otherFields.put("lat", 0); + otherFields.put("lon", 0); + otherFields.put("type", "Point"); + otherFields.put("coordinates", new double[] { 0.0, 0.0 }); + for (String other : otherFields.keySet()) { + XContentBuilder content = JsonXContent.contentBuilder(); + content.startObject(); + content.field(other, otherFields.get(other)).field("geohash", stringEncode(0d, 0d)); + content.endObject(); + + try (XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content))) { + parser.nextToken(); + Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); + assertThat(e.getMessage(), is("field must be either lat/lon, geohash string or type/coordinates")); + } + try (XContentParser parser2 = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content))) { + parser2.nextToken(); + Exception e = expectThrows( + ElasticsearchParseException.class, + () -> GeoUtils.parseGeoPoint(toObject(parser2), randomBoolean()) + ); + assertThat(e.getMessage(), is("field must be either lat/lon, geohash string or type/coordinates")); + } } } @@ -173,13 +170,13 @@ public void testInvalidField() throws IOException { try (XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content))) { parser.nextToken(); Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); - assertThat(e.getMessage(), is("field must be either [lat], [lon] or [geohash]")); + assertThat(e.getMessage(), is("field must be either [lat], [lon], [geohash], [coordinates] or [type]")); } try (XContentParser parser2 = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content))) { parser2.nextToken(); Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(toObject(parser2), randomBoolean())); - assertThat(e.getMessage(), is("field must be either [lat], [lon] or [geohash]")); + assertThat(e.getMessage(), is("field must be either [lat], [lon], [geohash], [coordinates] or [type]")); } } diff --git a/server/src/test/java/org/elasticsearch/index/search/geo/GeoUtilsTests.java b/server/src/test/java/org/elasticsearch/index/search/geo/GeoUtilsTests.java index 892f3bc9ddb26..16a713b7619ac 100644 --- a/server/src/test/java/org/elasticsearch/index/search/geo/GeoUtilsTests.java +++ b/server/src/test/java/org/elasticsearch/index/search/geo/GeoUtilsTests.java @@ -590,6 +590,100 @@ public void testParseGeoPointLatWrongType() throws IOException { } } + public void testParseGeoPointCoordinateNoType() throws IOException { + double[] coords = new double[] { 0.0, 0.0 }; + XContentBuilder json = jsonBuilder().startObject().field("coordinates", coords).endObject(); + try (XContentParser parser = createParser(json)) { + parser.nextToken(); + Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); + assertThat(e.getMessage(), is("field [type] missing")); + assertThat(parser.currentToken(), is(Token.END_OBJECT)); + assertNull(parser.nextToken()); + } + } + + public void testParseGeoPointTypeNoCoordinates() throws IOException { + XContentBuilder json = jsonBuilder().startObject().field("type", "Point").endObject(); + try (XContentParser parser = createParser(json)) { + parser.nextToken(); + Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); + assertThat(e.getMessage(), is("field [coordinates] missing")); + assertThat(parser.currentToken(), is(Token.END_OBJECT)); + assertNull(parser.nextToken()); + } + } + + public void testParseGeoPointTypeWrongValue() throws IOException { + double[] coords = new double[] { 0.0, 0.0 }; + XContentBuilder json = jsonBuilder().startObject().field("coordinates", coords).field("type", "LineString").endObject(); + try (XContentParser parser = createParser(json)) { + parser.nextToken(); + Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); + assertThat(e.getMessage(), is("GeoJSON 'type' for geo_point can only be 'Point'")); + assertThat(parser.currentToken(), is(Token.END_OBJECT)); + assertNull(parser.nextToken()); + } + } + + public void testParseGeoPointTypeWrongType() throws IOException { + double[] coords = new double[] { 0.0, 0.0 }; + XContentBuilder json = jsonBuilder().startObject().field("coordinates", coords).field("type", false).endObject(); + try (XContentParser parser = createParser(json)) { + parser.nextToken(); + Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); + assertThat(e.getMessage(), is("GeoJSON 'type' must be a string")); + assertThat(parser.currentToken(), is(Token.END_OBJECT)); + assertNull(parser.nextToken()); + } + } + + public void testParseGeoPointCoordinatesWrongType() throws IOException { + XContentBuilder json = jsonBuilder().startObject().field("coordinates", false).field("type", "Point").endObject(); + try (XContentParser parser = createParser(json)) { + parser.nextToken(); + Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); + assertThat(e.getMessage(), is("GeoJSON 'coordinates' must be an array")); + assertThat(parser.currentToken(), is(Token.END_OBJECT)); + assertNull(parser.nextToken()); + } + } + + public void testParseGeoPointCoordinatesTooShort() throws IOException { + double[] coords = new double[] { 0.0 }; + XContentBuilder json = jsonBuilder().startObject().field("coordinates", coords).field("type", "Point").endObject(); + try (XContentParser parser = createParser(json)) { + parser.nextToken(); + Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); + assertThat(e.getMessage(), is("GeoJSON 'coordinates' must contain at least two values")); + assertThat(parser.currentToken(), is(Token.END_OBJECT)); + assertNull(parser.nextToken()); + } + } + + public void testParseGeoPointCoordinatesTooLong() throws IOException { + double[] coords = new double[] { 0.0, 0.0, 0.0 }; + XContentBuilder json = jsonBuilder().startObject().field("coordinates", coords).field("type", "Point").endObject(); + try (XContentParser parser = createParser(json)) { + parser.nextToken(); + Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); + assertThat(e.getMessage(), containsString("found Z value [0.0] but [ignore_z_value] parameter is [false]")); + assertThat(parser.currentToken(), is(Token.END_OBJECT)); + assertNull(parser.nextToken()); + } + } + + public void testParseGeoPointCoordinatesWayTooLong() throws IOException { + double[] coords = new double[] { 0.0, 0.0, 0.0, 0.0 }; + XContentBuilder json = jsonBuilder().startObject().field("coordinates", coords).field("type", "Point").endObject(); + try (XContentParser parser = createParser(json)) { + parser.nextToken(); + Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); + assertThat(e.getMessage(), is("[geo_point] field type does not accept > 3 dimensions")); + assertThat(parser.currentToken(), is(Token.END_OBJECT)); + assertNull(parser.nextToken()); + } + } + public void testParseGeoPointExtraField() throws IOException { double lat = 0.0; double lon = 0.0; @@ -597,7 +691,7 @@ public void testParseGeoPointExtraField() throws IOException { try (XContentParser parser = createParser(json)) { parser.nextToken(); Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); - assertThat(e.getMessage(), is("field must be either [lat], [lon] or [geohash]")); + assertThat(e.getMessage(), is("field must be either [lat], [lon], [geohash], [coordinates] or [type]")); } } @@ -609,7 +703,7 @@ public void testParseGeoPointLonLatGeoHash() throws IOException { try (XContentParser parser = createParser(json)) { parser.nextToken(); Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); - assertThat(e.getMessage(), containsString("field must be either lat/lon or geohash")); + assertThat(e.getMessage(), containsString("fields matching more than one point format found")); } } diff --git a/server/src/test/java/org/elasticsearch/search/geo/GeoPointShapeQueryTests.java b/server/src/test/java/org/elasticsearch/search/geo/GeoPointShapeQueryTests.java index 77f60c32e14f8..63c651107735b 100644 --- a/server/src/test/java/org/elasticsearch/search/geo/GeoPointShapeQueryTests.java +++ b/server/src/test/java/org/elasticsearch/search/geo/GeoPointShapeQueryTests.java @@ -10,6 +10,7 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.geo.GeoJson; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.geo.GeometryTestUtils; import org.elasticsearch.geometry.Point; @@ -18,6 +19,7 @@ import org.elasticsearch.xcontent.XContentFactory; import java.io.IOException; +import java.util.Map; import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; import static org.elasticsearch.index.query.QueryBuilders.geoShapeQuery; @@ -67,4 +69,20 @@ public void testFieldAlias() throws IOException { SearchResponse response = client().prepareSearch(defaultIndexName).setQuery(geoShapeQuery("alias", point)).get(); assertEquals(1, response.getHits().getTotalHits().value); } + + /** + * Produce an array of objects each representing a single point in a variety of + * supported point formats. For `geo_shape` we only support GeoJSON and WKT, + * while for `geo_point` we support a variety of additional special case formats. + * Therefor we define here sample data for double[]{lon,lat} as well as + * a string "lat,lon". + */ + @Override + protected Object[] samplePointDataMultiFormat(Point pointA, Point pointB, Point pointC, Point pointD) { + String str = "" + pointA.getLat() + ", " + pointA.getLon(); + String wkt = WellKnownText.toWKT(pointB); + double[] pointDoubles = new double[] { pointC.getLon(), pointC.getLat() }; + Map geojson = GeoJson.toMap(pointD); + return new Object[] { str, wkt, pointDoubles, geojson }; + } } diff --git a/test/framework/src/main/java/org/elasticsearch/search/geo/GeoPointShapeQueryTestCase.java b/test/framework/src/main/java/org/elasticsearch/search/geo/GeoPointShapeQueryTestCase.java index c860ac4b4cfcd..87859daaa0d8d 100644 --- a/test/framework/src/main/java/org/elasticsearch/search/geo/GeoPointShapeQueryTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/search/geo/GeoPointShapeQueryTestCase.java @@ -9,11 +9,13 @@ package org.elasticsearch.search.geo; import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.geo.GeoJson; import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.geometry.Circle; @@ -27,6 +29,7 @@ import org.elasticsearch.geometry.Polygon; import org.elasticsearch.geometry.Rectangle; import org.elasticsearch.geometry.ShapeType; +import org.elasticsearch.geometry.utils.WellKnownText; import org.elasticsearch.index.query.GeoShapeQueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.plugins.Plugin; @@ -39,6 +42,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; @@ -583,4 +587,112 @@ public void testQueryMultiPoint() throws Exception { assertEquals(0, searchHits.getTotalHits().value); } } + + public void testQueryPointFromGeoJSON() throws Exception { + createMapping(defaultIndexName, defaultGeoFieldName); + ensureGreen(); + + String doc1 = """ + { + "geo": { + "coordinates": [ -35, -25.0 ], + "type": "Point" + } + }"""; + client().index(new IndexRequest(defaultIndexName).id("1").source(doc1, XContentType.JSON).setRefreshPolicy(IMMEDIATE)).actionGet(); + + Point point = new Point(-35, -25); + { + SearchResponse response = client().prepareSearch(defaultIndexName) + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, point)) + .get(); + SearchHits searchHits = response.getHits(); + assertEquals(1, searchHits.getTotalHits().value); + } + { + SearchResponse response = client().prepareSearch(defaultIndexName) + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, point).relation(ShapeRelation.WITHIN)) + .get(); + SearchHits searchHits = response.getHits(); + assertEquals(1, searchHits.getTotalHits().value); + } + { + SearchResponse response = client().prepareSearch(defaultIndexName) + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, point).relation(ShapeRelation.CONTAINS)) + .get(); + SearchHits searchHits = response.getHits(); + assertEquals(1, searchHits.getTotalHits().value); + } + { + SearchResponse response = client().prepareSearch(defaultIndexName) + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, point).relation(ShapeRelation.DISJOINT)) + .get(); + SearchHits searchHits = response.getHits(); + assertEquals(0, searchHits.getTotalHits().value); + } + } + + /** + * Produce an array of objects each representing a single point in a variety of + * supported point formats. For `geo_shape` we only support GeoJSON and WKT, + * while for `geo_point` we support a variety of additional special case formats. + * This method is therefor overridden in the tests for `geo_point` (@see GeoPointShapeQueryTests). + */ + protected Object[] samplePointDataMultiFormat(Point pointA, Point pointB, Point pointC, Point pointD) { + String wktA = WellKnownText.toWKT(pointA); + String wktB = WellKnownText.toWKT(pointB); + Map geojsonC = GeoJson.toMap(pointC); + Map geojsonD = GeoJson.toMap(pointD); + return new Object[] { wktA, wktB, geojsonC, geojsonD }; + } + + public void testQueryPointFromMultiPoint() throws Exception { + createMapping(defaultIndexName, defaultGeoFieldName); + ensureGreen(); + + Point pointA = new Point(-45, -35); + Point pointB = new Point(-35, -25); + Point pointC = new Point(35, 25); + Point pointD = new Point(45, 35); + Object[] points = samplePointDataMultiFormat(pointA, pointB, pointC, pointD); + client().prepareIndex(defaultIndexName) + .setId("1") + .setSource(jsonBuilder().startObject().field(defaultGeoFieldName, points).endObject()) + .setRefreshPolicy(IMMEDIATE) + .get(); + + Point pointInvalid = new Point(-35, -35); + for (Point point : new Point[] { pointA, pointB, pointC, pointD, pointInvalid }) { + int expectedDocs = point.equals(pointInvalid) ? 0 : 1; + int disjointDocs = point.equals(pointInvalid) ? 1 : 0; + { + SearchResponse response = client().prepareSearch(defaultIndexName) + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, point)) + .get(); + SearchHits searchHits = response.getHits(); + assertEquals("Doc matches %s" + point, expectedDocs, searchHits.getTotalHits().value); + } + { + SearchResponse response = client().prepareSearch(defaultIndexName) + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, point).relation(ShapeRelation.WITHIN)) + .get(); + SearchHits searchHits = response.getHits(); + assertEquals("Doc WITHIN %s" + point, 0, searchHits.getTotalHits().value); + } + { + SearchResponse response = client().prepareSearch(defaultIndexName) + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, point).relation(ShapeRelation.CONTAINS)) + .get(); + SearchHits searchHits = response.getHits(); + assertEquals("Doc CONTAINS %s" + point, expectedDocs, searchHits.getTotalHits().value); + } + { + SearchResponse response = client().prepareSearch(defaultIndexName) + .setQuery(QueryBuilders.geoShapeQuery(defaultGeoFieldName, point).relation(ShapeRelation.DISJOINT)) + .get(); + SearchHits searchHits = response.getHits(); + assertEquals("Doc DISJOINT with %s" + point, disjointDocs, searchHits.getTotalHits().value); + } + } + } }