Skip to content

Commit 7a9d9b0

Browse files
authored
Add support for ignore_unmapped to geo sort (#31153)
Adds support for `ignore_unmapped` parameter in geo distance sorting, which is functionally equivalent to specifying an `unmapped_type` in the field sort. Closes #28152
1 parent c352ff1 commit 7a9d9b0

File tree

4 files changed

+108
-7
lines changed

4 files changed

+108
-7
lines changed

docs/reference/search/request/sort.asciidoc

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,9 @@ GET /_search
288288
"pin.location" : [-70, 40],
289289
"order" : "asc",
290290
"unit" : "km",
291-
"mode" : "min",
292-
"distance_type" : "arc"
291+
"mode" : "min",
292+
"distance_type" : "arc",
293+
"ignore_unmapped": true
293294
}
294295
}
295296
],
@@ -317,6 +318,12 @@ GET /_search
317318

318319
The unit to use when computing sort values. The default is `m` (meters).
319320

321+
322+
`ignore_unmapped`::
323+
324+
Indicates if the unmapped field should be treated as a missing value. Setting it to `true` is equivalent to specifying
325+
an `unmapped_type` in the field sort. The default is `false` (unmapped field are causing the search to fail).
326+
320327
NOTE: geo distance sorting does not support configurable missing values: the
321328
distance will always be considered equal to +Infinity+ when a document does not
322329
have values for the field that is used for distance computation.

server/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public class GeoDistanceSortBuilder extends SortBuilder<GeoDistanceSortBuilder>
8181
private static final ParseField DISTANCE_TYPE_FIELD = new ParseField("distance_type");
8282
private static final ParseField VALIDATION_METHOD_FIELD = new ParseField("validation_method");
8383
private static final ParseField SORTMODE_FIELD = new ParseField("mode", "sort_mode");
84+
private static final ParseField IGNORE_UNMAPPED = new ParseField("ignore_unmapped");
8485

8586
private final String fieldName;
8687
private final List<GeoPoint> points = new ArrayList<>();
@@ -97,6 +98,8 @@ public class GeoDistanceSortBuilder extends SortBuilder<GeoDistanceSortBuilder>
9798

9899
private GeoValidationMethod validation = DEFAULT_VALIDATION;
99100

101+
private boolean ignoreUnmapped = false;
102+
100103
/**
101104
* Constructs a new distance based sort on a geo point like field.
102105
*
@@ -152,6 +155,7 @@ public GeoDistanceSortBuilder(String fieldName, String ... geohashes) {
152155
this.nestedPath = original.nestedPath;
153156
this.validation = original.validation;
154157
this.nestedSort = original.nestedSort;
158+
this.ignoreUnmapped = original.ignoreUnmapped;
155159
}
156160

157161
/**
@@ -171,6 +175,10 @@ public GeoDistanceSortBuilder(StreamInput in) throws IOException {
171175
nestedSort = in.readOptionalWriteable(NestedSortBuilder::new);
172176
}
173177
validation = GeoValidationMethod.readFromStream(in);
178+
// TODO: Change to 6_4_0 after backport
179+
if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
180+
ignoreUnmapped = in.readBoolean();
181+
}
174182
}
175183

176184
@Override
@@ -187,6 +195,10 @@ public void writeTo(StreamOutput out) throws IOException {
187195
out.writeOptionalWriteable(nestedSort);
188196
}
189197
validation.writeTo(out);
198+
// TODO: Change to 6_4_0 after backport
199+
if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
200+
out.writeBoolean(ignoreUnmapped);
201+
}
190202
}
191203

192204
/**
@@ -374,6 +386,18 @@ public GeoDistanceSortBuilder setNestedSort(final NestedSortBuilder nestedSort)
374386
return this;
375387
}
376388

389+
/**
390+
* Returns true if unmapped geo fields should be treated as located at an infinite distance
391+
*/
392+
public boolean ignoreUnmapped() {
393+
return ignoreUnmapped;
394+
}
395+
396+
public GeoDistanceSortBuilder ignoreUnmapped(boolean ignoreUnmapped) {
397+
this.ignoreUnmapped = ignoreUnmapped;
398+
return this;
399+
}
400+
377401
@Override
378402
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
379403
builder.startObject();
@@ -403,6 +427,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
403427
builder.field(NESTED_FIELD.getPreferredName(), nestedSort);
404428
}
405429
builder.field(VALIDATION_METHOD_FIELD.getPreferredName(), validation);
430+
builder.field(IGNORE_UNMAPPED.getPreferredName(), ignoreUnmapped);
406431

