Skip to content

Commit 80e6b73

Browse files
committed
Add null_value support to geo_point type (#29451)
Adds support for null_value attribute to the geo_point types. Closes #12998
1 parent 451454d commit 80e6b73

File tree

6 files changed

+180
-6
lines changed

6 files changed

+180
-6
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ The following parameters are accepted by `geo_point` fields:
122122
ignored. If `false`, geo-points containing any more than latitude and longitude
123123
(two dimensions) values throw an exception and reject the whole document.
124124

125+
<<null-value,`null_value`>>::
126+
127+
Accepts an geopoint value which is substituted for any explicit `null` values.
128+
Defaults to `null`, which means the field is treated as missing.
129+
125130
==== Using geo-points in scripts
126131

127132
When accessing the value of a geo-point in a script, the value is returned as

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,14 @@
2424
import org.apache.lucene.spatial.prefix.tree.QuadPrefixTree;
2525
import org.apache.lucene.util.SloppyMath;
2626
import org.elasticsearch.ElasticsearchParseException;
27+
import org.elasticsearch.common.bytes.BytesReference;
2728
import org.elasticsearch.common.unit.DistanceUnit;
29+
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
30+
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
31+
import org.elasticsearch.common.xcontent.XContentBuilder;
2832
import org.elasticsearch.common.xcontent.XContentParser;
2933
import org.elasticsearch.common.xcontent.XContentParser.Token;
34+
import org.elasticsearch.common.xcontent.json.JsonXContent;
3035
import org.elasticsearch.common.xcontent.support.XContentMapValues;
3136
import org.elasticsearch.index.fielddata.FieldData;
3237
import org.elasticsearch.index.fielddata.GeoPointValues;
@@ -36,6 +41,7 @@
3641
import org.elasticsearch.index.fielddata.SortingNumericDoubleValues;
3742

3843
import java.io.IOException;
44+
import java.io.InputStream;
3945

4046
public class GeoUtils {
4147

@@ -351,6 +357,36 @@ public static GeoPoint parseGeoPoint(XContentParser parser, GeoPoint point) thro
351357
return parseGeoPoint(parser, point, false);
352358
}
353359

360+
/**
361+
* Parses the value as a geopoint. The following types of values are supported:
362+
* <p>
363+
* Object: has to contain either lat and lon or geohash fields
364+
* <p>
365+
* String: expected to be in "latitude, longitude" format or a geohash
366+
* <p>
367+
* Array: two or more elements, the first element is longitude, the second is latitude, the rest is ignored if ignoreZValue is true
368+
*/
369+
public static GeoPoint parseGeoPoint(Object value, final boolean ignoreZValue) throws ElasticsearchParseException {
370+
try {
371+
XContentBuilder content = JsonXContent.contentBuilder();
372+
content.startObject();
373+
content.field("null_value", value);
374+
content.endObject();
375+
376+
try (InputStream stream = BytesReference.bytes(content).streamInput();
377+
XContentParser parser = JsonXContent.jsonXContent.createParser(
378+
NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) {
379+
parser.nextToken(); // start object
380+
parser.nextToken(); // field name
381+
parser.nextToken(); // field value
382+
return parseGeoPoint(parser, new GeoPoint(), ignoreZValue);
383+
}
384+
385+
} catch (IOException ex) {
386+
throw new ElasticsearchParseException("error parsing geopoint", ex);
387+
}
388+
}
389+
354390
/**
355391
* Parse a {@link GeoPoint} with a {@link XContentParser}. A geopoint has one of the following forms:
356392
*

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

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public class GeoPointFieldMapper extends FieldMapper implements ArrayValueMapper
5959
public static class Names {
6060
public static final String IGNORE_MALFORMED = "ignore_malformed";
6161
public static final ParseField IGNORE_Z_VALUE = new ParseField("ignore_z_value");
62+
public static final String NULL_VALUE = "null_value";
6263
}
6364

6465
public static class Defaults {
@@ -133,7 +134,7 @@ public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext
133134
throws MapperParsingException {
134135
Builder builder = new GeoPointFieldMapper.Builder(name);
135136
parseField(builder, name, node, parserContext);
136-
137+
Object nullValue = null;
137138
for (Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); iterator.hasNext();) {
138139
Map.Entry<String, Object> entry = iterator.next();
139140
String propName = entry.getKey();
@@ -146,9 +147,31 @@ public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext
146147
builder.ignoreZValue(TypeParsers.nodeBooleanValue(propName, Names.IGNORE_Z_VALUE.getPreferredName(),
147148
propNode, parserContext));
148149
iterator.remove();
150+
} else if (propName.equals(Names.NULL_VALUE)) {
151+
if (propNode == null) {
152+
throw new MapperParsingException("Property [null_value] cannot be null.");
153+
}
154+
nullValue = propNode;
155+
iterator.remove();
149156
}
150157
}
151158

159+
if (nullValue != null) {
160+
boolean ignoreZValue = builder.ignoreZValue == null ? Defaults.IGNORE_Z_VALUE.value() : builder.ignoreZValue;
161+
boolean ignoreMalformed = builder.ignoreMalformed == null ? Defaults.IGNORE_MALFORMED.value() : builder.ignoreZValue;
162+
GeoPoint point = GeoUtils.parseGeoPoint(nullValue, ignoreZValue);
163+
if (ignoreMalformed == false) {
164+
if (point.lat() > 90.0 || point.lat() < -90.0) {
165+
throw new IllegalArgumentException("illegal latitude value [" + point.lat() + "]");
166+
}
167+
if (point.lon() > 180.0 || point.lon() < -180) {
168+
throw new IllegalArgumentException("illegal longitude value [" + point.lon() + "]");
169+
}
170+
} else {
171+
GeoUtils.normalizePoint(point);
172+
}
173+
builder.nullValue(point);
174+
}
152175
return builder;
153176
}
154177
}
@@ -319,7 +342,11 @@ public Mapper parse(ParseContext context) throws IOException {
319342
}
320343
} else if (token == XContentParser.Token.VALUE_STRING) {
321344
parse(context, sparse.resetFromString(context.parser().text(), ignoreZValue.value()));
322-
} else if (token != XContentParser.Token.VALUE_NULL) {
345+
} else if (token == XContentParser.Token.VALUE_NULL) {
346+
if (fieldType.nullValue() != null) {
347+
parse(context, (GeoPoint) fieldType.nullValue());
348+
}
349+
} else {
323350
try {
324351
parse(context, GeoUtils.parseGeoPoint(context.parser(), sparse));
325352
} catch (ElasticsearchParseException e) {
@@ -338,11 +365,15 @@ public Mapper parse(ParseContext context) throws IOException {
338365
protected void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException {
339366
super.doXContentBody(builder, includeDefaults, params);
340367
if (includeDefaults || ignoreMalformed.explicit()) {
341-
builder.field(GeoPointFieldMapper.Names.IGNORE_MALFORMED, ignoreMalformed.value());
368+
builder.field(Names.IGNORE_MALFORMED, ignoreMalformed.value());
342369
}
343370
if (includeDefaults || ignoreZValue.explicit()) {
344371
builder.field(Names.IGNORE_Z_VALUE.getPreferredName(), ignoreZValue.value());
345372
}
373+
374+
if (includeDefaults || fieldType().nullValue() != null) {
375+
builder.field(Names.NULL_VALUE, fieldType().nullValue());
376+
}
346377
}
347378

348379
public Explicit<Boolean> ignoreZValue() {

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
package org.elasticsearch.index.mapper;
2020

21+
import org.apache.lucene.util.BytesRef;
2122
import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
2223
import org.elasticsearch.action.search.SearchResponse;
2324
import org.elasticsearch.common.Priority;
@@ -41,10 +42,12 @@
4142
import static org.elasticsearch.common.geo.GeoHashUtils.stringEncode;
4243
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
4344
import static org.elasticsearch.index.mapper.GeoPointFieldMapper.Names.IGNORE_Z_VALUE;
45+
import static org.elasticsearch.index.mapper.GeoPointFieldMapper.Names.NULL_VALUE;
4446
import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
4547
import static org.hamcrest.Matchers.containsString;
4648
import static org.hamcrest.Matchers.equalTo;
4749
import static org.hamcrest.Matchers.instanceOf;
50+
import static org.hamcrest.Matchers.not;
4851
import static org.hamcrest.Matchers.notNullValue;
4952

5053
public class GeoPointFieldMapperTests extends ESSingleNodeTestCase {
@@ -349,4 +352,50 @@ public void testEmptyName() throws Exception {
349352
);
350353
assertThat(e.getMessage(), containsString("name cannot be empty string"));
351354
}
355+
356+
public void testNullValue() throws Exception {
357+
String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type")
358+
.startObject("properties").startObject("location")
359+
.field("type", "geo_point")
360+
.field(NULL_VALUE, "1,2")
361+
.endObject().endObject()
362+
.endObject().endObject());
363+
364+
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
365+
.parse("type", new CompressedXContent(mapping));
366+
FieldMapper fieldMapper = defaultMapper.mappers().getMapper("location");
367+
assertThat(fieldMapper, instanceOf(GeoPointFieldMapper.class));
368+
369+
Object nullValue = fieldMapper.fieldType().nullValue();
370+
assertThat(nullValue, equalTo(new GeoPoint(1, 2)));
371+
372+
ParsedDocument doc = defaultMapper.parse(SourceToParse.source("test", "type", "1", BytesReference
373+
.bytes(XContentFactory.jsonBuilder()
374+
.startObject()
375+
.nullField("location")
376+
.endObject()),
377+
XContentType.JSON));
378+
379+
assertThat(doc.rootDoc().getField("location"), notNullValue());
380+
BytesRef defaultValue = doc.rootDoc().getField("location").binaryValue();
381+
382+
doc = defaultMapper.parse(SourceToParse.source("test", "type", "1", BytesReference
383+
.bytes(XContentFactory.jsonBuilder()
384+
.startObject()
385+
.field("location", "1, 2")
386+
.endObject()),
387+
XContentType.JSON));
388+
// Shouldn't matter if we specify the value explicitly or use null value
389+
assertThat(defaultValue, equalTo(doc.rootDoc().getField("location").binaryValue()));
390+
391+
doc = defaultMapper.parse(SourceToParse.source("test", "type", "1", BytesReference
392+
.bytes(XContentFactory.jsonBuilder()
393+
.startObject()
394+
.field("location", "3, 4")
395+
.endObject()),
396+
XContentType.JSON));
397+
// Shouldn't matter if we specify the value explicitly or use null value
398+
assertThat(defaultValue, not(equalTo(doc.rootDoc().getField("location").binaryValue())));
399+
}
400+
352401
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
public class NullValueTests extends ESSingleNodeTestCase {
3434
public void testNullNullValue() throws Exception {
3535
IndexService indexService = createIndex("test", Settings.builder().build());
36-
String[] typesToTest = {"integer", "long", "double", "float", "short", "date", "ip", "keyword", "boolean", "byte"};
36+
String[] typesToTest = {"integer", "long", "double", "float", "short", "date", "ip", "keyword", "boolean", "byte", "geo_point"};
3737

3838
for (String type : typesToTest) {
3939
String mapping = Strings.toString(XContentFactory.jsonBuilder()

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

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,26 @@ public void testGeoPointParsing() throws IOException {
7676
GeoPoint point = GeoUtils.parseGeoPoint(objectLatLon(randomPt.lat(), randomPt.lon()));
7777
assertPointsEqual(point, randomPt);
7878

79+
GeoUtils.parseGeoPoint(toObject(objectLatLon(randomPt.lat(), randomPt.lon())), randomBoolean());
80+
assertPointsEqual(point, randomPt);
81+
7982
GeoUtils.parseGeoPoint(arrayLatLon(randomPt.lat(), randomPt.lon()), point);
8083
assertPointsEqual(point, randomPt);
8184

85+
GeoUtils.parseGeoPoint(toObject(arrayLatLon(randomPt.lat(), randomPt.lon())), randomBoolean());
86+
assertPointsEqual(point, randomPt);
87+
8288
GeoUtils.parseGeoPoint(geohash(randomPt.lat(), randomPt.lon()), point);
8389
assertCloseTo(point, randomPt.lat(), randomPt.lon());
8490

91+
GeoUtils.parseGeoPoint(toObject(geohash(randomPt.lat(), randomPt.lon())), randomBoolean());
92+
assertCloseTo(point, randomPt.lat(), randomPt.lon());
93+
8594
GeoUtils.parseGeoPoint(stringLatLon(randomPt.lat(), randomPt.lon()), point);
8695
assertCloseTo(point, randomPt.lat(), randomPt.lon());
96+
97+
GeoUtils.parseGeoPoint(toObject(stringLatLon(randomPt.lat(), randomPt.lon())), randomBoolean());
98+
assertCloseTo(point, randomPt.lat(), randomPt.lon());
8799
}
88100

89101
// Based on #5390
@@ -99,6 +111,12 @@ public void testInvalidPointEmbeddedObject() throws IOException {
99111
parser.nextToken();
100112
Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser));
101113
assertThat(e.getMessage(), is("field must be either [lat], [lon] or [geohash]"));
114+
115+
XContentParser parser2 = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content));
116+
parser2.nextToken();
117+
e = expectThrows(ElasticsearchParseException.class, () ->
118+
GeoUtils.parseGeoPoint(toObject(parser2), randomBoolean()));
119+
assertThat(e.getMessage(), is("field must be either [lat], [lon] or [geohash]"));
102120
}
103121

104122
public void testInvalidPointLatHashMix() throws IOException {
@@ -109,9 +127,14 @@ public void testInvalidPointLatHashMix() throws IOException {
109127

110128
XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content));
111129
parser.nextToken();
112-
113130
Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser));
114131
assertThat(e.getMessage(), is("field must be either lat/lon or geohash"));
132+
133+
XContentParser parser2 = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content));
134+
parser2.nextToken();
135+
e = expectThrows(ElasticsearchParseException.class, () ->
136+
GeoUtils.parseGeoPoint(toObject(parser2), randomBoolean()));
137+
assertThat(e.getMessage(), is("field must be either lat/lon or geohash"));
115138
}
116139

117140
public void testInvalidPointLonHashMix() throws IOException {
@@ -125,6 +148,12 @@ public void testInvalidPointLonHashMix() throws IOException {
125148

126149
Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser));
127150
assertThat(e.getMessage(), is("field must be either lat/lon or geohash"));
151+
152+
XContentParser parser2 = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content));
153+
parser2.nextToken();
154+
e = expectThrows(ElasticsearchParseException.class, () ->
155+
GeoUtils.parseGeoPoint(toObject(parser2), randomBoolean()));
156+
assertThat(e.getMessage(), is("field must be either lat/lon or geohash"));
128157
}
129158

130159
public void testInvalidField() throws IOException {
@@ -135,9 +164,15 @@ public void testInvalidField() throws IOException {
135164

136165
XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content));
137166
parser.nextToken();
138-
139167
Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser));
140168
assertThat(e.getMessage(), is("field must be either [lat], [lon] or [geohash]"));
169+
170+
171+
XContentParser parser2 = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content));
172+
parser2.nextToken();
173+
e = expectThrows(ElasticsearchParseException.class, () ->
174+
GeoUtils.parseGeoPoint(toObject(parser2), randomBoolean()));
175+
assertThat(e.getMessage(), is("field must be either [lat], [lon] or [geohash]"));
141176
}
142177

143178
private XContentParser objectLatLon(double lat, double lon) throws IOException {
@@ -183,4 +218,22 @@ public static void assertCloseTo(final GeoPoint point, final double lat, final d
183218
assertEquals(point.lat(), lat, TOLERANCE);
184219
assertEquals(point.lon(), lon, TOLERANCE);
185220
}
221+
222+
public static Object toObject(XContentParser parser) throws IOException {
223+
XContentParser.Token token = parser.currentToken();
224+
if (token == XContentParser.Token.VALUE_NULL) {
225+
return null;
226+
} else if (token == XContentParser.Token.VALUE_STRING) {
227+
return parser.text();
228+
} else if (token == XContentParser.Token.VALUE_NUMBER) {
229+
return parser.numberValue();
230+
} else if (token == XContentParser.Token.START_OBJECT) {
231+
return parser.map();
232+
} else if (token == XContentParser.Token.START_ARRAY) {
233+
return parser.list();
234+
} else {
235+
fail("Unexpected token " + token);
236+
}
237+
return null;
238+
}
186239
}

0 commit comments

Comments
 (0)