Skip to content

Commit 568d248

Browse files
committed
Support spatial fields in field retrieval API. (#59821)
Although we accept a variety of formats during indexing, spatial data is returned in a single consistent format. This is GeoJSON by default, but well-known text is also supported by passing the option 'format: wkt'. Note that points (in addition to shapes) are returned in GeoJSON by default. The reasoning is that this gives better consistency, and is the most convenient format for most REST API users.
1 parent e033880 commit 568d248

File tree

24 files changed

+661
-127
lines changed

24 files changed

+661
-127
lines changed

docs/reference/mapping/types.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ string:: <<text,`text`>>, <<keyword,`keyword`>> and <<wildcard,`wildcard
2222
<<nested>>:: `nested` for arrays of JSON objects
2323

2424
[float]
25+
[[spatial_datatypes]]
2526
=== Spatial data types
2627

2728
<<geo-point>>:: `geo_point` for lat/lon points

docs/reference/search/search-fields.asciidoc

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,13 @@ POST twitter/_search
8787

8888
<1> Both full field names and wildcard patterns are accepted.
8989
<2> Using object notation, you can pass a `format` parameter to apply a custom
90-
format for the field's values. This is currently supported for
91-
<<date,`date` fields>> and <<date_nanos, `date_nanos` fields>>, which
92-
accept a <<mapping-date-format,date format>>.
90+
format for the field's values. The date fields
91+
<<date,`date`>> and <<date_nanos, `date_nanos`>> accept a
92+
<<mapping-date-format,date format>>. <<spatial_datatypes, Spatial fields>>
93+
accept either `geojson` for http://www.geojson.org[GeoJSON] (the default)
94+
or `wkt` for
95+
https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry[Well Known Text].
96+
Other field types do not support the `format` parameter.
9397

9498
The values are returned as a flat list in the `fields` section in each hit:
9599

modules/geo/src/yamlRestTest/resources/rest-api-spec/test/geo_shape/10_basic.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,26 @@ setup:
5757
field: location
5858

5959
- match: {hits.total: 1}
60+
61+
---
62+
"Test retrieve geo_shape field":
63+
- do:
64+
search:
65+
index: test
66+
body:
67+
fields: [location]
68+
_source: false
69+
70+
- match: { hits.hits.0.fields.location.0.type: "Point" }
71+
- match: { hits.hits.0.fields.location.0.coordinates: [1.0, 1.0] }
72+
73+
- do:
74+
search:
75+
index: test
76+
body:
77+
fields:
78+
- field: location
79+
format: wkt
80+
_source: false
81+
82+
- match: { hits.hits.0.fields.location.0: "POINT (1.0 1.0)" }

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,5 +609,4 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
609609
return builder;
610610
}
611611
}
612-
613612
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.common.geo;
21+
22+
import org.elasticsearch.common.bytes.BytesReference;
23+
import org.elasticsearch.common.io.stream.StreamInput;
24+
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
25+
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
26+
import org.elasticsearch.common.xcontent.ToXContent;
27+
import org.elasticsearch.common.xcontent.XContentBuilder;
28+
import org.elasticsearch.common.xcontent.XContentFactory;
29+
import org.elasticsearch.common.xcontent.XContentParser;
30+
import org.elasticsearch.common.xcontent.XContentType;
31+
import org.elasticsearch.geometry.Geometry;
32+
33+
import java.io.IOException;
34+
import java.io.UncheckedIOException;
35+
36+
public class GeoJsonGeometryFormat implements GeometryFormat<Geometry> {
37+
public static final String NAME = "geojson";
38+
39+
private final GeoJson geoJsonParser;
40+
41+
public GeoJsonGeometryFormat(GeoJson geoJsonParser) {
42+
this.geoJsonParser = geoJsonParser;
43+
}
44+
45+
@Override
46+
public String name() {
47+
return NAME;
48+
}
49+
50+
@Override
51+
public Geometry fromXContent(XContentParser parser) throws IOException {
52+
if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
53+
return null;
54+
}
55+
return geoJsonParser.fromXContent(parser);
56+
}
57+
58+
@Override
59+
public XContentBuilder toXContent(Geometry geometry, XContentBuilder builder, ToXContent.Params params) throws IOException {
60+
if (geometry != null) {
61+
return GeoJson.toXContent(geometry, builder, params);
62+
} else {
63+
return builder.nullValue();
64+
}
65+
}
66+
67+
@Override
68+
public Object toXContentAsObject(Geometry geometry) {
69+
try {
70+
XContentBuilder builder = XContentFactory.jsonBuilder();
71+
GeoJson.toXContent(geometry, builder, ToXContent.EMPTY_PARAMS);
72+
StreamInput input = BytesReference.bytes(builder).streamInput();
73+
74+
try (XContentParser parser = XContentType.JSON.xContent()
75+
.createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, input)) {
76+
return parser.map();
77+
}
78+
} catch (IOException e) {
79+
throw new UncheckedIOException(e);
80+
}
81+
}
82+
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
*/
3232
public interface GeometryFormat<ParsedFormat> {
3333

34+
/**
35+
* The name of the format, for example 'wkt'.
36+
*/
37+
String name();
38+
3439
/**
3540
* Parser JSON representation of a geometry
3641
*/
@@ -41,4 +46,10 @@ public interface GeometryFormat<ParsedFormat> {
4146
*/
4247
XContentBuilder toXContent(ParsedFormat geometry, XContentBuilder builder, ToXContent.Params params) throws IOException;
4348

49+
/**
50+
* Serializes the geometry into a standard Java object.
51+
*
52+
* For example, the GeoJson format returns the geometry as a map, while WKT returns a string.
53+
*/
54+
Object toXContentAsObject(ParsedFormat geometry);
4455
}

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

Lines changed: 23 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,13 @@
2222
import org.elasticsearch.ElasticsearchParseException;
2323
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
2424
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
25-
import org.elasticsearch.common.xcontent.ToXContent;
26-
import org.elasticsearch.common.xcontent.XContentBuilder;
2725
import org.elasticsearch.common.xcontent.XContentParser;
2826
import org.elasticsearch.common.xcontent.support.MapXContentParser;
2927
import org.elasticsearch.geometry.Geometry;
3028
import org.elasticsearch.geometry.GeometryCollection;
3129
import org.elasticsearch.geometry.Point;
32-
import org.elasticsearch.geometry.utils.StandardValidator;
3330
import org.elasticsearch.geometry.utils.GeometryValidator;
31+
import org.elasticsearch.geometry.utils.StandardValidator;
3432
import org.elasticsearch.geometry.utils.WellKnownText;
3533

3634
import java.io.IOException;
@@ -66,59 +64,31 @@ public Geometry parse(XContentParser parser) throws IOException, ParseException
6664
/**
6765
* Returns a geometry format object that can parse and then serialize the object back to the same format.
6866
*/
69-
public GeometryFormat<Geometry> geometryFormat(XContentParser parser) {
70-
if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
71-
return new GeometryFormat<Geometry>() {
72-
@Override
73-
public Geometry fromXContent(XContentParser parser) throws IOException {
74-
return null;
75-
}
76-
77-
@Override
78-
public XContentBuilder toXContent(Geometry geometry, XContentBuilder builder, ToXContent.Params params) throws IOException {
79-
if (geometry != null) {
80-
// We don't know the format of the original geometry - so going with default
81-
return GeoJson.toXContent(geometry, builder, params);
82-
} else {
83-
return builder.nullValue();
84-
}
85-
}
86-
};
87-
} else if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
88-
return new GeometryFormat<Geometry>() {
89-
@Override
90-
public Geometry fromXContent(XContentParser parser) throws IOException {
91-
return geoJsonParser.fromXContent(parser);
92-
}
67+
public GeometryFormat<Geometry> geometryFormat(String format) {
68+
if (format.equals(GeoJsonGeometryFormat.NAME)) {
69+
return new GeoJsonGeometryFormat(geoJsonParser);
70+
} else if (format.equals(WKTGeometryFormat.NAME)) {
71+
return new WKTGeometryFormat(wellKnownTextParser);
72+
} else {
73+
throw new IllegalArgumentException("Unrecognized geometry format [" + format + "].");
74+
}
75+
}
9376

94-
@Override
95-
public XContentBuilder toXContent(Geometry geometry, XContentBuilder builder, ToXContent.Params params) throws IOException {
96-
if (geometry != null) {
97-
return GeoJson.toXContent(geometry, builder, params);
98-
} else {
99-
return builder.nullValue();
100-
}
101-
}
102-
};
77+
/**
78+
* Returns a geometry format object that can parse and then serialize the object back to the same format.
79+
* This method automatically recognizes the format by examining the provided {@link XContentParser}.
80+
*/
81+
public GeometryFormat<Geometry> geometryFormat(XContentParser parser) {
82+
if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
83+
return new GeoJsonGeometryFormat(geoJsonParser);
10384
} else if (parser.currentToken() == XContentParser.Token.VALUE_STRING) {
104-
return new GeometryFormat<Geometry>() {
105-
@Override
106-
public Geometry fromXContent(XContentParser parser) throws IOException, ParseException {
107-
return wellKnownTextParser.fromWKT(parser.text());
108-
}
109-
110-
@Override
111-
public XContentBuilder toXContent(Geometry geometry, XContentBuilder builder, ToXContent.Params params) throws IOException {
112-
if (geometry != null) {
113-
return builder.value(wellKnownTextParser.toWKT(geometry));
114-
} else {
115-
return builder.nullValue();
116-
}
117-
}
118-
};
119-
85+
return new WKTGeometryFormat(wellKnownTextParser);
86+
} else if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
87+
// We don't know the format of the original geometry - so going with default
88+
return new GeoJsonGeometryFormat(geoJsonParser);
89+
} else {
90+
throw new ElasticsearchParseException("shape must be an object consisting of type and coordinates");
12091
}
121-
throw new ElasticsearchParseException("shape must be an object consisting of type and coordinates");
12292
}
12393

12494
/**
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.common.geo;
21+
22+
import org.elasticsearch.common.xcontent.ToXContent;
23+
import org.elasticsearch.common.xcontent.XContentBuilder;
24+
import org.elasticsearch.common.xcontent.XContentParser;
25+
import org.elasticsearch.geometry.Geometry;
26+
import org.elasticsearch.geometry.utils.WellKnownText;
27+
28+
import java.io.IOException;
29+
import java.text.ParseException;
30+
31+
public class WKTGeometryFormat implements GeometryFormat<Geometry> {
32+
public static final String NAME = "wkt";
33+
34+
private final WellKnownText wellKnownTextParser;
35+
36+
public WKTGeometryFormat(WellKnownText wellKnownTextParser) {
37+
this.wellKnownTextParser = wellKnownTextParser;
38+
}
39+
40+
@Override
41+
public String name() {
42+
return NAME;
43+
}
44+
45+
@Override
46+
public Geometry fromXContent(XContentParser parser) throws IOException, ParseException {
47+
if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
48+
return null;
49+
}
50+
return wellKnownTextParser.fromWKT(parser.text());
51+
}
52+
53+
@Override
54+
public XContentBuilder toXContent(Geometry geometry, XContentBuilder builder, ToXContent.Params params) throws IOException {
55+
if (geometry != null) {
56+
return builder.value(wellKnownTextParser.toWKT(geometry));
57+
} else {
58+
return builder.nullValue();
59+
}
60+
}
61+
62+
@Override
63+
public String toXContentAsObject(Geometry geometry) {
64+
return wellKnownTextParser.toWKT(geometry);
65+
}
66+
}

0 commit comments

Comments
 (0)