/* * 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.search.sort; import org.apache.lucene.document.LatLonDocValuesField; import org.apache.lucene.search.SortField; import org.elasticsearch.common.geo.GeoDistance; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.unit.DistanceUnit; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.mapper.GeoPointFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.query.GeoValidationMethod; import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.index.query.QueryParseContext; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.test.geo.RandomGeoGenerator; import java.io.IOException; import java.util.Arrays; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; public class GeoDistanceSortBuilderTests extends AbstractSortTestCase<GeoDistanceSortBuilder> { @Override protected GeoDistanceSortBuilder createTestItem() { return randomGeoDistanceSortBuilder(); } public static GeoDistanceSortBuilder randomGeoDistanceSortBuilder() { String fieldName = randomAlphaOfLengthBetween(1, 10); GeoDistanceSortBuilder result = null; int id = randomIntBetween(0, 2); switch(id) { case 0: int count = randomIntBetween(1, 10); String[] geohashes = new String[count]; for (int i = 0; i < count; i++) { geohashes[i] = RandomGeoGenerator.randomPoint(random()).geohash(); } result = new GeoDistanceSortBuilder(fieldName, geohashes); break; case 1: GeoPoint pt = RandomGeoGenerator.randomPoint(random()); result = new GeoDistanceSortBuilder(fieldName, pt.getLat(), pt.getLon()); break; case 2: result = new GeoDistanceSortBuilder(fieldName, points(new GeoPoint[0])); break; default: throw new IllegalStateException("one of three geo initialisation strategies must be used"); } if (randomBoolean()) { result.geoDistance(geoDistance(result.geoDistance())); } if (randomBoolean()) { result.unit(randomValueOtherThan(result.unit(), () -> randomFrom(DistanceUnit.values()))); } if (randomBoolean()) { result.order(randomFrom(SortOrder.values())); } if (randomBoolean()) { result.sortMode(randomValueOtherThan(SortMode.SUM, () -> randomFrom(SortMode.values()))); } if (randomBoolean()) { result.setNestedFilter(new MatchAllQueryBuilder()); } if (randomBoolean()) { result.setNestedPath( randomValueOtherThan( result.getNestedPath(), () -> randomAlphaOfLengthBetween(1, 10))); } if (randomBoolean()) { result.validation(randomValueOtherThan(result.validation(), () -> randomFrom(GeoValidationMethod.values()))); } return result; } @Override protected MappedFieldType provideMappedFieldType(String name) { MappedFieldType clone = GeoPointFieldMapper.Defaults.FIELD_TYPE.clone(); clone.setName(name); return clone; } private static GeoPoint[] points(GeoPoint[] original) { GeoPoint[] result = null; while (result == null || Arrays.deepEquals(original, result)) { int count = randomIntBetween(1, 10); result = new GeoPoint[count]; for (int i = 0; i < count; i++) { result[i] = RandomGeoGenerator.randomPoint(random()); } } return result; } private static GeoDistance geoDistance(GeoDistance original) { int id = -1; while (id == -1 || (original != null && original.ordinal() == id)) { id = randomIntBetween(0, GeoDistance.values().length - 1); } return GeoDistance.values()[id]; } @Override protected GeoDistanceSortBuilder mutate(GeoDistanceSortBuilder original) throws IOException { GeoDistanceSortBuilder result = new GeoDistanceSortBuilder(original); int parameter = randomIntBetween(0, 8); switch (parameter) { case 0: while (Arrays.deepEquals(original.points(), result.points())) { GeoPoint pt = RandomGeoGenerator.randomPoint(random()); result.point(pt.getLat(), pt.getLon()); } break; case 1: result.points(points(original.points())); break; case 2: result.geoDistance(geoDistance(original.geoDistance())); break; case 3: result.unit(randomValueOtherThan(result.unit(), () -> randomFrom(DistanceUnit.values()))); break; case 4: result.order(randomValueOtherThan(original.order(), () -> randomFrom(SortOrder.values()))); break; case 5: result.sortMode(randomValueOtherThanMany( Arrays.asList(SortMode.SUM, result.sortMode())::contains, () -> randomFrom(SortMode.values()))); break; case 6: result.setNestedFilter(randomValueOtherThan( original.getNestedFilter(), () -> randomNestedFilter())); break; case 7: result.setNestedPath(randomValueOtherThan( result.getNestedPath(), () -> randomAlphaOfLengthBetween(1, 10))); break; case 8: result.validation(randomValueOtherThan(result.validation(), () -> randomFrom(GeoValidationMethod.values()))); break; } return result; } @Override protected void sortFieldAssertions(GeoDistanceSortBuilder builder, SortField sortField, DocValueFormat format) throws IOException { assertEquals(builder.order() == SortOrder.ASC ? false : true, sortField.getReverse()); assertEquals(builder.fieldName(), sortField.getField()); } public void testSortModeSumIsRejectedInSetter() { GeoDistanceSortBuilder builder = new GeoDistanceSortBuilder("testname", -1, -1); GeoPoint point = RandomGeoGenerator.randomPoint(random()); builder.point(point.getLat(), point.getLon()); try { builder.sortMode(SortMode.SUM); fail("sort mode sum should not be supported"); } catch (IllegalArgumentException e) { // all good } } public void testSortModeSumIsRejectedInJSON() throws IOException { String json = "{\n" + " \"testname\" : [ {\n" + " \"lat\" : -6.046997540714173,\n" + " \"lon\" : -51.94128329747579\n" + " } ],\n" + " \"unit\" : \"m\",\n" + " \"distance_type\" : \"arc\",\n" + " \"mode\" : \"SUM\"\n" + "}"; XContentParser itemParser = createParser(JsonXContent.jsonXContent, json); itemParser.nextToken(); QueryParseContext context = new QueryParseContext(itemParser); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> GeoDistanceSortBuilder.fromXContent(context, "")); assertEquals("sort_mode [sum] isn't supported for sorting by geo distance", e.getMessage()); } public void testGeoDistanceSortCanBeParsedFromGeoHash() throws IOException { String json = "{\n" + " \"VDcvDuFjE\" : [ \"7umzzv8eychg\", \"dmdgmt5z13uw\", " + " \"ezu09wxw6v4c\", \"kc7s3515p6k6\", \"jgeuvjwrmfzn\", \"kcpcfj7ruyf8\" ],\n" + " \"unit\" : \"m\",\n" + " \"distance_type\" : \"arc\",\n" + " \"mode\" : \"MAX\",\n" + " \"nested_filter\" : {\n" + " \"ids\" : {\n" + " \"type\" : [ ],\n" + " \"values\" : [ ],\n" + " \"boost\" : 5.711116\n" + " }\n" + " },\n" + " \"validation_method\" : \"STRICT\"\n" + " }"; XContentParser itemParser = createParser(JsonXContent.jsonXContent, json); itemParser.nextToken(); QueryParseContext context = new QueryParseContext(itemParser); GeoDistanceSortBuilder result = GeoDistanceSortBuilder.fromXContent(context, json); assertEquals("[-19.700583312660456, -2.8225036337971687, " + "31.537466906011105, -74.63590376079082, " + "43.71844606474042, -5.548660643398762, " + "-37.20467280596495, 38.71751043945551, " + "-69.44606635719538, 84.25200328230858, " + "-39.03717711567879, 44.74099852144718]", Arrays.toString(result.points())); } public void testGeoDistanceSortParserManyPointsNoException() throws Exception { XContentBuilder sortBuilder = jsonBuilder(); sortBuilder.startObject(); sortBuilder.startArray("location"); sortBuilder.startArray().value(1.2).value(3).endArray().startArray().value(5).value(6).endArray(); sortBuilder.endArray(); sortBuilder.field("order", "desc"); sortBuilder.field("unit", "km"); sortBuilder.field("mode", "max"); sortBuilder.endObject(); parse(sortBuilder); sortBuilder = jsonBuilder(); sortBuilder.startObject(); sortBuilder.startArray("location"); sortBuilder.value(new GeoPoint(1.2, 3)).value(new GeoPoint(1.2, 3)); sortBuilder.endArray(); sortBuilder.field("order", "desc"); sortBuilder.field("unit", "km"); sortBuilder.field("mode", "max"); sortBuilder.endObject(); parse(sortBuilder); sortBuilder = jsonBuilder(); sortBuilder.startObject(); sortBuilder.startArray("location"); sortBuilder.value("1,2").value("3,4"); sortBuilder.endArray(); sortBuilder.field("order", "desc"); sortBuilder.field("unit", "km"); sortBuilder.field("mode", "max"); sortBuilder.endObject(); parse(sortBuilder); sortBuilder = jsonBuilder(); sortBuilder.startObject(); sortBuilder.startArray("location"); sortBuilder.value("s3y0zh7w1z0g").value("s6wjr4et3f8v"); sortBuilder.endArray(); sortBuilder.field("order", "desc"); sortBuilder.field("unit", "km"); sortBuilder.field("mode", "max"); sortBuilder.endObject(); parse(sortBuilder); sortBuilder = jsonBuilder(); sortBuilder.startObject(); sortBuilder.startArray("location"); sortBuilder.value(1.2).value(3); sortBuilder.endArray(); sortBuilder.field("order", "desc"); sortBuilder.field("unit", "km"); sortBuilder.field("mode", "max"); sortBuilder.endObject(); parse(sortBuilder); sortBuilder = jsonBuilder(); sortBuilder.startObject(); sortBuilder.field("location", new GeoPoint(1, 2)); sortBuilder.field("order", "desc"); sortBuilder.field("unit", "km"); sortBuilder.field("mode", "max"); sortBuilder.endObject(); parse(sortBuilder); sortBuilder = jsonBuilder(); sortBuilder.startObject(); sortBuilder.field("location", "1,2"); sortBuilder.field("order", "desc"); sortBuilder.field("unit", "km"); sortBuilder.field("mode", "max"); sortBuilder.endObject(); parse(sortBuilder); sortBuilder = jsonBuilder(); sortBuilder.startObject(); sortBuilder.field("location", "s3y0zh7w1z0g"); sortBuilder.field("order", "desc"); sortBuilder.field("unit", "km"); sortBuilder.field("mode", "max"); sortBuilder.endObject(); parse(sortBuilder); sortBuilder = jsonBuilder(); sortBuilder.startObject(); sortBuilder.startArray("location"); sortBuilder.value(new GeoPoint(1, 2)).value("s3y0zh7w1z0g").startArray().value(1).value(2).endArray().value("1,2"); sortBuilder.endArray(); sortBuilder.field("order", "desc"); sortBuilder.field("unit", "km"); sortBuilder.field("mode", "max"); sortBuilder.endObject(); parse(sortBuilder); } public void testGeoDistanceSortDeprecatedSortModeException() throws Exception { XContentBuilder sortBuilder = jsonBuilder(); sortBuilder.startObject(); sortBuilder.startArray("location"); sortBuilder.startArray().value(1.2).value(3).endArray().startArray().value(5).value(6).endArray(); sortBuilder.endArray(); sortBuilder.field("order", "desc"); sortBuilder.field("unit", "km"); sortBuilder.field("sort_mode", "max"); sortBuilder.endObject(); parse(sortBuilder); assertWarnings("Deprecated field [sort_mode] used, expected [mode] instead"); } private GeoDistanceSortBuilder parse(XContentBuilder sortBuilder) throws Exception { XContentParser parser = createParser(sortBuilder); QueryParseContext parseContext = new QueryParseContext(parser); parser.nextToken(); return GeoDistanceSortBuilder.fromXContent(parseContext, null); } @Override protected GeoDistanceSortBuilder fromXContent(QueryParseContext context, String fieldName) throws IOException { return GeoDistanceSortBuilder.fromXContent(context, fieldName); } public void testCommonCaseIsOptimized() throws IOException { // make sure the below tests test something... assertFalse(SortField.class.equals(LatLonDocValuesField.newDistanceSort("random_field_name", 3.5, 2.1).getClass())); QueryShardContext context = createMockShardContext(); // The common case should use LatLonDocValuesField.newDistanceSort GeoDistanceSortBuilder builder = new GeoDistanceSortBuilder("", new GeoPoint(3.5, 2.1)); SortFieldAndFormat sort = builder.build(context); assertEquals(LatLonDocValuesField.newDistanceSort("random_field_name", 3.5, 2.1).getClass(), sort.field.getClass()); // however this might be disabled by fancy options builder = new GeoDistanceSortBuilder("random_field_name", new GeoPoint(3.5, 2.1), new GeoPoint(3.0, 4)); sort = builder.build(context); assertEquals(SortField.class, sort.field.getClass()); // 2 points -> plain SortField with a custom comparator builder = new GeoDistanceSortBuilder("random_field_name", new GeoPoint(3.5, 2.1)); builder.unit(DistanceUnit.KILOMETERS); sort = builder.build(context); assertEquals(SortField.class, sort.field.getClass()); // km rather than m -> plain SortField with a custom comparator builder = new GeoDistanceSortBuilder("random_field_name", new GeoPoint(3.5, 2.1)); builder.order(SortOrder.DESC); sort = builder.build(context); assertEquals(SortField.class, sort.field.getClass()); // descending means the max value should be considered rather than min builder = new GeoDistanceSortBuilder("random_field_name", new GeoPoint(3.5, 2.1)); builder.setNestedPath("some_nested_path"); sort = builder.build(context); assertEquals(SortField.class, sort.field.getClass()); // can't use LatLon optimized sorting with nested fields builder = new GeoDistanceSortBuilder("random_field_name", new GeoPoint(3.5, 2.1)); builder.order(SortOrder.DESC); sort = builder.build(context); assertEquals(SortField.class, sort.field.getClass()); // can't use LatLon optimized sorting with DESC sorting } }