Add null_value support to geo_point type (#29451)

Adds support for null_value attribute to the geo_point types.

Closes #12998
This commit is contained in:
Igor Motov 2018-04-17 10:19:54 -04:00 committed by GitHub
parent 3367948be6
commit 983d6c15a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 180 additions and 6 deletions

View File

@ -122,6 +122,11 @@ The following parameters are accepted by `geo_point` fields:
ignored. If `false`, geo-points containing any more than latitude and longitude ignored. If `false`, geo-points containing any more than latitude and longitude
(two dimensions) values throw an exception and reject the whole document. (two dimensions) values throw an exception and reject the whole document.
<<null-value,`null_value`>>::
Accepts an geopoint value which is substituted for any explicit `null` values.
Defaults to `null`, which means the field is treated as missing.
==== Using geo-points in scripts ==== Using geo-points in scripts
When accessing the value of a geo-point in a script, the value is returned as When accessing the value of a geo-point in a script, the value is returned as

View File

@ -24,9 +24,14 @@ import org.apache.lucene.spatial.prefix.tree.GeohashPrefixTree;
import org.apache.lucene.spatial.prefix.tree.QuadPrefixTree; import org.apache.lucene.spatial.prefix.tree.QuadPrefixTree;
import org.apache.lucene.util.SloppyMath; import org.apache.lucene.util.SloppyMath;
import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.unit.DistanceUnit; import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentParser.Token; import org.elasticsearch.common.xcontent.XContentParser.Token;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.common.xcontent.support.XContentMapValues; import org.elasticsearch.common.xcontent.support.XContentMapValues;
import org.elasticsearch.index.fielddata.FieldData; import org.elasticsearch.index.fielddata.FieldData;
import org.elasticsearch.index.fielddata.GeoPointValues; import org.elasticsearch.index.fielddata.GeoPointValues;
@ -36,6 +41,7 @@ import org.elasticsearch.index.fielddata.SortedNumericDoubleValues;
import org.elasticsearch.index.fielddata.SortingNumericDoubleValues; import org.elasticsearch.index.fielddata.SortingNumericDoubleValues;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
public class GeoUtils { public class GeoUtils {
@ -351,6 +357,36 @@ public class GeoUtils {
return parseGeoPoint(parser, point, false); return parseGeoPoint(parser, point, false);
} }
/**
* Parses the value as a geopoint. The following types of values are supported:
* <p>
* Object: has to contain either lat and lon or geohash fields
* <p>
* String: expected to be in "latitude, longitude" format or a geohash
* <p>
* Array: two or more elements, the first element is longitude, the second is latitude, the rest is ignored if ignoreZValue is true
*/
public static GeoPoint parseGeoPoint(Object value, final boolean ignoreZValue) throws ElasticsearchParseException {
try {
XContentBuilder content = JsonXContent.contentBuilder();
content.startObject();
content.field("null_value", value);
content.endObject();
try (InputStream stream = BytesReference.bytes(content).streamInput();
XContentParser parser = JsonXContent.jsonXContent.createParser(
NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) {
parser.nextToken(); // start object
parser.nextToken(); // field name
parser.nextToken(); // field value
return parseGeoPoint(parser, new GeoPoint(), ignoreZValue);
}
} catch (IOException ex) {
throw new ElasticsearchParseException("error parsing geopoint", ex);
}
}
/** /**
* Parse a {@link GeoPoint} with a {@link XContentParser}. A geopoint has one of the following forms: * Parse a {@link GeoPoint} with a {@link XContentParser}. A geopoint has one of the following forms:
* *

View File

@ -60,6 +60,7 @@ public class GeoPointFieldMapper extends FieldMapper implements ArrayValueMapper
public static class Names { public static class Names {
public static final String IGNORE_MALFORMED = "ignore_malformed"; public static final String IGNORE_MALFORMED = "ignore_malformed";
public static final ParseField IGNORE_Z_VALUE = new ParseField("ignore_z_value"); public static final ParseField IGNORE_Z_VALUE = new ParseField("ignore_z_value");
public static final String NULL_VALUE = "null_value";
} }
public static class Defaults { public static class Defaults {
@ -134,7 +135,7 @@ public class GeoPointFieldMapper extends FieldMapper implements ArrayValueMapper
throws MapperParsingException { throws MapperParsingException {
Builder builder = new GeoPointFieldMapper.Builder(name); Builder builder = new GeoPointFieldMapper.Builder(name);
parseField(builder, name, node, parserContext); parseField(builder, name, node, parserContext);
Object nullValue = null;
for (Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); iterator.hasNext();) { for (Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); iterator.hasNext();) {
Map.Entry<String, Object> entry = iterator.next(); Map.Entry<String, Object> entry = iterator.next();
String propName = entry.getKey(); String propName = entry.getKey();
@ -147,9 +148,31 @@ public class GeoPointFieldMapper extends FieldMapper implements ArrayValueMapper
builder.ignoreZValue(XContentMapValues.nodeBooleanValue(propNode, builder.ignoreZValue(XContentMapValues.nodeBooleanValue(propNode,
name + "." + Names.IGNORE_Z_VALUE.getPreferredName())); name + "." + Names.IGNORE_Z_VALUE.getPreferredName()));
iterator.remove(); iterator.remove();
} else if (propName.equals(Names.NULL_VALUE)) {
if (propNode == null) {
throw new MapperParsingException("Property [null_value] cannot be null.");
}
nullValue = propNode;
iterator.remove();
} }
} }
if (nullValue != null) {
boolean ignoreZValue = builder.ignoreZValue == null ? Defaults.IGNORE_Z_VALUE.value() : builder.ignoreZValue;
boolean ignoreMalformed = builder.ignoreMalformed == null ? Defaults.IGNORE_MALFORMED.value() : builder.ignoreZValue;
GeoPoint point = GeoUtils.parseGeoPoint(nullValue, ignoreZValue);
if (ignoreMalformed == false) {
if (point.lat() > 90.0 || point.lat() < -90.0) {
throw new IllegalArgumentException("illegal latitude value [" + point.lat() + "]");
}
if (point.lon() > 180.0 || point.lon() < -180) {
throw new IllegalArgumentException("illegal longitude value [" + point.lon() + "]");
}
} else {
GeoUtils.normalizePoint(point);
}
builder.nullValue(point);
}
return builder; return builder;
} }
} }
@ -318,7 +341,11 @@ public class GeoPointFieldMapper extends FieldMapper implements ArrayValueMapper
} }
} else if (token == XContentParser.Token.VALUE_STRING) { } else if (token == XContentParser.Token.VALUE_STRING) {
parse(context, sparse.resetFromString(context.parser().text(), ignoreZValue.value())); parse(context, sparse.resetFromString(context.parser().text(), ignoreZValue.value()));
} else if (token != XContentParser.Token.VALUE_NULL) { } else if (token == XContentParser.Token.VALUE_NULL) {
if (fieldType.nullValue() != null) {
parse(context, (GeoPoint) fieldType.nullValue());
}
} else {
try { try {
parse(context, GeoUtils.parseGeoPoint(context.parser(), sparse)); parse(context, GeoUtils.parseGeoPoint(context.parser(), sparse));
} catch (ElasticsearchParseException e) { } catch (ElasticsearchParseException e) {
@ -337,11 +364,15 @@ public class GeoPointFieldMapper extends FieldMapper implements ArrayValueMapper
protected void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException { protected void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException {
super.doXContentBody(builder, includeDefaults, params); super.doXContentBody(builder, includeDefaults, params);
if (includeDefaults || ignoreMalformed.explicit()) { if (includeDefaults || ignoreMalformed.explicit()) {
builder.field(GeoPointFieldMapper.Names.IGNORE_MALFORMED, ignoreMalformed.value()); builder.field(Names.IGNORE_MALFORMED, ignoreMalformed.value());
} }
if (includeDefaults || ignoreZValue.explicit()) { if (includeDefaults || ignoreZValue.explicit()) {
builder.field(Names.IGNORE_Z_VALUE.getPreferredName(), ignoreZValue.value()); builder.field(Names.IGNORE_Z_VALUE.getPreferredName(), ignoreZValue.value());
} }
if (includeDefaults || fieldType().nullValue() != null) {
builder.field(Names.NULL_VALUE, fieldType().nullValue());
}
} }
public Explicit<Boolean> ignoreZValue() { public Explicit<Boolean> ignoreZValue() {

View File

@ -18,6 +18,7 @@
*/ */
package org.elasticsearch.index.mapper; package org.elasticsearch.index.mapper;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder; import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.Priority; import org.elasticsearch.common.Priority;
@ -41,10 +42,12 @@ import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDI
import static org.elasticsearch.common.geo.GeoHashUtils.stringEncode; import static org.elasticsearch.common.geo.GeoHashUtils.stringEncode;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.elasticsearch.index.mapper.GeoPointFieldMapper.Names.IGNORE_Z_VALUE; import static org.elasticsearch.index.mapper.GeoPointFieldMapper.Names.IGNORE_Z_VALUE;
import static org.elasticsearch.index.mapper.GeoPointFieldMapper.Names.NULL_VALUE;
import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
public class GeoPointFieldMapperTests extends ESSingleNodeTestCase { public class GeoPointFieldMapperTests extends ESSingleNodeTestCase {
@ -349,4 +352,50 @@ public class GeoPointFieldMapperTests extends ESSingleNodeTestCase {
); );
assertThat(e.getMessage(), containsString("name cannot be empty string")); assertThat(e.getMessage(), containsString("name cannot be empty string"));
} }
public void testNullValue() throws Exception {
String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type")
.startObject("properties").startObject("location")
.field("type", "geo_point")
.field(NULL_VALUE, "1,2")
.endObject().endObject()
.endObject().endObject());
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
.parse("type", new CompressedXContent(mapping));
FieldMapper fieldMapper = defaultMapper.mappers().getMapper("location");
assertThat(fieldMapper, instanceOf(GeoPointFieldMapper.class));
Object nullValue = fieldMapper.fieldType().nullValue();
assertThat(nullValue, equalTo(new GeoPoint(1, 2)));
ParsedDocument doc = defaultMapper.parse(SourceToParse.source("test", "type", "1", BytesReference
.bytes(XContentFactory.jsonBuilder()
.startObject()
.nullField("location")
.endObject()),
XContentType.JSON));
assertThat(doc.rootDoc().getField("location"), notNullValue());
BytesRef defaultValue = doc.rootDoc().getField("location").binaryValue();
doc = defaultMapper.parse(SourceToParse.source("test", "type", "1", BytesReference
.bytes(XContentFactory.jsonBuilder()
.startObject()
.field("location", "1, 2")
.endObject()),
XContentType.JSON));
// Shouldn't matter if we specify the value explicitly or use null value
assertThat(defaultValue, equalTo(doc.rootDoc().getField("location").binaryValue()));
doc = defaultMapper.parse(SourceToParse.source("test", "type", "1", BytesReference
.bytes(XContentFactory.jsonBuilder()
.startObject()
.field("location", "3, 4")
.endObject()),
XContentType.JSON));
// Shouldn't matter if we specify the value explicitly or use null value
assertThat(defaultValue, not(equalTo(doc.rootDoc().getField("location").binaryValue())));
}
} }

