Browse Source

Support GeoJSON for geo_point (#85120)

Support GeoJSON for points when the mapper specifies geo_point. Before this, only geo_shape supported GeoJSON even for point data.

This PR also adds tests for previously existing, but not tested features like adding an array of points where each point in the array was a different format. This was working, and is now tested, and also includes the new GeoJSON format.
Craig Taverner 3 years ago
parent
commit
042b96437d

+ 5 - 0
docs/changelog/85120.yaml

@@ -0,0 +1,5 @@
+pr: 85120
+summary: Support GeoJSON for `geo_point`
+area: Geo
+type: enhancement
+issues: []

+ 96 - 46
server/src/main/java/org/elasticsearch/common/geo/GeoUtils.java

@@ -26,7 +26,9 @@ import org.elasticsearch.xcontent.XContentSubParser;
 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 class GeoUtils {
         double lat = Double.NaN;
         double lon = Double.NaN;
         String geohash = null;
-        NumberFormatException numberFormatException = null;
+        String geojsonType = null;
+        ArrayList<Double> 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 class GeoUtils {
         }
     }
 
+    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<String>();
+        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:
      *

+ 10 - 0
server/src/test/java/org/elasticsearch/index/mapper/GeoPointFieldTypeTests.java

@@ -71,6 +71,16 @@ public class GeoPointFieldTypeTests extends FieldTypeTestCase {
             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 {

+ 39 - 42
server/src/test/java/org/elasticsearch/index/search/geo/GeoPointParsingTests.java

@@ -10,8 +10,10 @@ package org.elasticsearch.index.search.geo;
 
 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.XContentParser;
 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 class GeoPointParsingTests extends ESTestCase {
         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 class GeoPointParsingTests extends ESTestCase {
         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<String, Object> 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 class GeoPointParsingTests extends ESTestCase {
         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]"));
         }
     }
 

+ 96 - 2
server/src/test/java/org/elasticsearch/index/search/geo/GeoUtilsTests.java

@@ -590,6 +590,100 @@ public class GeoUtilsTests extends ESTestCase {
         }
     }
 
+    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 class GeoUtilsTests extends ESTestCase {
         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 class GeoUtilsTests extends ESTestCase {
         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"));
         }
     }
 

+ 18 - 0
server/src/test/java/org/elasticsearch/search/geo/GeoPointShapeQueryTests.java

@@ -10,6 +10,7 @@ package org.elasticsearch.search.geo;
 
 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.XContentBuilder;
 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 class GeoPointShapeQueryTests extends GeoPointShapeQueryTestCase {
         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 <code>double[]{lon,lat}</code> 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<String, Object> geojson = GeoJson.toMap(pointD);
+        return new Object[] { str, wkt, pointDoubles, geojson };
+    }
 }

+ 112 - 0
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.Point;
 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 org.elasticsearch.xcontent.XContentType;
 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 abstract class GeoPointShapeQueryTestCase extends ESSingleNodeTestCase {
             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<String, Object> geojsonC = GeoJson.toMap(pointC);
+        Map<String, Object> 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);
+            }
+        }
+    }
 }