Skip to content

Commit 156d741

Browse files
committed
Support spatial fields in field retrieval API.
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 1de5a51 commit 156d741

File tree

20 files changed

+427
-30
lines changed

20 files changed

+427
-30
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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,21 @@
2222
import org.elasticsearch.ElasticsearchException;
2323
import org.elasticsearch.ElasticsearchParseException;
2424
import org.elasticsearch.common.ParseField;
25+
import org.elasticsearch.common.bytes.BytesReference;
2526
import org.elasticsearch.common.geo.parsers.ShapeParser;
27+
import org.elasticsearch.common.io.stream.StreamInput;
2628
import org.elasticsearch.common.unit.DistanceUnit;
2729
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
30+
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
31+
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
2832
import org.elasticsearch.common.xcontent.ObjectParser;
2933
import org.elasticsearch.common.xcontent.ToXContent;
3034
import org.elasticsearch.common.xcontent.ToXContentObject;
3135
import org.elasticsearch.common.xcontent.XContentBuilder;
36+
import org.elasticsearch.common.xcontent.XContentFactory;
3237
import org.elasticsearch.common.xcontent.XContentParser;
3338
import org.elasticsearch.common.xcontent.XContentSubParser;
39+
import org.elasticsearch.common.xcontent.XContentType;
3440
import org.elasticsearch.geometry.Circle;
3541
import org.elasticsearch.geometry.Geometry;
3642
import org.elasticsearch.geometry.GeometryCollection;
@@ -50,6 +56,7 @@
5056
import java.util.ArrayList;
5157
import java.util.List;
5258
import java.util.Locale;
59+
import java.util.Map;
5360

5461
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
5562
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
@@ -610,4 +617,14 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
610617
}
611618
}
612619

620+
public static Map<?, ?> toXContentMap(Geometry geometry) throws IOException {
621+
XContentBuilder builder = XContentFactory.jsonBuilder();
622+
GeoJson.toXContent(geometry, builder, ToXContent.EMPTY_PARAMS);
623+
StreamInput input = BytesReference.bytes(builder).streamInput();
624+
625+
try (XContentParser parser = XContentType.JSON.xContent()
626+
.createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, input)) {
627+
return parser.map();
628+
}
629+
}
613630
}

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,21 @@
3030
import org.elasticsearch.common.geo.ShapeRelation;
3131
import org.elasticsearch.common.geo.SpatialStrategy;
3232
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
33+
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
3334
import org.elasticsearch.common.xcontent.XContentBuilder;
3435
import org.elasticsearch.common.xcontent.XContentParser;
36+
import org.elasticsearch.common.xcontent.XContentType;
37+
import org.elasticsearch.common.xcontent.support.MapXContentParser;
3538
import org.elasticsearch.common.xcontent.support.XContentMapValues;
3639
import org.elasticsearch.geometry.Geometry;
3740
import org.elasticsearch.index.query.QueryShardContext;
3841
import org.elasticsearch.index.query.QueryShardException;
3942

4043
import java.io.IOException;
44+
import java.io.UncheckedIOException;
4145
import java.text.ParseException;
4246
import java.util.ArrayList;
47+
import java.util.Collections;
4348
import java.util.HashMap;
4449
import java.util.Iterator;
4550
import java.util.List;
@@ -82,6 +87,11 @@ public interface Parser<Parsed> {
8287
Parsed parse(XContentParser parser, AbstractGeometryFieldMapper mapper) throws IOException, ParseException;
8388
}
8489

