Skip to content

Commit

Permalink
Support spatial fields in field retrieval API. (#59821)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jtibshirani committed Jul 22, 2020
1 parent e033880 commit 568d248
Show file tree
Hide file tree
Showing 24 changed files with 661 additions and 127 deletions.
1 change: 1 addition & 0 deletions docs/reference/mapping/types.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ string:: <<text,`text`>>, <<keyword,`keyword`>> and <<wildcard,`wildcard
<<nested>>:: `nested` for arrays of JSON objects

[float]
[[spatial_datatypes]]
=== Spatial data types

<<geo-point>>:: `geo_point` for lat/lon points
Expand Down
10 changes: 7 additions & 3 deletions docs/reference/search/search-fields.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,13 @@ POST twitter/_search

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,26 @@ setup:
field: location

- match: {hits.total: 1}

---
"Test retrieve geo_shape field":
- do:
search:
index: test
body:
fields: [location]
_source: false

- match: { hits.hits.0.fields.location.0.type: "Point" }
- match: { hits.hits.0.fields.location.0.coordinates: [1.0, 1.0] }

- do:
search:
index: test
body:
fields:
- field: location
format: wkt
_source: false

- match: { hits.hits.0.fields.location.0: "POINT (1.0 1.0)" }
Original file line number Diff line number Diff line change
Expand Up @@ -609,5 +609,4 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
return builder;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.common.geo;

import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.geometry.Geometry;

import java.io.IOException;
import java.io.UncheckedIOException;

public class GeoJsonGeometryFormat implements GeometryFormat<Geometry> {
public static final String NAME = "geojson";

private final GeoJson geoJsonParser;

public GeoJsonGeometryFormat(GeoJson geoJsonParser) {
this.geoJsonParser = geoJsonParser;
}

@Override
public String name() {
return NAME;
}

@Override
public Geometry fromXContent(XContentParser parser) throws IOException {
if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
return null;
}
return geoJsonParser.fromXContent(parser);
}

@Override
public XContentBuilder toXContent(Geometry geometry, XContentBuilder builder, ToXContent.Params params) throws IOException {
if (geometry != null) {
return GeoJson.toXContent(geometry, builder, params);
} else {
return builder.nullValue();
}
}

@Override
public Object toXContentAsObject(Geometry geometry) {
try {
XContentBuilder builder = XContentFactory.jsonBuilder();
GeoJson.toXContent(geometry, builder, ToXContent.EMPTY_PARAMS);
StreamInput input = BytesReference.bytes(builder).streamInput();

try (XContentParser parser = XContentType.JSON.xContent()
.createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, input)) {
return parser.map();
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
*/
public interface GeometryFormat<ParsedFormat> {

/**
* The name of the format, for example 'wkt'.
*/
String name();

/**
* Parser JSON representation of a geometry
*/
Expand All @@ -41,4 +46,10 @@ public interface GeometryFormat<ParsedFormat> {
*/
XContentBuilder toXContent(ParsedFormat geometry, XContentBuilder builder, ToXContent.Params params) throws IOException;

/**
* Serializes the geometry into a standard Java object.
*
* For example, the GeoJson format returns the geometry as a map, while WKT returns a string.
*/
Object toXContentAsObject(ParsedFormat geometry);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,13 @@
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.support.MapXContentParser;
import org.elasticsearch.geometry.Geometry;
import org.elasticsearch.geometry.GeometryCollection;
import org.elasticsearch.geometry.Point;
import org.elasticsearch.geometry.utils.StandardValidator;
import org.elasticsearch.geometry.utils.GeometryValidator;
import org.elasticsearch.geometry.utils.StandardValidator;
import org.elasticsearch.geometry.utils.WellKnownText;

import java.io.IOException;
Expand Down Expand Up @@ -66,59 +64,31 @@ public Geometry parse(XContentParser parser) throws IOException, ParseException
/**
* Returns a geometry format object that can parse and then serialize the object back to the same format.
*/
public GeometryFormat<Geometry> geometryFormat(XContentParser parser) {
if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
return new GeometryFormat<Geometry>() {
@Override
public Geometry fromXContent(XContentParser parser) throws IOException {
return null;
}

@Override
public XContentBuilder toXContent(Geometry geometry, XContentBuilder builder, ToXContent.Params params) throws IOException {
if (geometry != null) {
// We don't know the format of the original geometry - so going with default
return GeoJson.toXContent(geometry, builder, params);
} else {
return builder.nullValue();
}
}
};
} else if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
return new GeometryFormat<Geometry>() {
@Override
public Geometry fromXContent(XContentParser parser) throws IOException {
return geoJsonParser.fromXContent(parser);
}
public GeometryFormat<Geometry> geometryFormat(String format) {
if (format.equals(GeoJsonGeometryFormat.NAME)) {
return new GeoJsonGeometryFormat(geoJsonParser);
} else if (format.equals(WKTGeometryFormat.NAME)) {
return new WKTGeometryFormat(wellKnownTextParser);
} else {
throw new IllegalArgumentException("Unrecognized geometry format [" + format + "].");
}
}

@Override
public XContentBuilder toXContent(Geometry geometry, XContentBuilder builder, ToXContent.Params params) throws IOException {
if (geometry != null) {
return GeoJson.toXContent(geometry, builder, params);
} else {
return builder.nullValue();
}
}
};
/**
* Returns a geometry format object that can parse and then serialize the object back to the same format.
* This method automatically recognizes the format by examining the provided {@link XContentParser}.
*/
public GeometryFormat<Geometry> geometryFormat(XContentParser parser) {
if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
return new GeoJsonGeometryFormat(geoJsonParser);
} else if (parser.currentToken() == XContentParser.Token.VALUE_STRING) {
return new GeometryFormat<Geometry>() {
@Override
public Geometry fromXContent(XContentParser parser) throws IOException, ParseException {
return wellKnownTextParser.fromWKT(parser.text());
}

@Override
public XContentBuilder toXContent(Geometry geometry, XContentBuilder builder, ToXContent.Params params) throws IOException {
if (geometry != null) {
return builder.value(wellKnownTextParser.toWKT(geometry));
} else {
return builder.nullValue();
}
}
};

return new WKTGeometryFormat(wellKnownTextParser);
} else if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
// We don't know the format of the original geometry - so going with default
return new GeoJsonGeometryFormat(geoJsonParser);
} else {
throw new ElasticsearchParseException("shape must be an object consisting of type and coordinates");
}
throw new ElasticsearchParseException("shape must be an object consisting of type and coordinates");
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.common.geo;

import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.geometry.Geometry;
import org.elasticsearch.geometry.utils.WellKnownText;

import java.io.IOException;
import java.text.ParseException;

public class WKTGeometryFormat implements GeometryFormat<Geometry> {
public static final String NAME = "wkt";

private final WellKnownText wellKnownTextParser;

public WKTGeometryFormat(WellKnownText wellKnownTextParser) {
this.wellKnownTextParser = wellKnownTextParser;
}

@Override
public String name() {
return NAME;
}

@Override
public Geometry fromXContent(XContentParser parser) throws IOException, ParseException {
if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
return null;
}
return wellKnownTextParser.fromWKT(parser.text());
}

@Override
public XContentBuilder toXContent(Geometry geometry, XContentBuilder builder, ToXContent.Params params) throws IOException {
if (geometry != null) {
return builder.value(wellKnownTextParser.toWKT(geometry));
} else {
return builder.nullValue();
}
}

@Override
public String toXContentAsObject(Geometry geometry) {
return wellKnownTextParser.toWKT(geometry);
}
}
Loading

0 comments on commit 568d248

Please sign in to comment.