/*
* 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.index.query;
import org.apache.lucene.document.LatLonDocValuesField;
import org.apache.lucene.document.LatLonPoint;
import org.apache.lucene.search.IndexOrDocValuesQuery;
import org.apache.lucene.search.MatchNoDocsQuery;
import org.apache.lucene.search.Query;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.geo.GeoDistance;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.search.internal.SearchContext;
import org.elasticsearch.test.AbstractQueryTestCase;
import org.elasticsearch.test.geo.RandomShapeGenerator;
import org.locationtech.spatial4j.shape.Point;
import java.io.IOException;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.notNullValue;
public class GeoDistanceQueryBuilderTests extends AbstractQueryTestCase<GeoDistanceQueryBuilder> {
@Override
protected GeoDistanceQueryBuilder doCreateTestQueryBuilder() {
GeoDistanceQueryBuilder qb = new GeoDistanceQueryBuilder(GEO_POINT_FIELD_NAME);
String distance = "" + randomDouble();
if (randomBoolean()) {
DistanceUnit unit = randomFrom(DistanceUnit.values());
distance = distance + unit.toString();
}
int selector = randomIntBetween(0, 2);
switch (selector) {
case 0:
qb.distance(randomDouble(), randomFrom(DistanceUnit.values()));
break;
case 1:
qb.distance(distance, randomFrom(DistanceUnit.values()));
break;
case 2:
qb.distance(distance);
break;
}
Point p = RandomShapeGenerator.xRandomPoint(random());
qb.point(new GeoPoint(p.getY(), p.getX()));
if (randomBoolean()) {
qb.setValidationMethod(randomFrom(GeoValidationMethod.values()));
}
if (randomBoolean()) {
qb.geoDistance(randomFrom(GeoDistance.values()));
}
if (randomBoolean()) {
qb.ignoreUnmapped(randomBoolean());
}
return qb;
}
public void testIllegalValues() {
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new GeoDistanceQueryBuilder(""));
assertEquals("fieldName must not be null or empty", e.getMessage());
e = expectThrows(IllegalArgumentException.class, () -> new GeoDistanceQueryBuilder((String) null));
assertEquals("fieldName must not be null or empty", e.getMessage());
GeoDistanceQueryBuilder query = new GeoDistanceQueryBuilder("fieldName");
e = expectThrows(IllegalArgumentException.class, () -> query.distance(""));
assertEquals("distance must not be null or empty", e.getMessage());
e = expectThrows(IllegalArgumentException.class, () -> query.distance(null));
assertEquals("distance must not be null or empty", e.getMessage());
e = expectThrows(IllegalArgumentException.class, () -> query.distance("", DistanceUnit.DEFAULT));
assertEquals("distance must not be null or empty", e.getMessage());
e = expectThrows(IllegalArgumentException.class, () -> query.distance(null, DistanceUnit.DEFAULT));
assertEquals("distance must not be null or empty", e.getMessage());
e = expectThrows(IllegalArgumentException.class, () -> query.distance("1", null));
assertEquals("distance unit must not be null", e.getMessage());
e = expectThrows(IllegalArgumentException.class, () -> query.distance(1, null));
assertEquals("distance unit must not be null", e.getMessage());
e = expectThrows(IllegalArgumentException.class, () -> query.distance(
randomIntBetween(Integer.MIN_VALUE, 0), DistanceUnit.DEFAULT));
assertEquals("distance must be greater than zero", e.getMessage());
e = expectThrows(IllegalArgumentException.class, () -> query.geohash(null));
assertEquals("geohash must not be null or empty", e.getMessage());
e = expectThrows(IllegalArgumentException.class, () -> query.geohash(""));
assertEquals("geohash must not be null or empty", e.getMessage());
e = expectThrows(IllegalArgumentException.class, () -> query.geoDistance(null));
assertEquals("geoDistance must not be null", e.getMessage());
}
/**
* Overridden here to ensure the test is only run if at least one type is
* present in the mappings. Geo queries do not execute if the field is not
* explicitly mapped
*/
@Override
public void testToQuery() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
super.testToQuery();
}
@Override
protected void doAssertLuceneQuery(GeoDistanceQueryBuilder queryBuilder, Query query, SearchContext context) throws IOException {
// TODO: remove the if statement once we always use LatLonPoint
if (query instanceof IndexOrDocValuesQuery) {
Query indexQuery = ((IndexOrDocValuesQuery) query).getIndexQuery();
assertEquals(LatLonPoint.newDistanceQuery(queryBuilder.fieldName(),
queryBuilder.point().lat(),
queryBuilder.point().lon(),
queryBuilder.distance()),
indexQuery);
Query dvQuery = ((IndexOrDocValuesQuery) query).getRandomAccessQuery();
assertEquals(LatLonDocValuesField.newDistanceQuery(queryBuilder.fieldName(),
queryBuilder.point().lat(),
queryBuilder.point().lon(),
queryBuilder.distance()),
dvQuery);
}
}
public void testParsingAndToQuery1() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
String query = "{\n" +
" \"geo_distance\":{\n" +
" \"distance\":\"12mi\",\n" +
" \"" + GEO_POINT_FIELD_NAME + "\":{\n" +
" \"lat\":40,\n" +
" \"lon\":-70\n" +
" }\n" +
" }\n" +
"}\n";
assertGeoDistanceRangeQuery(query, 40, -70, 12, DistanceUnit.DEFAULT);
}
public void testParsingAndToQuery2() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
String query = "{\n" +
" \"geo_distance\":{\n" +
" \"distance\":\"12mi\",\n" +
" \"" + GEO_POINT_FIELD_NAME + "\":[-70, 40]\n" +
" }\n" +
"}\n";
assertGeoDistanceRangeQuery(query, 40, -70, 12, DistanceUnit.DEFAULT);
}
public void testParsingAndToQuery3() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
String query = "{\n" +
" \"geo_distance\":{\n" +
" \"distance\":\"12mi\",\n" +
" \"" + GEO_POINT_FIELD_NAME + "\":\"40, -70\"\n" +
" }\n" +
"}\n";
assertGeoDistanceRangeQuery(query, 40, -70, 12, DistanceUnit.DEFAULT);
}
public void testParsingAndToQuery4() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
String query = "{\n" +
" \"geo_distance\":{\n" +
" \"distance\":\"12mi\",\n" +
" \"" + GEO_POINT_FIELD_NAME + "\":\"drn5x1g8cu2y\"\n" +
" }\n" +
"}\n";
assertGeoDistanceRangeQuery(query, 40, -70, 12, DistanceUnit.DEFAULT);
}
public void testParsingAndToQuery5() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
String query = "{\n" +
" \"geo_distance\":{\n" +
" \"distance\":12,\n" +
" \"unit\":\"mi\",\n" +
" \"" + GEO_POINT_FIELD_NAME + "\":{\n" +
" \"lat\":40,\n" +
" \"lon\":-70\n" +
" }\n" +
" }\n" +
"}\n";
assertGeoDistanceRangeQuery(query, 40, -70, 12, DistanceUnit.DEFAULT);
}
public void testParsingAndToQuery6() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
String query = "{\n" +
" \"geo_distance\":{\n" +
" \"distance\":\"12\",\n" +
" \"unit\":\"mi\",\n" +
" \"" + GEO_POINT_FIELD_NAME + "\":{\n" +
" \"lat\":40,\n" +
" \"lon\":-70\n" +
" }\n" +
" }\n" +
"}\n";
assertGeoDistanceRangeQuery(query, 40, -70, 12, DistanceUnit.DEFAULT);
}
public void testParsingAndToQuery7() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
String query = "{\n" +
" \"geo_distance\":{\n" +
" \"distance\":\"19.312128\",\n" +
" \"" + GEO_POINT_FIELD_NAME + "\":{\n" +
" \"lat\":40,\n" +
" \"lon\":-70\n" +
" }\n" +
" }\n" +
"}\n";
assertGeoDistanceRangeQuery(query, 40, -70, 0.012, DistanceUnit.DEFAULT);
}
public void testParsingAndToQuery8() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
String query = "{\n" +
" \"geo_distance\":{\n" +
" \"distance\":19.312128,\n" +
" \"" + GEO_POINT_FIELD_NAME + "\":{\n" +
" \"lat\":40,\n" +
" \"lon\":-70\n" +
" }\n" +
" }\n" +
"}\n";
assertGeoDistanceRangeQuery(query, 40, -70, 12, DistanceUnit.KILOMETERS);
}
public void testParsingAndToQuery9() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
String query = "{\n" +
" \"geo_distance\":{\n" +
" \"distance\":\"19.312128\",\n" +
" \"unit\":\"km\",\n" +
" \"" + GEO_POINT_FIELD_NAME + "\":{\n" +
" \"lat\":40,\n" +
" \"lon\":-70\n" +
" }\n" +
" }\n" +
"}\n";
assertGeoDistanceRangeQuery(query, 40, -70, 12, DistanceUnit.DEFAULT);
}
public void testParsingAndToQuery10() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
String query = "{\n" +
" \"geo_distance\":{\n" +
" \"distance\":19.312128,\n" +
" \"unit\":\"km\",\n" +
" \"" + GEO_POINT_FIELD_NAME + "\":{\n" +
" \"lat\":40,\n" +
" \"lon\":-70\n" +
" }\n" +
" }\n" +
"}\n";
assertGeoDistanceRangeQuery(query, 40, -70, 12, DistanceUnit.DEFAULT);
}
public void testParsingAndToQuery11() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
String query = "{\n" +
" \"geo_distance\":{\n" +
" \"distance\":\"19.312128km\",\n" +
" \"" + GEO_POINT_FIELD_NAME + "\":{\n" +
" \"lat\":40,\n" +
" \"lon\":-70\n" +
" }\n" +
" }\n" +
"}\n";
assertGeoDistanceRangeQuery(query, 40, -70, 12, DistanceUnit.DEFAULT);
}
public void testParsingAndToQuery12() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
String query = "{\n" +
" \"geo_distance\":{\n" +
" \"distance\":\"12mi\",\n" +
" \"unit\":\"km\",\n" +
" \"" + GEO_POINT_FIELD_NAME + "\":{\n" +
" \"lat\":40,\n" +
" \"lon\":-70\n" +
" }\n" +
" }\n" +
"}\n";
assertGeoDistanceRangeQuery(query, 40, -70, 12, DistanceUnit.DEFAULT);
}
private void assertGeoDistanceRangeQuery(String query, double lat, double lon, double distance, DistanceUnit distanceUnit) throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
Query parsedQuery = parseQuery(query).toQuery(createShardContext());
// TODO: what can we check?
}
public void testFromJson() throws IOException {
String json =
"{\n" +
" \"geo_distance\" : {\n" +
" \"pin.location\" : [ -70.0, 40.0 ],\n" +
" \"distance\" : 12000.0,\n" +
" \"distance_type\" : \"arc\",\n" +
" \"validation_method\" : \"STRICT\",\n" +
" \"ignore_unmapped\" : false,\n" +
" \"boost\" : 1.0\n" +
" }\n" +
"}";
GeoDistanceQueryBuilder parsed = (GeoDistanceQueryBuilder) parseQuery(json);
checkGeneratedJson(json, parsed);
assertEquals(json, -70.0, parsed.point().getLon(), 0.0001);
assertEquals(json, 40.0, parsed.point().getLat(), 0.0001);
assertEquals(json, 12000.0, parsed.distance(), 0.0001);
}
@Override
public void testMustRewrite() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
super.testMustRewrite();
}
public void testIgnoreUnmapped() throws IOException {
final GeoDistanceQueryBuilder queryBuilder = new GeoDistanceQueryBuilder("unmapped").point(0.0, 0.0).distance("20m");
queryBuilder.ignoreUnmapped(true);
QueryShardContext shardContext = createShardContext();
Query query = queryBuilder.toQuery(shardContext);
assertThat(query, notNullValue());
assertThat(query, instanceOf(MatchNoDocsQuery.class));
final GeoDistanceQueryBuilder failingQueryBuilder = new GeoDistanceQueryBuilder("unmapped").point(0.0, 0.0).distance("20m");
failingQueryBuilder.ignoreUnmapped(false);
QueryShardException e = expectThrows(QueryShardException.class, () -> failingQueryBuilder.toQuery(shardContext));
assertThat(e.getMessage(), containsString("failed to find geo_point field [unmapped]"));
}
public void testParseFailsWithMultipleFields() throws IOException {
String json = "{\n" +
" \"geo_distance\" : {\n" +
" \"point1\" : {\n" +
" \"lat\" : 30, \"lon\" : 12\n" +
" },\n" +
" \"point2\" : {\n" +
" \"lat\" : 30, \"lon\" : 12\n" +
" }\n" +
" }\n" +
"}";
ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(json));
assertEquals("[geo_distance] query doesn't support multiple fields, found [point1] and [point2]", e.getMessage());
}
}