View File

@ -33,7 +33,7 @@ import static org.hamcrest.Matchers.equalTo;
public class NullValueTests extends ESSingleNodeTestCase { public class NullValueTests extends ESSingleNodeTestCase {
public void testNullNullValue() throws Exception { public void testNullNullValue() throws Exception {
IndexService indexService = createIndex("test", Settings.builder().build()); IndexService indexService = createIndex("test", Settings.builder().build());
String[] typesToTest = {"integer", "long", "double", "float", "short", "date", "ip", "keyword", "boolean", "byte"}; String[] typesToTest = {"integer", "long", "double", "float", "short", "date", "ip", "keyword", "boolean", "byte", "geo_point"};
for (String type : typesToTest) { for (String type : typesToTest) {
String mapping = Strings.toString(XContentFactory.jsonBuilder() String mapping = Strings.toString(XContentFactory.jsonBuilder()

View File

@ -76,14 +76,26 @@ public class GeoPointParsingTests extends ESTestCase {
GeoPoint point = GeoUtils.parseGeoPoint(objectLatLon(randomPt.lat(), randomPt.lon())); GeoPoint point = GeoUtils.parseGeoPoint(objectLatLon(randomPt.lat(), randomPt.lon()));
assertPointsEqual(point, randomPt); assertPointsEqual(point, randomPt);
GeoUtils.parseGeoPoint(toObject(objectLatLon(randomPt.lat(), randomPt.lon())), randomBoolean());
assertPointsEqual(point, randomPt);
GeoUtils.parseGeoPoint(arrayLatLon(randomPt.lat(), randomPt.lon()), point); GeoUtils.parseGeoPoint(arrayLatLon(randomPt.lat(), randomPt.lon()), point);
assertPointsEqual(point, randomPt); assertPointsEqual(point, randomPt);
GeoUtils.parseGeoPoint(toObject(arrayLatLon(randomPt.lat(), randomPt.lon())), randomBoolean());
assertPointsEqual(point, randomPt);
GeoUtils.parseGeoPoint(geohash(randomPt.lat(), randomPt.lon()), point); GeoUtils.parseGeoPoint(geohash(randomPt.lat(), randomPt.lon()), point);
assertCloseTo(point, randomPt.lat(), randomPt.lon()); assertCloseTo(point, randomPt.lat(), randomPt.lon());
GeoUtils.parseGeoPoint(toObject(geohash(randomPt.lat(), randomPt.lon())), randomBoolean());
assertCloseTo(point, randomPt.lat(), randomPt.lon());
GeoUtils.parseGeoPoint(stringLatLon(randomPt.lat(), randomPt.lon()), point); GeoUtils.parseGeoPoint(stringLatLon(randomPt.lat(), randomPt.lon()), point);
assertCloseTo(point, randomPt.lat(), randomPt.lon()); assertCloseTo(point, randomPt.lat(), randomPt.lon());
GeoUtils.parseGeoPoint(toObject(stringLatLon(randomPt.lat(), randomPt.lon())), randomBoolean());
assertCloseTo(point, randomPt.lat(), randomPt.lon());
} }
// Based on #5390 // Based on #5390
@ -99,6 +111,12 @@ public class GeoPointParsingTests extends ESTestCase {
parser.nextToken(); parser.nextToken();
Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); 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] or [geohash]"));
XContentParser parser2 = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content));
parser2.nextToken();
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 { public void testInvalidPointLatHashMix() throws IOException {
@ -109,9 +127,14 @@ public class GeoPointParsingTests extends ESTestCase {
XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content)); XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content));
parser.nextToken(); parser.nextToken();
Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); 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 or geohash"));
XContentParser parser2 = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content));
parser2.nextToken();
e = expectThrows(ElasticsearchParseException.class, () ->
GeoUtils.parseGeoPoint(toObject(parser2), randomBoolean()));
assertThat(e.getMessage(), is("field must be either lat/lon or geohash"));
} }
public void testInvalidPointLonHashMix() throws IOException { public void testInvalidPointLonHashMix() throws IOException {
@ -125,6 +148,12 @@ public class GeoPointParsingTests extends ESTestCase {
Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); 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 or geohash"));
XContentParser parser2 = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content));
parser2.nextToken();
e = expectThrows(ElasticsearchParseException.class, () ->
GeoUtils.parseGeoPoint(toObject(parser2), randomBoolean()));
assertThat(e.getMessage(), is("field must be either lat/lon or geohash"));
} }
public void testInvalidField() throws IOException { public void testInvalidField() throws IOException {
@ -135,9 +164,15 @@ public class GeoPointParsingTests extends ESTestCase {
XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content)); XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content));
parser.nextToken(); parser.nextToken();
Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser)); 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] or [geohash]"));
XContentParser parser2 = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content));
parser2.nextToken();
e = expectThrows(ElasticsearchParseException.class, () ->
GeoUtils.parseGeoPoint(toObject(parser2), randomBoolean()));
assertThat(e.getMessage(), is("field must be either [lat], [lon] or [geohash]"));
} }
private XContentParser objectLatLon(double lat, double lon) throws IOException { private XContentParser objectLatLon(double lat, double lon) throws IOException {
@ -183,4 +218,22 @@ public class GeoPointParsingTests extends ESTestCase {
assertEquals(point.lat(), lat, TOLERANCE); assertEquals(point.lat(), lat, TOLERANCE);
assertEquals(point.lon(), lon, TOLERANCE); assertEquals(point.lon(), lon, TOLERANCE);
} }
public static Object toObject(XContentParser parser) throws IOException {
XContentParser.Token token = parser.currentToken();
if (token == XContentParser.Token.VALUE_NULL) {
return null;
} else if (token == XContentParser.Token.VALUE_STRING) {
return parser.text();
} else if (token == XContentParser.Token.VALUE_NUMBER) {
return parser.numberValue();
} else if (token == XContentParser.Token.START_OBJECT) {
return parser.map();
} else if (token == XContentParser.Token.START_ARRAY) {
return parser.list();
} else {
fail("Unexpected token " + token);
}
return null;
}
} }