From fe86c8bc88a321bf587dd8eb4df52aaed9ed2156 Mon Sep 17 00:00:00 2001 From: Britta Weber Date: Wed, 30 Jul 2014 18:48:36 +0200 Subject: [PATCH] _geo_distance sort: allow many to many geo point distance Add computation of disyance to many geo points. Example request: ``` { "sort": [ { "_geo_distance": { "location": [ { "lat":1.2, "lon":3 }, { "lat":1.2, "lon":3 } ], "order": "desc", "unit": "km", "sort_mode": "max" } } ] } ``` closes #3926 --- docs/reference/search/request/sort.asciidoc | 16 ++ .../elasticsearch/common/geo/GeoDistance.java | 23 ++- .../range/geodistance/GeoDistanceParser.java | 2 +- .../search/sort/GeoDistanceSortBuilder.java | 50 +++-- .../search/sort/GeoDistanceSortParser.java | 46 ++++- .../search/sort/SimpleSortTests.java | 195 ++++++++++++++++++ .../search/sort/SortParserTests.java | 149 +++++++++++++ .../test/ElasticsearchSingleNodeTest.java | 15 +- .../elasticsearch/test/TestSearchContext.java | 13 +- 9 files changed, 478 insertions(+), 31 deletions(-) create mode 100644 src/test/java/org/elasticsearch/search/sort/SortParserTests.java diff --git a/docs/reference/search/request/sort.asciidoc b/docs/reference/search/request/sort.asciidoc index fa3d5919546..a15af314b40 100644 --- a/docs/reference/search/request/sort.asciidoc +++ b/docs/reference/search/request/sort.asciidoc @@ -263,6 +263,22 @@ conform with http://geojson.org/[GeoJSON]. } -------------------------------------------------- + +==== Multiple reference points + +Multiple geo points can be passed as an array containing any `geo_point` format, for example + +``` +"pin.location" : [[-70, 40], [-71, 42]] +"pin.location" : [{"lat": -70, "lon": 40}, {"lat": -71, "lon": 42}] + +``` +and so forth. + +The final distance for a document will then be `min`/`max` distance of all points contained in the document to all points given in the sort request. + + + ==== Script Based Sorting Allow to sort based on custom scripts, here is an example: diff --git a/src/main/java/org/elasticsearch/common/geo/GeoDistance.java b/src/main/java/org/elasticsearch/common/geo/GeoDistance.java index 148e844367d..ac5d960c350 100644 --- a/src/main/java/org/elasticsearch/common/geo/GeoDistance.java +++ b/src/main/java/org/elasticsearch/common/geo/GeoDistance.java @@ -25,6 +25,7 @@ import org.elasticsearch.ElasticsearchIllegalArgumentException; import org.elasticsearch.common.unit.DistanceUnit; import org.elasticsearch.index.fielddata.*; +import java.util.List; import java.util.Locale; /** @@ -371,12 +372,13 @@ public enum GeoDistance { } } + /** - * Return a {@link SortedNumericDoubleValues} instance that returns the distance to a given geo-point for each document. + * Return a {@link SortedNumericDoubleValues} instance that returns the distances to a list of geo-points for each document. */ - public static SortedNumericDoubleValues distanceValues(final FixedSourceDistance distance, final MultiGeoPointValues geoPointValues) { + public static SortedNumericDoubleValues distanceValues(final MultiGeoPointValues geoPointValues, final FixedSourceDistance... distances) { final GeoPointValues singleValues = FieldData.unwrapSingleton(geoPointValues); - if (singleValues != null) { + if (singleValues != null && distances.length == 1) { final Bits docsWithField = FieldData.unwrapSingletonBits(geoPointValues); return FieldData.singleton(new NumericDoubleValues() { @@ -386,7 +388,7 @@ public enum GeoDistance { return 0d; } final GeoPoint point = singleValues.get(docID); - return distance.calculate(point.lat(), point.lon()); + return distances[0].calculate(point.lat(), point.lon()); } }, docsWithField); @@ -396,15 +398,18 @@ public enum GeoDistance { @Override public void setDocument(int doc) { geoPointValues.setDocument(doc); - count = geoPointValues.count(); + count = geoPointValues.count() * distances.length; grow(); - for (int i = 0; i < count; ++i) { - final GeoPoint point = geoPointValues.valueAt(i); - values[i] = distance.calculate(point.lat(), point.lon()); + int valueCounter = 0; + for (FixedSourceDistance distance : distances) { + for (int i = 0; i < geoPointValues.count(); ++i) { + final GeoPoint point = geoPointValues.valueAt(i); + values[valueCounter] = distance.calculate(point.lat(), point.lon()); + valueCounter++; + } } sort(); } - }; } } diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/GeoDistanceParser.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/GeoDistanceParser.java index 0348b1b3539..b2e68570a35 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/GeoDistanceParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/range/geodistance/GeoDistanceParser.java @@ -209,7 +209,7 @@ public class GeoDistanceParser implements Aggregator.Parser { public void setNextReader(AtomicReaderContext reader) { final MultiGeoPointValues geoValues = source.geoPointValues(); final FixedSourceDistance distance = distanceType.fixedSourceDistance(origin.getLat(), origin.getLon(), unit); - distanceValues = GeoDistance.distanceValues(distance, geoValues); + distanceValues = GeoDistance.distanceValues(geoValues, distance); } @Override diff --git a/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java b/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java index d99e7157af9..f8bb20805b6 100644 --- a/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java +++ b/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortBuilder.java @@ -19,12 +19,17 @@ package org.elasticsearch.search.sort; +import org.elasticsearch.ElasticsearchParseException; 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.index.query.FilterBuilder; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Locale; /** @@ -33,10 +38,8 @@ import java.util.Locale; public class GeoDistanceSortBuilder extends SortBuilder { final String fieldName; - - private double lat; - private double lon; - private String geohash; + private final List points = new ArrayList<>(); + private final List geohashes = new ArrayList<>(); private GeoDistance geoDistance; private DistanceUnit unit; @@ -61,16 +64,25 @@ public class GeoDistanceSortBuilder extends SortBuilder { * @param lon longitude. */ public GeoDistanceSortBuilder point(double lat, double lon) { - this.lat = lat; - this.lon = lon; + points.add(new GeoPoint(lat, lon)); + return this; + } + + /** + * The point to create the range distance facets from. + * + * @param points reference points. + */ + public GeoDistanceSortBuilder points(GeoPoint... points) { + this.points.addAll(Arrays.asList(points)); return this; } /** * The geohash of the geo point to create the range distance facets from. */ - public GeoDistanceSortBuilder geohash(String geohash) { - this.geohash = geohash; + public GeoDistanceSortBuilder geohashes(String... geohashes) { + this.geohashes.addAll(Arrays.asList(geohashes)); return this; } @@ -137,13 +149,23 @@ public class GeoDistanceSortBuilder extends SortBuilder { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject("_geo_distance"); - - if (geohash != null) { - builder.field(fieldName, geohash); - } else { - builder.startArray(fieldName).value(lon).value(lat).endArray(); + if (geohashes.size() == 0 && points.size() == 0) { + throw new ElasticsearchParseException("No points provided for _geo_distance sort."); + } + if (geohashes.size() == 1 && points.size() == 0) { + builder.field(fieldName, geohashes.get(0)); + } else if (geohashes.size() == 1 && points.size() == 0) { + builder.field(fieldName, points.get(0)); + } else { + builder.startArray(fieldName); + for (GeoPoint point : points) { + builder.value(point); + } + for (String geohash : geohashes) { + builder.value(geohash); + } + builder.endArray(); } - if (unit != null) { builder.field("unit", unit); } diff --git a/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortParser.java b/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortParser.java index 9832888d5c9..46aae3788de 100644 --- a/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortParser.java +++ b/src/main/java/org/elasticsearch/search/sort/GeoDistanceSortParser.java @@ -26,6 +26,7 @@ import org.apache.lucene.search.Filter; import org.apache.lucene.search.SortField; import org.apache.lucene.util.FixedBitSet; import org.elasticsearch.ElasticsearchIllegalArgumentException; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.geo.GeoDistance; import org.elasticsearch.common.geo.GeoDistance.FixedSourceDistance; import org.elasticsearch.common.geo.GeoPoint; @@ -43,6 +44,8 @@ import org.elasticsearch.search.MultiValueMode; import org.elasticsearch.search.internal.SearchContext; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; /** * @@ -57,7 +60,7 @@ public class GeoDistanceSortParser implements SortParser { @Override public SortField parse(XContentParser parser, SearchContext context) throws Exception { String fieldName = null; - GeoPoint point = new GeoPoint(); + List geoPoints = new ArrayList<>(); DistanceUnit unit = DistanceUnit.DEFAULT; GeoDistance geoDistance = GeoDistance.DEFAULT; boolean reverse = false; @@ -74,7 +77,8 @@ public class GeoDistanceSortParser implements SortParser { if (token == XContentParser.Token.FIELD_NAME) { currentName = parser.currentName(); } else if (token == XContentParser.Token.START_ARRAY) { - GeoUtils.parseGeoPoint(parser, point); + parseGeoPoints(parser, geoPoints); + fieldName = currentName; } else if (token == XContentParser.Token.START_OBJECT) { // the json in the format of -> field : { lat : 30, lon : 12 } @@ -83,7 +87,9 @@ public class GeoDistanceSortParser implements SortParser { nestedFilter = parsedFilter == null ? null : parsedFilter.filter(); } else { fieldName = currentName; + GeoPoint point = new GeoPoint(); GeoUtils.parseGeoPoint(parser, point); + geoPoints.add(point); } } else if (token.isValue()) { if ("reverse".equals(currentName)) { @@ -102,14 +108,18 @@ public class GeoDistanceSortParser implements SortParser { } else if ("nested_path".equals(currentName) || "nestedPath".equals(currentName)) { nestedPath = parser.text(); } else { + GeoPoint point = new GeoPoint(); point.resetFromString(parser.text()); + geoPoints.add(point); fieldName = currentName; } } } if (normalizeLat || normalizeLon) { - GeoUtils.normalizePoint(point, normalizeLat, normalizeLon); + for (GeoPoint point : geoPoints) { + GeoUtils.normalizePoint(point, normalizeLat, normalizeLon); + } } if (sortMode == null) { @@ -126,7 +136,10 @@ public class GeoDistanceSortParser implements SortParser { } final MultiValueMode finalSortMode = sortMode; // final reference for use in the anonymous class final IndexGeoPointFieldData geoIndexFieldData = context.fieldData().getForField(mapper); - final FixedSourceDistance distance = geoDistance.fixedSourceDistance(point.lat(), point.lon(), unit); + final FixedSourceDistance[] distances = new FixedSourceDistance[geoPoints.size()]; + for (int i = 0; i< geoPoints.size(); i++) { + distances[i] = geoDistance.fixedSourceDistance(geoPoints.get(i).lat(), geoPoints.get(i).lon(), unit); + } ObjectMapper objectMapper; if (nestedPath != null) { ObjectMappers objectMappers = context.mapperService().objectMapper(nestedPath); @@ -167,7 +180,7 @@ public class GeoDistanceSortParser implements SortParser { @Override protected Doubles getDoubleValues(AtomicReaderContext context, String field) throws IOException { final MultiGeoPointValues geoPointValues = geoIndexFieldData.load(context).getGeoPointValues(); - final SortedNumericDoubleValues distanceValues = GeoDistance.distanceValues(distance, geoPointValues); + final SortedNumericDoubleValues distanceValues = GeoDistance.distanceValues(geoPointValues, distances); final NumericDoubleValues selectedValues; if (nested == null) { selectedValues = finalSortMode.select(distanceValues, Double.MAX_VALUE); @@ -190,4 +203,27 @@ public class GeoDistanceSortParser implements SortParser { return new SortField(fieldName, geoDistanceComparatorSource, reverse); } + + private void parseGeoPoints(XContentParser parser, List geoPoints) throws IOException { + while (!parser.nextToken().equals(XContentParser.Token.END_ARRAY)) { + if (parser.currentToken() == XContentParser.Token.VALUE_NUMBER) { + // we might get here if the geo point is " number, number] " and the parser already moved over the opening bracket + // in this case we cannot use GeoUtils.parseGeoPoint(..) because this expects an opening bracket + double lon = parser.doubleValue(); + parser.nextToken(); + if (!parser.currentToken().equals(XContentParser.Token.VALUE_NUMBER)) { + throw new ElasticsearchParseException("geo point parsing: expected second number but got" + parser.currentToken()); + } + double lat = parser.doubleValue(); + GeoPoint point = new GeoPoint(); + point.reset(lat, lon); + geoPoints.add(point); + } else { + GeoPoint point = new GeoPoint(); + GeoUtils.parseGeoPoint(parser, point); + geoPoints.add(point); + } + + } + } } diff --git a/src/test/java/org/elasticsearch/search/sort/SimpleSortTests.java b/src/test/java/org/elasticsearch/search/sort/SimpleSortTests.java index 47abb0669ff..80d6c76a699 100644 --- a/src/test/java/org/elasticsearch/search/sort/SimpleSortTests.java +++ b/src/test/java/org/elasticsearch/search/sort/SimpleSortTests.java @@ -30,8 +30,11 @@ import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.geo.GeoDistance; +import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.text.StringAndBytesText; import org.elasticsearch.common.text.Text; +import org.elasticsearch.common.unit.DistanceUnit; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.index.mapper.Uid; @@ -1675,5 +1678,197 @@ public class SimpleSortTests extends ElasticsearchIntegrationTest { } } + public void testManyToManyGeoPoints() throws ExecutionException, InterruptedException, IOException { + /** + * | q | d1 | d2 + * | | | + * | | | + * | | | + * |2 o| x | x + * | | | + * |1 o| x | x + * |___________________________ + * 1 2 3 4 5 6 7 + */ + assertAcked(prepareCreate("index").addMapping("type", "location", "type=geo_point")); + XContentBuilder d1Builder = jsonBuilder(); + GeoPoint[] d1Points = {new GeoPoint(3, 2), new GeoPoint(4, 1)}; + createShuffeldJSONArray(d1Builder, d1Points); + + XContentBuilder d2Builder = jsonBuilder(); + GeoPoint[] d2Points = {new GeoPoint(5, 1), new GeoPoint(6, 2)}; + createShuffeldJSONArray(d2Builder, d2Points); + + logger.info(d1Builder.string()); + logger.info(d2Builder.string()); + indexRandom(true, + client().prepareIndex("index", "type", "d1").setSource(d1Builder), + client().prepareIndex("index", "type", "d2").setSource(d2Builder)); + ensureYellow(); + GeoPoint[] q = new GeoPoint[2]; + if (randomBoolean()) { + q[0] = new GeoPoint(2, 1); + q[1] = new GeoPoint(2, 2); + } else { + q[1] = new GeoPoint(2, 2); + q[0] = new GeoPoint(2, 1); + } + + SearchResponse searchResponse = client().prepareSearch() + .setQuery(matchAllQuery()) + .addSort(new GeoDistanceSortBuilder("location").points(q).sortMode("min").order(SortOrder.ASC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) + .execute().actionGet(); + assertOrderedSearchHits(searchResponse, "d1", "d2"); + assertThat((Double) searchResponse.getHits().getAt(0).getSortValues()[0], equalTo(GeoDistance.PLANE.calculate(2, 2, 3, 2, DistanceUnit.KILOMETERS))); + assertThat((Double) searchResponse.getHits().getAt(1).getSortValues()[0], equalTo(GeoDistance.PLANE.calculate(2, 1, 5, 1, DistanceUnit.KILOMETERS))); + + searchResponse = client().prepareSearch() + .setQuery(matchAllQuery()) + .addSort(new GeoDistanceSortBuilder("location").points(q).sortMode("min").order(SortOrder.DESC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) + .execute().actionGet(); + assertOrderedSearchHits(searchResponse, "d2", "d1"); + assertThat((Double) searchResponse.getHits().getAt(0).getSortValues()[0], equalTo(GeoDistance.PLANE.calculate(2, 1, 5, 1, DistanceUnit.KILOMETERS))); + assertThat((Double) searchResponse.getHits().getAt(1).getSortValues()[0], equalTo(GeoDistance.PLANE.calculate(2, 2, 3, 2, DistanceUnit.KILOMETERS))); + + searchResponse = client().prepareSearch() + .setQuery(matchAllQuery()) + .addSort(new GeoDistanceSortBuilder("location").points(q).sortMode("max").order(SortOrder.ASC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) + .execute().actionGet(); + assertOrderedSearchHits(searchResponse, "d1", "d2"); + assertThat((Double) searchResponse.getHits().getAt(0).getSortValues()[0], equalTo(GeoDistance.PLANE.calculate(2, 2, 4, 1, DistanceUnit.KILOMETERS))); + assertThat((Double) searchResponse.getHits().getAt(1).getSortValues()[0], equalTo(GeoDistance.PLANE.calculate(2, 1, 6, 2, DistanceUnit.KILOMETERS))); + + searchResponse = client().prepareSearch() + .setQuery(matchAllQuery()) + .addSort(new GeoDistanceSortBuilder("location").points(q).sortMode("max").order(SortOrder.DESC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) + .execute().actionGet(); + assertOrderedSearchHits(searchResponse, "d2", "d1"); + assertThat((Double) searchResponse.getHits().getAt(0).getSortValues()[0], equalTo(GeoDistance.PLANE.calculate(2, 1, 6, 2, DistanceUnit.KILOMETERS))); + assertThat((Double) searchResponse.getHits().getAt(1).getSortValues()[0], equalTo(GeoDistance.PLANE.calculate(2, 2, 4, 1, DistanceUnit.KILOMETERS))); + } + + protected void createShuffeldJSONArray(XContentBuilder builder, GeoPoint[] pointsArray) throws IOException { + List points = new ArrayList<>(); + points.addAll(Arrays.asList(pointsArray)); + builder.startObject(); + builder.startArray("location"); + int numPoints = points.size(); + for (int i = 0; i < numPoints; i++) { + builder.value(points.remove(randomInt(points.size() - 1))); + } + builder.endArray(); + builder.endObject(); + } + + public void testManyToManyGeoPointsWithDifferentFormats() throws ExecutionException, InterruptedException, IOException { + /** q d1 d2 + * |4 o| x | x + * | | | + * |3 o| x | x + * | | | + * |2 o| x | x + * | | | + * |1 o|x |x + * |______________________ + * 1 2 3 4 5 6 + */ + assertAcked(prepareCreate("index").addMapping("type", "location", "type=geo_point")); + XContentBuilder d1Builder = jsonBuilder(); + GeoPoint[] d1Points = {new GeoPoint(2.5, 1), new GeoPoint(2.75, 2), new GeoPoint(3, 3), new GeoPoint(3.25, 4)}; + createShuffeldJSONArray(d1Builder, d1Points); + + XContentBuilder d2Builder = jsonBuilder(); + GeoPoint[] d2Points = {new GeoPoint(4.5, 1), new GeoPoint(4.75, 2), new GeoPoint(5, 3), new GeoPoint(5.25, 4)}; + createShuffeldJSONArray(d2Builder, d2Points); + + indexRandom(true, + client().prepareIndex("index", "type", "d1").setSource(d1Builder), + client().prepareIndex("index", "type", "d2").setSource(d2Builder)); + ensureYellow(); + + List qHashes = new ArrayList<>(); + List qPoints = new ArrayList<>(); + createQPoints(qHashes, qPoints); + + GeoDistanceSortBuilder geoDistanceSortBuilder = new GeoDistanceSortBuilder("location"); + for (int i = 0; i < 4; i++) { + int at = randomInt(3 - i); + if (randomBoolean()) { + geoDistanceSortBuilder.geohashes(qHashes.get(at)); + } else { + geoDistanceSortBuilder.points(qPoints.get(at)); + } + qHashes.remove(at); + qPoints.remove(at); + } + + SearchResponse searchResponse = client().prepareSearch() + .setQuery(matchAllQuery()) + .addSort(geoDistanceSortBuilder.sortMode("min").order(SortOrder.ASC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) + .execute().actionGet(); + assertOrderedSearchHits(searchResponse, "d1", "d2"); + assertThat((Double) searchResponse.getHits().getAt(0).getSortValues()[0], closeTo(GeoDistance.PLANE.calculate(2.5, 1, 2, 1, DistanceUnit.KILOMETERS), 1.e-5)); + assertThat((Double) searchResponse.getHits().getAt(1).getSortValues()[0], closeTo(GeoDistance.PLANE.calculate(4.5, 1, 2, 1, DistanceUnit.KILOMETERS), 1.e-5)); + + searchResponse = client().prepareSearch() + .setQuery(matchAllQuery()) + .addSort(geoDistanceSortBuilder.sortMode("max").order(SortOrder.ASC).geoDistance(GeoDistance.PLANE).unit(DistanceUnit.KILOMETERS)) + .execute().actionGet(); + assertOrderedSearchHits(searchResponse, "d1", "d2"); + assertThat((Double) searchResponse.getHits().getAt(0).getSortValues()[0], closeTo(GeoDistance.PLANE.calculate(3.25, 4, 2, 1, DistanceUnit.KILOMETERS), 1.e-5)); + assertThat((Double) searchResponse.getHits().getAt(1).getSortValues()[0], closeTo(GeoDistance.PLANE.calculate(5.25, 4, 2, 1, DistanceUnit.KILOMETERS), 1.e-5)); + + //test all the different formats in one + createQPoints(qHashes, qPoints); + XContentBuilder searchSourceBuilder = jsonBuilder(); + searchSourceBuilder.startObject().startArray("sort").startObject().startObject("_geo_distance").startArray("location"); + + for (int i = 0; i < 4; i++) { + int at = randomInt(qPoints.size() - 1); + int format = randomInt(3); + switch (format) { + case 0: { + searchSourceBuilder.value(qHashes.get(at)); + break; + } + case 1: { + searchSourceBuilder.value(qPoints.get(at).lat() + "," + qPoints.get(at).lon()); + break; + } + case 2: { + searchSourceBuilder.value(qPoints.get(at)); + break; + } + case 3: { + searchSourceBuilder.startArray().value(qPoints.get(at).lon()).value(qPoints.get(at).lat()).endArray(); + break; + } + } + qHashes.remove(at); + qPoints.remove(at); + } + + searchSourceBuilder.endArray(); + searchSourceBuilder.field("order", "asc"); + searchSourceBuilder.field("unit", "km"); + searchSourceBuilder.field("sort_mode", "min"); + searchSourceBuilder.field("distance_type", "plane"); + searchSourceBuilder.endObject(); + searchSourceBuilder.endObject(); + searchSourceBuilder.endArray(); + searchSourceBuilder.endObject(); + + searchResponse = client().prepareSearch().setSource(searchSourceBuilder).execute().actionGet(); + assertOrderedSearchHits(searchResponse, "d1", "d2"); + assertThat((Double) searchResponse.getHits().getAt(0).getSortValues()[0], closeTo(GeoDistance.PLANE.calculate(2.5, 1, 2, 1, DistanceUnit.KILOMETERS), 1.e-5)); + assertThat((Double) searchResponse.getHits().getAt(1).getSortValues()[0], closeTo(GeoDistance.PLANE.calculate(4.5, 1, 2, 1, DistanceUnit.KILOMETERS), 1.e-5)); + } + + protected void createQPoints(List qHashes, List qPoints) { + GeoPoint[] qp = {new GeoPoint(2, 1), new GeoPoint(2, 2), new GeoPoint(2, 3), new GeoPoint(2, 4)}; + qPoints.addAll(Arrays.asList(qp)); + String[] qh = {"s02equ04ven0", "s037ms06g7h0", "s065kk0dc540", "s06g7h0dyg00"}; + qHashes.addAll(Arrays.asList(qh)); + } } diff --git a/src/test/java/org/elasticsearch/search/sort/SortParserTests.java b/src/test/java/org/elasticsearch/search/sort/SortParserTests.java new file mode 100644 index 00000000000..614492c7d57 --- /dev/null +++ b/src/test/java/org/elasticsearch/search/sort/SortParserTests.java @@ -0,0 +1,149 @@ +/* + * 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.elasticsearch.common.geo.GeoPoint; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.service.IndexService; +import org.elasticsearch.test.ElasticsearchSingleNodeTest; +import org.elasticsearch.test.TestSearchContext; +import org.junit.Test; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; + +public class SortParserTests extends ElasticsearchSingleNodeTest { + + @Test + public void testGeoDistanceSortParserManyPointsNoException() throws Exception { + XContentBuilder mapping = jsonBuilder(); + mapping.startObject().startObject("type").startObject("properties").startObject("location").field("type", "geo_point").endObject().endObject().endObject().endObject(); + IndexService indexService = createIndex("testidx", ImmutableSettings.settingsBuilder().build(), "type", mapping); + TestSearchContext context = (TestSearchContext) createSearchContext(indexService); + context.setTypes("type"); + + 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(); + XContentParser parser = XContentHelper.createParser(sortBuilder.bytes()); + parser.nextToken(); + GeoDistanceSortParser geoParser = new GeoDistanceSortParser(); + geoParser.parse(parser, context); + + 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("sort_mode", "max"); + sortBuilder.endObject(); + parse(context, 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("sort_mode", "max"); + sortBuilder.endObject(); + parse(context, 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("sort_mode", "max"); + sortBuilder.endObject(); + parse(context, 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("sort_mode", "max"); + sortBuilder.endObject(); + parse(context, sortBuilder); + + sortBuilder = jsonBuilder(); + sortBuilder.startObject(); + sortBuilder.field("location", new GeoPoint(1, 2)); + sortBuilder.field("order", "desc"); + sortBuilder.field("unit", "km"); + sortBuilder.field("sort_mode", "max"); + sortBuilder.endObject(); + parse(context, sortBuilder); + + sortBuilder = jsonBuilder(); + sortBuilder.startObject(); + sortBuilder.field("location", "1,2"); + sortBuilder.field("order", "desc"); + sortBuilder.field("unit", "km"); + sortBuilder.field("sort_mode", "max"); + sortBuilder.endObject(); + parse(context, sortBuilder); + + sortBuilder = jsonBuilder(); + sortBuilder.startObject(); + sortBuilder.field("location", "s3y0zh7w1z0g"); + sortBuilder.field("order", "desc"); + sortBuilder.field("unit", "km"); + sortBuilder.field("sort_mode", "max"); + sortBuilder.endObject(); + parse(context, 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("sort_mode", "max"); + sortBuilder.endObject(); + parse(context, sortBuilder); + } + + protected void parse(TestSearchContext context, XContentBuilder sortBuilder) throws Exception { + XContentParser parser = XContentHelper.createParser(sortBuilder.bytes()); + parser.nextToken(); + GeoDistanceSortParser geoParser = new GeoDistanceSortParser(); + geoParser.parse(parser, context); + } +} diff --git a/src/test/java/org/elasticsearch/test/ElasticsearchSingleNodeTest.java b/src/test/java/org/elasticsearch/test/ElasticsearchSingleNodeTest.java index 3d6cee03cdc..e55df4e5f82 100644 --- a/src/test/java/org/elasticsearch/test/ElasticsearchSingleNodeTest.java +++ b/src/test/java/org/elasticsearch/test/ElasticsearchSingleNodeTest.java @@ -20,6 +20,7 @@ package org.elasticsearch.test; import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; import org.elasticsearch.action.admin.cluster.health.ClusterHealthStatus; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder; import org.elasticsearch.cache.recycler.CacheRecycler; import org.elasticsearch.cache.recycler.PageCacheRecycler; import org.elasticsearch.client.Requests; @@ -32,6 +33,7 @@ import org.elasticsearch.common.settings.ImmutableSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.service.IndexService; import org.elasticsearch.indices.IndicesService; import org.elasticsearch.node.Node; @@ -126,7 +128,18 @@ public abstract class ElasticsearchSingleNodeTest extends ElasticsearchTestCase * Create a new index on the singleton node with the provided index settings. */ protected static IndexService createIndex(String index, Settings settings) { - assertAcked(Holder.NODE.client().admin().indices().prepareCreate(index).setSettings(settings).get()); + return createIndex(index, settings, null, null); + } + + /** + * Create a new index on the singleton node with the provided index settings. + */ + protected static IndexService createIndex(String index, Settings settings, String type, XContentBuilder mappings) { + CreateIndexRequestBuilder createIndexRequestBuilder = Holder.NODE.client().admin().indices().prepareCreate(index).setSettings(settings); + if (type != null && mappings != null) { + createIndexRequestBuilder.addMapping(type, mappings); + } + assertAcked(createIndexRequestBuilder.get()); // Wait for the index to be allocated so that cluster state updates don't override // changes that would have been done locally ClusterHealthResponse health = Holder.NODE.client().admin().cluster() diff --git a/src/test/java/org/elasticsearch/test/TestSearchContext.java b/src/test/java/org/elasticsearch/test/TestSearchContext.java index e4599610dcc..e98d5a0b75d 100644 --- a/src/test/java/org/elasticsearch/test/TestSearchContext.java +++ b/src/test/java/org/elasticsearch/test/TestSearchContext.java @@ -77,6 +77,7 @@ public class TestSearchContext extends SearchContext { ContextIndexSearcher searcher; int size; private int terminateAfter = DEFAULT_TERMINATE_AFTER; + private String[] types; public TestSearchContext(ThreadPool threadPool, CacheRecycler cacheRecycler, PageCacheRecycler pageCacheRecycler, BigArrays bigArrays, IndexService indexService, FilterCache filterCache, IndexFieldDataService indexFieldDataService) { this.cacheRecycler = cacheRecycler; @@ -98,6 +99,10 @@ public class TestSearchContext extends SearchContext { this.threadPool = null; } + public void setTypes(String... types) { + this.types = types; + } + @Override public void preProcess() { } @@ -290,7 +295,10 @@ public class TestSearchContext extends SearchContext { @Override public MapperService mapperService() { - return indexService.mapperService(); + if (indexService != null) { + return indexService.mapperService(); + } + return null; } @Override @@ -582,6 +590,9 @@ public class TestSearchContext extends SearchContext { @Override public FieldMapper smartNameFieldMapper(String name) { + if (mapperService() != null) { + return mapperService().smartNameFieldMapper(name, types()); + } return null; }