Skip to content

Commit a851992

Browse files
Hoholimotov
authored andcommitted
Support WKT point conversion to geo_point type (#44107)
This PR adds support for parsing geo_point values from WKT POINT format. Also, a few minor bugs in geo_point parsing were fixed. Closes #41821
1 parent b3a7b22 commit a851992

File tree

6 files changed

+110
-83
lines changed

6 files changed

+110
-83
lines changed

docs/reference/mapping/types/geo-point.asciidoc

+11-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Fields of type `geo_point` accept latitude-longitude pairs, which can be used:
1111
* to integrate distance into a document's <<query-dsl-function-score-query,relevance score>>.
1212
* to <<geo-sorting,sort>> documents by distance.
1313

14-
There are four ways that a geo-point may be specified, as demonstrated below:
14+
There are five ways that a geo-point may be specified, as demonstrated below:
1515

1616
[source,js]
1717
--------------------------------------------------
@@ -53,10 +53,16 @@ PUT my_index/_doc/4
5353
"location": [ -71.34, 41.12 ] <4>
5454
}
5555
56+
PUT my_index/_doc/5
57+
{
58+
"text": "Geo-point as a WKT POINT primitive",
59+
"location" : "POINT (-71.34 41.12)" <5>
60+
}
61+
5662
GET my_index/_search
5763
{
5864
"query": {
59-
"geo_bounding_box": { <5>
65+
"geo_bounding_box": { <6>
6066
"location": {
6167
"top_left": {
6268
"lat": 42,
@@ -76,7 +82,9 @@ GET my_index/_search
7682
<2> Geo-point expressed as a string with the format: `"lat,lon"`.
7783
<3> Geo-point expressed as a geohash.
7884
<4> Geo-point expressed as an array with the format: [ `lon`, `lat`]
79-
<5> A geo-bounding box query which finds all geo-points that fall inside the box.
85+
<5> Geo-point expressed as a http://docs.opengeospatial.org/is/12-063r5/12-063r5.html[Well-Known Text]
86+
POINT with the format: `"POINT(lon lat)"`
87+
<6> A geo-bounding box query which finds all geo-points that fall inside the box.
8088

8189
[IMPORTANT]
8290
.Geo-points expressed as an array or string

server/src/main/java/org/elasticsearch/common/geo/GeoPoint.java

+47-4
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,21 @@
2525
import org.apache.lucene.index.IndexableField;
2626
import org.apache.lucene.util.BitUtil;
2727
import org.apache.lucene.util.BytesRef;
28+
import org.elasticsearch.common.geo.GeoUtils.EffectivePoint;
2829
import org.elasticsearch.common.xcontent.ToXContentFragment;
2930
import org.elasticsearch.common.xcontent.XContentBuilder;
3031
import org.elasticsearch.ElasticsearchParseException;
32+
import org.elasticsearch.geo.geometry.Geometry;
33+
import org.elasticsearch.geo.geometry.Point;
34+
import org.elasticsearch.geo.geometry.Rectangle;
35+
import org.elasticsearch.geo.geometry.ShapeType;
36+
import org.elasticsearch.geo.utils.GeographyValidator;
3137
import org.elasticsearch.geo.utils.Geohash;
38+
import org.elasticsearch.geo.utils.WellKnownText;
3239

3340
import java.io.IOException;
3441
import java.util.Arrays;
42+
import java.util.Locale;
3543

3644
import static org.elasticsearch.index.mapper.GeoPointFieldMapper.Names.IGNORE_Z_VALUE;
3745

@@ -79,14 +87,16 @@ public GeoPoint resetLon(double lon) {
7987
}
8088

8189
public GeoPoint resetFromString(String value) {
82-
return resetFromString(value, false);
90+
return resetFromString(value, false, EffectivePoint.BOTTOM_LEFT);
8391
}
8492

85-
public GeoPoint resetFromString(String value, final boolean ignoreZValue) {
86-
if (value.contains(",")) {
93+
public GeoPoint resetFromString(String value, final boolean ignoreZValue, EffectivePoint effectivePoint) {
94+
if (value.toLowerCase(Locale.ROOT).contains("point")) {
95+
return resetFromWKT(value, ignoreZValue);
96+
} else if (value.contains(",")) {
8797
return resetFromCoordinates(value, ignoreZValue);
8898
}
89-
return resetFromGeoHash(value);
99+
return parseGeoHash(value, effectivePoint);
90100
}
91101

92102

@@ -114,6 +124,39 @@ public GeoPoint resetFromCoordinates(String value, final boolean ignoreZValue) {
114124
return reset(lat, lon);
115125
}
116126

127+
private GeoPoint resetFromWKT(String value, boolean ignoreZValue) {
128+
Geometry geometry;
129+
try {
130+
geometry = new WellKnownText(false, new GeographyValidator(ignoreZValue))
131+
.fromWKT(value);
132+
} catch (Exception e) {
133+
throw new ElasticsearchParseException("Invalid WKT format", e);
134+
}
135+
if (geometry.type() != ShapeType.POINT) {
136+
throw new ElasticsearchParseException("[geo_point] supports only POINT among WKT primitives, " +
137+
"but found " + geometry.type());
138+
}
139+
Point point = (Point) geometry;
140+
return reset(point.getLat(), point.getLon());
141+
}
142+
143+
GeoPoint parseGeoHash(String geohash, EffectivePoint effectivePoint) {
144+
if (effectivePoint == EffectivePoint.BOTTOM_LEFT) {
145+
return resetFromGeoHash(geohash);
146+
} else {
147+
Rectangle rectangle = Geohash.toBoundingBox(geohash);
148+
switch (effectivePoint) {
149+
case TOP_LEFT:
150+
return reset(rectangle.getMaxLat(), rectangle.getMinLon());
151+
case TOP_RIGHT:
152+
return reset(rectangle.getMaxLat(), rectangle.getMaxLon());
153+
case BOTTOM_RIGHT:
154+
return reset(rectangle.getMinLat(), rectangle.getMaxLon());
155+
default:
156+
throw new IllegalArgumentException("Unsupported effective point " + effectivePoint);
157+
}
158+
}
159+
}
117160

118161
public GeoPoint resetFromIndexHash(long hash) {
119162
lon = Geohash.decodeLongitude(hash);

server/src/main/java/org/elasticsearch/common/geo/GeoUtils.java

+6-34
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@
3131
import org.elasticsearch.common.xcontent.XContentSubParser;
3232
import org.elasticsearch.common.xcontent.support.MapXContentParser;
3333
import org.elasticsearch.common.xcontent.support.XContentMapValues;
34-
import org.elasticsearch.geo.geometry.Rectangle;
35-
import org.elasticsearch.geo.utils.Geohash;
3634
import org.elasticsearch.index.fielddata.FieldData;
3735
import org.elasticsearch.index.fielddata.GeoPointValues;
3836
import org.elasticsearch.index.fielddata.MultiGeoPointValues;
@@ -476,7 +474,7 @@ public static GeoPoint parseGeoPoint(XContentParser parser, GeoPoint point, fina
476474
if(!Double.isNaN(lat) || !Double.isNaN(lon)) {
477475
throw new ElasticsearchParseException("field must be either lat/lon or geohash");
478476
} else {
479-
return parseGeoHash(point, geohash, effectivePoint);
477+
return point.parseGeoHash(geohash, effectivePoint);
480478
}
481479
} else if (numberFormatException != null) {
482480
throw new ElasticsearchParseException("[{}] and [{}] must be valid double values", numberFormatException, LATITUDE,
@@ -499,8 +497,10 @@ public static GeoPoint parseGeoPoint(XContentParser parser, GeoPoint point, fina
499497
lon = subParser.doubleValue();
500498
} else if (element == 2) {
501499
lat = subParser.doubleValue();
502-
} else {
500+
} else if (element == 3) {
503501
GeoPoint.assertZValue(ignoreZValue, subParser.doubleValue());
502+
} else {
503+
throw new ElasticsearchParseException("[geo_point] field type does not accept > 3 dimensions");
504504
}
505505
} else {
506506
throw new ElasticsearchParseException("numeric value expected");
@@ -510,35 +510,12 @@ public static GeoPoint parseGeoPoint(XContentParser parser, GeoPoint point, fina
510510
return point.reset(lat, lon);
511511
} else if(parser.currentToken() == Token.VALUE_STRING) {
512512
String val = parser.text();
513-
if (val.contains(",")) {
514-
return point.resetFromString(val, ignoreZValue);
515-
} else {
516-
return parseGeoHash(point, val, effectivePoint);
517-
}
518-
513+
return point.resetFromString(val, ignoreZValue, effectivePoint);
519514
} else {
520515
throw new ElasticsearchParseException("geo_point expected");
521516
}
522517
}
523518

524-
private static GeoPoint parseGeoHash(GeoPoint point, String geohash, EffectivePoint effectivePoint) {
525-
if (effectivePoint == EffectivePoint.BOTTOM_LEFT) {
526-
return point.resetFromGeoHash(geohash);
527-
} else {
528-
Rectangle rectangle = Geohash.toBoundingBox(geohash);
529-
switch (effectivePoint) {
530-
case TOP_LEFT:
531-
return point.reset(rectangle.getMaxLat(), rectangle.getMinLon());
532-
case TOP_RIGHT:
533-
return point.reset(rectangle.getMaxLat(), rectangle.getMaxLon());
534-
case BOTTOM_RIGHT:
535-
return point.reset(rectangle.getMinLat(), rectangle.getMaxLon());
536-
default:
537-
throw new IllegalArgumentException("Unsupported effective point " + effectivePoint);
538-
}
539-
}
540-
}
541-
542519
/**
543520
* Parse a {@link GeoPoint} from a string. The string must have one of the following forms:
544521
*
@@ -552,12 +529,7 @@ private static GeoPoint parseGeoHash(GeoPoint point, String geohash, EffectivePo
552529
*/
553530
public static GeoPoint parseFromString(String val) {
554531
GeoPoint point = new GeoPoint();
555-
boolean ignoreZValue = false;
556-
if (val.contains(",")) {
557-
return point.resetFromString(val, ignoreZValue);
558-
} else {
559-
return parseGeoHash(point, val, EffectivePoint.BOTTOM_LEFT);
560-
}
532+
return point.resetFromString(val, false, EffectivePoint.BOTTOM_LEFT);
561533
}
562534

563535
/**

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

+13-42
Original file line numberDiff line numberDiff line change
@@ -301,38 +301,23 @@ public void parse(ParseContext context) throws IOException {
301301
XContentParser.Token token = context.parser().currentToken();
302302
if (token == XContentParser.Token.START_ARRAY) {
303303
token = context.parser().nextToken();
304-
if (token == XContentParser.Token.START_ARRAY) {
305-
// its an array of array of lon/lat [ [1.2, 1.3], [1.4, 1.5] ]
306-
while (token != XContentParser.Token.END_ARRAY) {
307-
parseGeoPointIgnoringMalformed(context, sparse);
308-
token = context.parser().nextToken();
304+
if (token == XContentParser.Token.VALUE_NUMBER) {
305+
double lon = context.parser().doubleValue();
306+
context.parser().nextToken();
307+
double lat = context.parser().doubleValue();
308+
token = context.parser().nextToken();
309+
if (token == XContentParser.Token.VALUE_NUMBER) {
310+
GeoPoint.assertZValue(ignoreZValue.value(), context.parser().doubleValue());
311+
} else if (token != XContentParser.Token.END_ARRAY) {
312+
throw new ElasticsearchParseException("[{}] field type does not accept > 3 dimensions", CONTENT_TYPE);
309313
}
314+
parse(context, sparse.reset(lat, lon));
310315
} else {
311-
// its an array of other possible values
312-
if (token == XContentParser.Token.VALUE_NUMBER) {
313-
double lon = context.parser().doubleValue();
314-
context.parser().nextToken();
315-
double lat = context.parser().doubleValue();
316+
while (token != XContentParser.Token.END_ARRAY) {
317+
parseGeoPointIgnoringMalformed(context, sparse);
316318
token = context.parser().nextToken();
317-
if (token == XContentParser.Token.VALUE_NUMBER) {
318-
GeoPoint.assertZValue(ignoreZValue.value(), context.parser().doubleValue());
319-
} else if (token != XContentParser.Token.END_ARRAY) {
320-
throw new ElasticsearchParseException("[{}] field type does not accept > 3 dimensions", CONTENT_TYPE);
321-
}
322-
parse(context, sparse.reset(lat, lon));
323-
} else {
324-
while (token != XContentParser.Token.END_ARRAY) {
325-
if (token == XContentParser.Token.VALUE_STRING) {
326-
parseGeoPointStringIgnoringMalformed(context, sparse);
327-
} else {
328-
parseGeoPointIgnoringMalformed(context, sparse);
329-
}
330-
token = context.parser().nextToken();
331-
}
332319
}
333320
}
334-
} else if (token == XContentParser.Token.VALUE_STRING) {
335-
parseGeoPointStringIgnoringMalformed(context, sparse);
336321
} else if (token == XContentParser.Token.VALUE_NULL) {
337322
if (fieldType.nullValue() != null) {
338323
parse(context, (GeoPoint) fieldType.nullValue());
@@ -353,21 +338,7 @@ public void parse(ParseContext context) throws IOException {
353338
*/
354339
private void parseGeoPointIgnoringMalformed(ParseContext context, GeoPoint sparse) throws IOException {
355340
try {
356-
parse(context, GeoUtils.parseGeoPoint(context.parser(), sparse));
357-
} catch (ElasticsearchParseException e) {
358-
if (ignoreMalformed.value() == false) {
359-
throw e;
360-
}
361-
context.addIgnoredField(fieldType.name());
362-
}
363-
}
364-
365-
/**
366-
* Parses geopoint represented as a string and ignores malformed geopoints if needed
367-
*/
368-
private void parseGeoPointStringIgnoringMalformed(ParseContext context, GeoPoint sparse) throws IOException {
369-
try {
370-
parse(context, sparse.resetFromString(context.parser().text(), ignoreZValue.value()));
341+
parse(context, GeoUtils.parseGeoPoint(context.parser(), sparse, ignoreZValue.value()));
371342
} catch (ElasticsearchParseException e) {
372343
if (ignoreMalformed.value() == false) {
373344
throw e;

server/src/test/java/org/elasticsearch/index/mapper/GeoPointFieldMapperTests.java

+17
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,23 @@ public void testGeoHashValue() throws Exception {
7575
assertThat(doc.rootDoc().getField("point"), notNullValue());
7676
}
7777

78+
public void testWKT() throws Exception {
79+
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type")
80+
.startObject("properties").startObject("point").field("type", "geo_point");
81+
String mapping = Strings.toString(xContentBuilder.endObject().endObject().endObject().endObject());
82+
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
83+
.parse("type", new CompressedXContent(mapping));
84+
85+
ParsedDocument doc = defaultMapper.parse(new SourceToParse("test", "type", "1",
86+
BytesReference.bytes(XContentFactory.jsonBuilder()
87+
.startObject()
88+
.field("point", "POINT (2 3)")
89+
.endObject()),
90+
XContentType.JSON));
91+
92+
assertThat(doc.rootDoc().getField("point"), notNullValue());
93+
}
94+
7895
public void testLatLonValuesStored() throws Exception {
7996
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type")
8097
.startObject("properties").startObject("point").field("type", "geo_point");

server/src/test/java/org/elasticsearch/index/search/geo/GeoPointParsingTests.java

+16
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@ public void testGeoPointReset() throws IOException {
5757
assertPointsEqual(point.reset(0, 0), point2.reset(0, 0));
5858
assertPointsEqual(point.resetFromString(Double.toString(lat) + ", " + Double.toHexString(lon)), point2.reset(lat, lon));
5959
assertPointsEqual(point.reset(0, 0), point2.reset(0, 0));
60+
assertPointsEqual(point.resetFromString("POINT(" + lon + " " + lat + ")"), point2.reset(lat, lon));
61+
}
62+
63+
public void testParseWktInvalid() {
64+
GeoPoint point = new GeoPoint(0, 0);
65+
Exception e = expectThrows(
66+
ElasticsearchParseException.class,
67+
() -> point.resetFromString("NOT A POINT(1 2)")
68+
);
69+
assertEquals("Invalid WKT format", e.getMessage());
70+
71+
Exception e2 = expectThrows(
72+
ElasticsearchParseException.class,
73+
() -> point.resetFromString("MULTIPOINT(1 2, 3 4)")
74+
);
75+
assertEquals("[geo_point] supports only POINT among WKT primitives, but found MULTIPOINT", e2.getMessage());
6076
}
6177

6278
public void testEqualsHashCodeContract() {

0 commit comments

Comments
 (0)