407432
builder.endObject();
408433
builder.endObject();
@@ -434,14 +459,15 @@ public boolean equals(Object object) {
434459
Objects.equals(nestedFilter, other.nestedFilter) &&
435460
Objects.equals(nestedPath, other.nestedPath) &&
436461
Objects.equals(validation, other.validation) &&
437-
Objects.equals(nestedSort, other.nestedSort);
462+
Objects.equals(nestedSort, other.nestedSort) &&
463+
ignoreUnmapped == other.ignoreUnmapped;
438464
}
439465

440466
@Override
441467
public int hashCode() {
442468
return Objects.hash(this.fieldName, this.points, this.geoDistance,
443469
this.unit, this.sortMode, this.order, this.nestedFilter,
444-
this.nestedPath, this.validation, this.nestedSort);
470+
this.nestedPath, this.validation, this.nestedSort, this.ignoreUnmapped);
445471
}
446472

447473
/**
@@ -465,6 +491,7 @@ public static GeoDistanceSortBuilder fromXContent(XContentParser parser, String
465491
String nestedPath = null;
466492
NestedSortBuilder nestedSort = null;
467493
GeoValidationMethod validation = null;
494+
boolean ignoreUnmapped = false;
468495

469496
XContentParser.Token token;
470497
String currentName = parser.currentName();
@@ -509,6 +536,8 @@ public static GeoDistanceSortBuilder fromXContent(XContentParser parser, String
509536
} else if (NESTED_PATH_FIELD.match(currentName, parser.getDeprecationHandler())) {
510537
DEPRECATION_LOGGER.deprecated("[nested_path] has been deprecated in favour of the [nested] parameter");
511538
nestedPath = parser.text();
539+
} else if (IGNORE_UNMAPPED.match(currentName, parser.getDeprecationHandler())) {
540+
ignoreUnmapped = parser.booleanValue();
512541
} else if (token == Token.VALUE_STRING){
513542
if (fieldName != null && fieldName.equals(currentName) == false) {
514543
throw new ParsingException(
@@ -554,6 +583,7 @@ public static GeoDistanceSortBuilder fromXContent(XContentParser parser, String
554583
if (validation != null) {
555584
result.validation(validation);
556585
}
586+
result.ignoreUnmapped(ignoreUnmapped);
557587
return result;
558588
}
559589

@@ -596,8 +626,11 @@ public SortFieldAndFormat build(QueryShardContext context) throws IOException {
596626

597627
MappedFieldType fieldType = context.fieldMapper(fieldName);
598628
if (fieldType == null) {
599-
throw new IllegalArgumentException("failed to find mapper for [" + fieldName
600-
+ "] for geo distance based sort");
629+
if (ignoreUnmapped) {
630+
fieldType = context.getMapperService().unmappedFieldType("geo_point");
631+
} else {
632+
throw new IllegalArgumentException("failed to find mapper for [" + fieldName + "] for geo distance based sort");
633+
}
601634
}
602635
final IndexGeoPointFieldData geoIndexFieldData = context.getForField(fieldType);
603636

server/src/test/java/org/elasticsearch/search/sort/GeoDistanceIT.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.hasId;
5050
import static org.hamcrest.Matchers.closeTo;
5151
import static org.hamcrest.Matchers.equalTo;
52+
import static org.hamcrest.Matchers.greaterThan;
5253

5354

5455
public class GeoDistanceIT extends ESIntegTestCase {
@@ -406,4 +407,57 @@ public void testGeoDistanceFilter() throws IOException {
406407
assertHitCount(result, 1);
407408
}
408409

410+
public void testDistanceSortingWithUnmappedField() throws Exception {
411+
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type1").startObject("properties")
412+
.startObject("locations").field("type", "geo_point");
413+
xContentBuilder.endObject().endObject().endObject().endObject();
414+
assertAcked(prepareCreate("test1").addMapping("type1", xContentBuilder));
415+
assertAcked(prepareCreate("test2"));
416+
ensureGreen();
417+
418+
client().prepareIndex("test1", "type1", "1")
419+
.setSource(jsonBuilder().startObject().array("names", "Times Square", "Tribeca").startArray("locations")
420+
// to NY: 5.286 km
421+
.startObject().field("lat", 40.759011).field("lon", -73.9844722).endObject()
422+
// to NY: 0.4621 km
423+
.startObject().field("lat", 40.718266).field("lon", -74.007819).endObject().endArray().endObject())
424+
.execute().actionGet();
425+
426+
client().prepareIndex("test2", "type1", "2")
427+
.setSource(jsonBuilder().startObject().array("names", "Wall Street", "Soho").endObject())
428+
.execute().actionGet();
429+
430+
refresh();
431+
432+
// Order: Asc
433+
SearchResponse searchResponse = client().prepareSearch("test1", "test2").setQuery(matchAllQuery())
434+
.addSort(SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).ignoreUnmapped(true).order(SortOrder.ASC)).execute()
435+
.actionGet();
436+
437+
assertHitCount(searchResponse, 2);
438+
assertOrderedSearchHits(searchResponse, "1", "2");
439+
assertThat(((Number) searchResponse.getHits().getAt(0).getSortValues()[0]).doubleValue(), closeTo(462.1d, 10d));
440+
assertThat(((Number) searchResponse.getHits().getAt(1).getSortValues()[0]).doubleValue(), equalTo(Double.POSITIVE_INFINITY));
441+
442+
// Order: Desc
443+
searchResponse = client().prepareSearch("test1", "test2").setQuery(matchAllQuery())
444+
.addSort(
445+
SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).ignoreUnmapped(true).order(SortOrder.DESC)
446+
).execute()
447+
.actionGet();
448+
449+
// Doc with missing geo point is first, is consistent with 0.20.x
450+
assertHitCount(searchResponse, 2);
451+
assertOrderedSearchHits(searchResponse, "2", "1");
452+
assertThat(((Number) searchResponse.getHits().getAt(0).getSortValues()[0]).doubleValue(), equalTo(Double.POSITIVE_INFINITY));
453+
assertThat(((Number) searchResponse.getHits().getAt(1).getSortValues()[0]).doubleValue(), closeTo(5286d, 10d));
454+
455+
// Make sure that by default the unmapped fields continue to fail
456+
searchResponse = client().prepareSearch("test1", "test2").setQuery(matchAllQuery())
457+
.addSort( SortBuilders.geoDistanceSort("locations", 40.7143528, -74.0059731).order(SortOrder.DESC)).execute()
458+
.actionGet();
459+
assertThat(searchResponse.getFailedShards(), greaterThan(0));
460+
assertHitCount(searchResponse, 1);
461+
}
462+
409463
}

server/src/test/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderTests.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.elasticsearch.index.query.RangeQueryBuilder;
4848
import org.elasticsearch.search.DocValueFormat;
4949
import org.elasticsearch.search.MultiValueMode;
50+
import org.elasticsearch.test.ESTestCase;
5051
import org.elasticsearch.test.geo.RandomGeoGenerator;
5152

5253
import java.io.IOException;
@@ -121,6 +122,9 @@ public static GeoDistanceSortBuilder randomGeoDistanceSortBuilder() {
121122
}
122123
}
123124
}
125+
if (randomBoolean()) {
126+
result.ignoreUnmapped(result.ignoreUnmapped() == false);
127+
}
124128
return result;
125129
}
126130

@@ -154,7 +158,7 @@ private static GeoDistance geoDistance(GeoDistance original) {
154158
@Override
155159
protected GeoDistanceSortBuilder mutate(GeoDistanceSortBuilder original) throws IOException {
156160
GeoDistanceSortBuilder result = new GeoDistanceSortBuilder(original);
157-
int parameter = randomIntBetween(0, 7);
161+
int parameter = randomIntBetween(0, 8);
158162
switch (parameter) {
159163
case 0:
160164
while (Arrays.deepEquals(original.points(), result.points())) {
@@ -194,6 +198,9 @@ protected GeoDistanceSortBuilder mutate(GeoDistanceSortBuilder original) throws
194198
case 7:
195199
result.validation(randomValueOtherThan(result.validation(), () -> randomFrom(GeoValidationMethod.values())));
196200
break;
201+
case 8:
202+
result.ignoreUnmapped(result.ignoreUnmapped() == false);
203+
break;
197204
}
198205
return result;
199206
}

0 commit comments

Comments
 (0)