90+
public interface Formatter<Parsed> {
91+
Object formatGeoJson(Parsed value);
92+
Object formatWKT(Parsed value);
93+
}
94+
8595
public abstract static class Builder<T extends Builder<T, FT>, FT extends AbstractGeometryFieldType>
8696
extends FieldMapper.Builder<T> {
8797
protected Boolean ignoreMalformed;
@@ -143,7 +153,31 @@ public Builder ignoreZValue(final boolean ignoreZValue) {
143153

144154
@Override
145155
protected Object parseSourceValue(Object value, String format) {
146-
throw new UnsupportedOperationException();
156+
AbstractGeometryFieldType<Parsed, Processed> mappedFieldType = fieldType();
157+
Parser<Parsed> geometryParser = mappedFieldType.geometryParser();
158+
Formatter<Parsed> geometryFormatter = mappedFieldType.geometryFormatter();
159+
160+
Parsed geometry;
161+
try (XContentParser parser = new MapXContentParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE,
162+
Collections.singletonMap("dummy_field", value), XContentType.JSON)) {
163+
parser.nextToken(); // start object
164+
parser.nextToken(); // field name
165+
parser.nextToken(); // field value
166+
geometry = geometryParser.parse(parser, this);
167+
} catch (IOException e) {
168+
throw new UncheckedIOException(e);
169+
} catch (ParseException e) {
170+
throw new RuntimeException(e);
171+
}
172+
173+
if (format == null || format.equals("geojson")) {
174+
return geometryFormatter.formatGeoJson(geometry);
175+
} else if (format.equals("wkt")) {
176+
return geometryFormatter.formatWKT(geometry);
177+
} else {
178+
throw new IllegalArgumentException("Encountered an unsupported format [" + format + "] when " +
179+
"loading values for the [" + contentType() +"] field named [" + name() + "]");
180+
}
147181
}
148182

149183
public abstract static class TypeParser<T extends Builder> implements Mapper.TypeParser {
@@ -187,6 +221,7 @@ public T parse(String name, Map<String, Object> node, ParserContext parserContex
187221
public abstract static class AbstractGeometryFieldType<Parsed, Processed> extends MappedFieldType {
188222
protected Indexer<Parsed, Processed> geometryIndexer;
189223
protected Parser<Parsed> geometryParser;
224+
protected Formatter<Parsed> geometryFormatter;
190225
protected QueryProcessor geometryQueryBuilder;
191226

192227
protected AbstractGeometryFieldType(String name, boolean indexed, boolean hasDocValues, Map<String, String> meta) {
@@ -213,6 +248,14 @@ protected Parser<Parsed> geometryParser() {
213248
return geometryParser;
214249
}
215250

251+
public void setGeometryFormatter(Formatter<Parsed> geometryFormatter) {
252+
this.geometryFormatter = geometryFormatter;
253+
}
254+
255+
protected Formatter<Parsed> geometryFormatter() {
256+
return geometryFormatter;
257+
}
258+
216259
public QueryProcessor geometryQueryBuilder() {
217260
return geometryQueryBuilder;
218261
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,17 @@
2222
import org.elasticsearch.ElasticsearchParseException;
2323
import org.elasticsearch.common.Explicit;
2424
import org.elasticsearch.common.ParseField;
25+
import org.elasticsearch.common.geo.GeoJson;
2526
import org.elasticsearch.common.geo.GeoPoint;
2627
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
2728
import org.elasticsearch.common.xcontent.XContentBuilder;
2829
import org.elasticsearch.common.xcontent.XContentParser;
30+
import org.elasticsearch.geometry.Geometry;
31+
import org.elasticsearch.geometry.Point;
32+
import org.elasticsearch.geometry.utils.WellKnownText;
2933

3034
import java.io.IOException;
35+
import java.io.UncheckedIOException;
3136
import java.text.ParseException;
3237
import java.util.ArrayList;
3338
import java.util.Iterator;
@@ -158,6 +163,7 @@ public interface ParsedPoint {
158163
void validate(String fieldName);
159164
void normalize(String fieldName);
160165
void resetCoords(double x, double y);
166+
Point asGeometry();
161167
default boolean isNormalizable(double coord) {
162168
return Double.isNaN(coord) == false && Double.isInfinite(coord) == false;
163169
}
@@ -239,4 +245,30 @@ public List<P> parse(XContentParser parser, AbstractGeometryFieldMapper geometry
239245
}
240246
}
241247
}
248+
249+
public static class PointFormatter<P extends ParsedPoint> implements Formatter<List<P>> {
250+
@Override
251+
public Object formatGeoJson(List<P> points) {
252+
List<Object> result = new ArrayList<>();
253+
for (ParsedPoint point : points) {
254+
try {
255+
Geometry geometry = point.asGeometry();
256+
result.add(GeoJson.toXContentMap(geometry));
257+
} catch (IOException e) {
258+
throw new UncheckedIOException(e);
259+
}
260+
}
261+
return result;
262+
}
263+
264+
@Override
265+
public Object formatWKT(List<P> points) {
266+
List<String> result = new ArrayList<>();
267+
for (ParsedPoint point : points) {
268+
Geometry geometry = point.asGeometry();
269+
result.add(WellKnownText.INSTANCE.toWKT(geometry));
270+
}
271+
return result;
272+
}
273+
}
242274
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@
2929
import org.elasticsearch.common.geo.GeoPoint;
3030
import org.elasticsearch.common.geo.GeoUtils;
3131
import org.elasticsearch.common.xcontent.XContentParser;
32+
import org.elasticsearch.geometry.Point;
3233
import org.elasticsearch.index.fielddata.IndexFieldData;
3334
import org.elasticsearch.index.fielddata.plain.AbstractLatLonPointIndexFieldData;
34-
import org.elasticsearch.index.query.VectorGeoPointShapeQueryProcessor;
3535
import org.elasticsearch.index.mapper.GeoPointFieldMapper.ParsedGeoPoint;
36+
import org.elasticsearch.index.query.VectorGeoPointShapeQueryProcessor;
3637
import org.elasticsearch.search.aggregations.support.CoreValuesSourceType;
3738

3839
import java.io.IOException;
@@ -49,6 +50,7 @@
4950
public class GeoPointFieldMapper extends AbstractPointGeometryFieldMapper<List<ParsedGeoPoint>, List<? extends GeoPoint>> {
5051
public static final String CONTENT_TYPE = "geo_point";
5152
public static final FieldType FIELD_TYPE = new FieldType();
53+
5254
static {
5355
FIELD_TYPE.setStored(false);
5456
FIELD_TYPE.setIndexOptions(IndexOptions.DOCS);
@@ -70,6 +72,7 @@ public GeoPointFieldMapper build(BuilderContext context, String simpleName, Fiel
7072
GeoPointFieldType ft = new GeoPointFieldType(buildFullName(context), indexed, hasDocValues, meta);
7173
ft.setGeometryParser(new PointParser<>());
7274
ft.setGeometryIndexer(new GeoPointIndexer(ft));
75+
ft.setGeometryFormatter(new PointFormatter<>());
7376
ft.setGeometryQueryBuilder(new VectorGeoPointShapeQueryProcessor());
7477
return new GeoPointFieldMapper(name, fieldType, ft, multiFields, ignoreMalformed, ignoreZValue, nullValue, copyTo);
7578
}
@@ -218,6 +221,10 @@ public void resetCoords(double x, double y) {
218221
this.reset(y, x);
219222
}
220223

224+
public Point asGeometry() {
225+
return new Point(lon(), lat());
226+
}
227+
221228
@Override
222229
public boolean equals(Object other) {
223230
double oLat;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ private GeoShapeFieldType buildFieldType(BuilderContext context) {
7373
ignoreZValue().value());
7474
ft.setGeometryParser((parser, mapper) -> geometryParser.parse(parser));
7575
ft.setGeometryIndexer(new GeoShapeIndexer(orientation().value().getAsBoolean(), buildFullName(context)));
76+
ft.setGeometryFormatter(new GeoShapeFormatter());
7677
ft.setGeometryQueryBuilder(new VectorGeoShapeQueryProcessor());
7778
ft.setOrientation(orientation == null ? Defaults.ORIENTATION.value() : orientation);
7879
return ft;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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.index.mapper;
21+
22+
import org.elasticsearch.common.geo.GeoJson;
23+
import org.elasticsearch.geometry.Geometry;
24+
import org.elasticsearch.geometry.utils.WellKnownText;
25+
26+
import java.io.IOException;
27+
import java.io.UncheckedIOException;
28+
29+
public class GeoShapeFormatter implements AbstractGeometryFieldMapper.Formatter<Geometry> {
30+
@Override
31+
public Object formatGeoJson(Geometry value) {
32+
try {
33+
return GeoJson.toXContentMap(value);
34+
} catch (IOException e) {
35+
throw new UncheckedIOException(e);
36+
}
37+
}
38+
39+
@Override
40+
public Object formatWKT(Geometry value) {
41+
return WellKnownText.INSTANCE.toWKT(value);
42+
}
43+
}

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.elasticsearch.Version;
3232
import org.elasticsearch.common.Explicit;
3333
import org.elasticsearch.common.ParseField;
34+
import org.elasticsearch.common.geo.GeoJson;
3435
import org.elasticsearch.common.geo.GeoUtils;
3536
import org.elasticsearch.common.geo.ShapesAvailability;
3637
import org.elasticsearch.common.geo.SpatialStrategy;
@@ -43,10 +44,13 @@
4344
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
4445
import org.elasticsearch.common.xcontent.XContentBuilder;
4546
import org.elasticsearch.common.xcontent.support.XContentMapValues;
47+
import org.elasticsearch.geometry.Geometry;
48+
import org.elasticsearch.geometry.utils.WellKnownText;
4649
import org.elasticsearch.index.query.LegacyGeoShapeQueryProcessor;
4750
import org.locationtech.spatial4j.shape.Shape;
4851

4952
import java.io.IOException;
53+
import java.io.UncheckedIOException;
5054
import java.util.Collections;
5155
import java.util.List;
5256
import java.util.Map;
@@ -256,6 +260,7 @@ private GeoShapeFieldType buildFieldType(BuilderContext context) {
256260
setupPrefixTrees(ft);
257261
ft.setGeometryIndexer(new LegacyGeoShapeIndexer(ft));
258262
ft.setGeometryParser(ShapeParser::parse);
263+
ft.setGeometryFormatter(new LegacyGeoShapeFormatter());
259264
ft.setGeometryQueryBuilder(new LegacyGeoShapeQueryProcessor(ft));
260265
ft.setOrientation(orientation == null ? Defaults.ORIENTATION.value() : orientation);
261266
return ft;
@@ -277,6 +282,23 @@ public LegacyGeoShapeFieldMapper build(BuilderContext context) {
277282
}
278283
}
279284

285+
private static class LegacyGeoShapeFormatter implements Formatter<ShapeBuilder<?, ?, ?>> {
286+
@Override
287+
public Object formatGeoJson(ShapeBuilder<?, ?, ?> value) {
288+
try {
289+
Geometry geometry = value.buildGeometry();
290+
return GeoJson.toXContentMap(geometry);
291+
} catch (IOException e) {
292+
throw new UncheckedIOException(e);
293+
}
294+
}
295+
296+
@Override
297+
public String formatWKT(ShapeBuilder<?, ?, ?> value) {
298+
return WellKnownText.INSTANCE.toWKT(value.buildGeometry());
299+
}
300+
}
301+
280302
public static final class GeoShapeFieldType extends AbstractShapeGeometryFieldType<ShapeBuilder<?, ?, ?>, Shape> {
281303

282304
private String tree = DeprecatedParameters.Defaults.TREE;

0 commit comments

Comments
 (0)