diff --git a/docs/reference/query-dsl/geo-bounding-box-query.asciidoc b/docs/reference/query-dsl/geo-bounding-box-query.asciidoc index ca355413b2e5c..afa1175055719 100644 --- a/docs/reference/query-dsl/geo-bounding-box-query.asciidoc +++ b/docs/reference/query-dsl/geo-bounding-box-query.asciidoc @@ -4,8 +4,13 @@ Geo-bounding box ++++ -A query allowing to filter hits based on a point location using a -bounding box. Assuming the following indexed document: +Matches <> and <> values that +intersect a bounding box. + +[discrete] +[[geo-bounding-box-query-ex]] +==== Example +Assume the following the following documents are indexed: [source,console] -------------------------------------------------- @@ -33,11 +38,36 @@ PUT /my_locations/_doc/1 } } } + +PUT /my_geoshapes +{ + "mappings": { + "properties": { + "pin": { + "properties": { + "location": { + "type": "geo_shape" + } + } + } + } + } +} + +PUT /my_geoshapes/_doc/1 +{ + "pin": { + "location": { + "type" : "polygon", + "coordinates" : [[[13.0 ,51.5], [15.0, 51.5], [15.0, 54.0], [13.0, 54.0], [13.0 ,51.5]]] + } + } +} -------------------------------------------------- // TESTSETUP -Then the following simple query can be executed with a -`geo_bounding_box` filter: +Use a `geo_bounding_box` filter to match `geo_point` values that intersect a bounding +box. To define the box, provide geopoint values for two opposite corners. [source,console] -------------------------------------------------- @@ -67,6 +97,66 @@ GET my_locations/_search } -------------------------------------------------- +Use the same filter to match `geo_shape` values that intersect the bounding box: + +[source,console] +-------------------------------------------------- +GET my_geoshapes/_search +{ + "query": { + "bool": { + "must": { + "match_all": {} + }, + "filter": { + "geo_bounding_box": { + "pin.location": { + "top_left": { + "lat": 40.73, + "lon": -74.1 + }, + "bottom_right": { + "lat": 40.01, + "lon": -71.12 + } + } + } + } + } + } +} +-------------------------------------------------- + +To match both `geo_point` and `geo_shape` values, search both indices: + +[source,console] +-------------------------------------------------- +GET my_locations,my_geoshapes/_search +{ + "query": { + "bool": { + "must": { + "match_all": {} + }, + "filter": { + "geo_bounding_box": { + "pin.location": { + "top_left": { + "lat": 40.73, + "lon": -74.1 + }, + "bottom_right": { + "lat": 40.01, + "lon": -71.12 + } + } + } + } + } + } +} +-------------------------------------------------- + [discrete] ==== Query Options @@ -291,13 +381,6 @@ GET my_locations/_search } -------------------------------------------------- - -[discrete] -==== geo_point Type - -The filter *requires* the `geo_point` type to be set on the relevant -field. - [discrete] ==== Multi Location Per Document @@ -366,3 +449,8 @@ the upper bounds (top and right edges) might be selected by the query even if they are located slightly outside the edge. The rounding error should be less than 4.20e-8 degrees on the latitude and less than 8.39e-8 degrees on the longitude, which translates to less than 1cm error even at the equator. + +Geoshapes also have limited precision due to rounding. Geoshape edges along the +bounding box's bottom and left edges may not match a `geo_bounding_box` query. +Geoshape edges slightly outside the box's top and right edges may still match +the query. diff --git a/docs/reference/query-dsl/geo-distance-query.asciidoc b/docs/reference/query-dsl/geo-distance-query.asciidoc index cfb2779659e2b..be76c24402c7a 100644 --- a/docs/reference/query-dsl/geo-distance-query.asciidoc +++ b/docs/reference/query-dsl/geo-distance-query.asciidoc @@ -4,9 +4,14 @@ Geo-distance ++++ -Filters documents that include only hits that exists within a specific -distance from a geo point. Assuming the following mapping and indexed -document: +Matches <> and <> values within +a given distance of a geopoint. + +[discrete] +[[geo-distance-query-ex]] +==== Example + +Assume the following the following documents are indexed: [source,console] -------------------------------------------------- @@ -34,12 +39,37 @@ PUT /my_locations/_doc/1 } } } + +PUT /my_geoshapes +{ + "mappings": { + "properties": { + "pin": { + "properties": { + "location": { + "type": "geo_shape" + } + } + } + } + } +} + +PUT /my_geoshapes/_doc/1 +{ + "pin": { + "location": { + "type" : "polygon", + "coordinates" : [[[13.0 ,51.5], [15.0, 51.5], [15.0, 54.0], [13.0, 54.0], [13.0 ,51.5]]] + } + } +} -------------------------------------------------- // TESTSETUP -Then the following simple query can be executed with a `geo_distance` -filter: +Use a `geo_distance` filter to match `geo_point` values within a specified +distance of another geopoint: [source,console] -------------------------------------------------- @@ -64,6 +94,57 @@ GET /my_locations/_search } -------------------------------------------------- +Use the same filter to match `geo_shape` values within the given distance: + +[source,console] +-------------------------------------------------- +GET my_geoshapes/_search +{ + "query": { + "bool": { + "must": { + "match_all": {} + }, + "filter": { + "geo_distance": { + "distance": "200km", + "pin.location": { + "lat": 40, + "lon": -70 + } + } + } + } + } +} +-------------------------------------------------- + +To match both `geo_point` and `geo_shape` values, search both indices: + +[source,console] +-------------------------------------------------- +GET my_locations,my_geoshapes/_search +{ + "query": { + "bool": { + "must": { + "match_all": {} + }, + "filter": { + "geo_distance": { + "distance": "200km", + "pin.location": { + "lat": 40, + "lon": -70 + } + } + } + } + } +} +-------------------------------------------------- + + [discrete] ==== Accepted Formats @@ -198,12 +279,6 @@ The following are options allowed on the filter: longitude, set to `COERCE` to additionally try and infer correct coordinates (default is `STRICT`). -[discrete] -==== geo_point Type - -The filter *requires* the `geo_point` type to be set on the relevant -field. - [discrete] ==== Multi Location Per Document diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/geo/AbstractGeoBoundingBoxQueryIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/AbstractGeoBoundingBoxQueryIT.java new file mode 100644 index 0000000000000..5cfe2bfe6bc95 --- /dev/null +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/AbstractGeoBoundingBoxQueryIT.java @@ -0,0 +1,276 @@ +/* + * 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.geo; + +import org.elasticsearch.Version; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.DistanceUnit; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.GeoValidationMethod; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.test.VersionUtils; + +import java.io.IOException; + +import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.index.query.QueryBuilders.boolQuery; +import static org.elasticsearch.index.query.QueryBuilders.geoBoundingBoxQuery; +import static org.elasticsearch.index.query.QueryBuilders.geoDistanceQuery; +import static org.elasticsearch.index.query.QueryBuilders.termQuery; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.equalTo; + +abstract class AbstractGeoBoundingBoxQueryIT extends ESIntegTestCase { + + @Override + protected boolean forbidPrivateIndexSettings() { + return false; + } + + public abstract XContentBuilder getMapping() throws IOException; + + public void testSimpleBoundingBoxTest() throws Exception { + Version version = VersionUtils.randomIndexCompatibleVersion(random()); + Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); + XContentBuilder xContentBuilder = getMapping(); + assertAcked(prepareCreate("test").setSettings(settings).addMapping("type1", xContentBuilder)); + ensureGreen(); + + client().prepareIndex("test", "type1", "1").setSource(jsonBuilder().startObject() + .field("name", "New York") + .field("location", "POINT(-74.0059731 40.7143528)") + .endObject()).get(); + + // to NY: 5.286 km + client().prepareIndex("test", "type1", "2").setSource(jsonBuilder().startObject() + .field("name", "Times Square") + .field("location", "POINT(-73.9844722 40.759011)") + .endObject()).get(); + + // to NY: 0.4621 km + client().prepareIndex("test", "type1", "3").setSource(jsonBuilder().startObject() + .field("name", "Tribeca") + .field("location", "POINT(-74.007819 40.718266)") + .endObject()).get(); + + // to NY: 1.055 km + client().prepareIndex("test", "type1", "4").setSource(jsonBuilder().startObject() + .field("name", "Wall Street") + .field("location", "POINT(-74.0088305 40.7051157)") + .endObject()).get(); + + // to NY: 1.258 km + client().prepareIndex("test", "type1", "5").setSource(jsonBuilder().startObject() + .field("name", "Soho") + .field("location", "POINT(-74 40.7247222)") + .endObject()).get(); + + // to NY: 2.029 km + client().prepareIndex("test", "type1", "6").setSource(jsonBuilder().startObject() + .field("name", "Greenwich Village") + .field("location", "POINT(-73.9962255 40.731033)") + .endObject()).get(); + + // to NY: 8.572 km + client().prepareIndex("test", "type1", "7").setSource(jsonBuilder().startObject() + .field("name", "Brooklyn") + .field("location", "POINT(-73.95 40.65)") + .endObject()).get(); + + client().admin().indices().prepareRefresh().get(); + + SearchResponse searchResponse = client().prepareSearch() // from NY + .setQuery(geoBoundingBoxQuery("location").setCorners(40.73, -74.1, 40.717, -73.99)) + .get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(2L)); + assertThat(searchResponse.getHits().getHits().length, equalTo(2)); + for (SearchHit hit : searchResponse.getHits()) { + assertThat(hit.getId(), anyOf(equalTo("1"), equalTo("3"), equalTo("5"))); + } + + searchResponse = client().prepareSearch() // from NY + .setQuery(geoBoundingBoxQuery("location").setCorners(40.73, -74.1, 40.717, -73.99).type("indexed")) + .get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(2L)); + assertThat(searchResponse.getHits().getHits().length, equalTo(2)); + for (SearchHit hit : searchResponse.getHits()) { + assertThat(hit.getId(), anyOf(equalTo("1"), equalTo("3"), equalTo("5"))); + } + // Distance query + searchResponse = client().prepareSearch() // from NY + .setQuery(geoDistanceQuery("location").point(40.5, -73.9).distance(25, DistanceUnit.KILOMETERS)) + .get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(2L)); + assertThat(searchResponse.getHits().getHits().length, equalTo(2)); + for (SearchHit hit : searchResponse.getHits()) { + assertThat(hit.getId(), anyOf(equalTo("7"), equalTo("4"))); + } + } + + public void testLimit2BoundingBox() throws Exception { + Version version = VersionUtils.randomIndexCompatibleVersion(random()); + Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); + XContentBuilder xContentBuilder = getMapping(); + assertAcked(prepareCreate("test").setSettings(settings).addMapping("type1", xContentBuilder)); + ensureGreen(); + + client().prepareIndex("test", "type1", "1").setSource(jsonBuilder().startObject() + .field("userid", 880) + .field("title", "Place in Stockholm") + .field("location", "POINT(59.328355000000002 18.036842)") + .endObject()) + .setRefreshPolicy(IMMEDIATE) + .get(); + + client().prepareIndex("test", "type1", "2").setSource(jsonBuilder().startObject() + .field("userid", 534) + .field("title", "Place in Montreal") + .field("location", "POINT(-73.570986000000005 45.509526999999999)") + .endObject()) + .setRefreshPolicy(IMMEDIATE) + .get(); + + SearchResponse searchResponse = client().prepareSearch() + .setQuery( + boolQuery().must(termQuery("userid", 880)).filter( + geoBoundingBoxQuery("location").setCorners(74.579421999999994, 143.5, -66.668903999999998, 113.96875)) + ).get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + searchResponse = client().prepareSearch() + .setQuery( + boolQuery().must(termQuery("userid", 880)).filter( + geoBoundingBoxQuery("location").setCorners(74.579421999999994, 143.5, -66.668903999999998, 113.96875) + .type("indexed")) + ).get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + + searchResponse = client().prepareSearch() + .setQuery( + boolQuery().must(termQuery("userid", 534)).filter( + geoBoundingBoxQuery("location").setCorners(74.579421999999994, 143.5, -66.668903999999998, 113.96875)) + ).get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + searchResponse = client().prepareSearch() + .setQuery( + boolQuery().must(termQuery("userid", 534)).filter( + geoBoundingBoxQuery("location").setCorners(74.579421999999994, 143.5, -66.668903999999998, 113.96875) + .type("indexed")) + ).get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + + // Distance query + searchResponse = client().prepareSearch() + .setQuery( + boolQuery().must(termQuery("userid", 880)).filter( + geoDistanceQuery("location").point(20, 60.0).distance(500, DistanceUnit.MILES)) + ).get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + + searchResponse = client().prepareSearch() + .setQuery( + boolQuery().must(termQuery("userid", 534)).filter( + geoDistanceQuery("location").point(45.0, -73.0).distance(500, DistanceUnit.MILES)) + ).get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + } + + public void testCompleteLonRange() throws Exception { + Version version = VersionUtils.randomIndexCompatibleVersion(random()); + Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); + XContentBuilder xContentBuilder = getMapping(); + assertAcked(prepareCreate("test").setSettings(settings).addMapping("type1", xContentBuilder)); + ensureGreen(); + + client().prepareIndex("test", "type1", "1").setSource(jsonBuilder().startObject() + .field("userid", 880) + .field("title", "Place in Stockholm") + .field("location", "POINT(18.036842 59.328355000000002)") + .endObject()) + .setRefreshPolicy(IMMEDIATE) + .get(); + + client().prepareIndex("test", "type1", "2").setSource(jsonBuilder().startObject() + .field("userid", 534) + .field("title", "Place in Montreal") + .field("location", "POINT(-73.570986000000005 45.509526999999999)") + .endObject()) + .setRefreshPolicy(IMMEDIATE) + .get(); + + SearchResponse searchResponse = client().prepareSearch() + .setQuery( + geoBoundingBoxQuery("location").setValidationMethod(GeoValidationMethod.COERCE).setCorners(50, -180, -50, 180) + ).get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + searchResponse = client().prepareSearch() + .setQuery( + geoBoundingBoxQuery("location").setValidationMethod(GeoValidationMethod.COERCE).setCorners(50, -180, -50, 180) + .type("indexed") + ).get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + searchResponse = client().prepareSearch() + .setQuery( + geoBoundingBoxQuery("location").setValidationMethod(GeoValidationMethod.COERCE).setCorners(90, -180, -90, 180) + ).get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(2L)); + searchResponse = client().prepareSearch() + .setQuery( + geoBoundingBoxQuery("location").setValidationMethod(GeoValidationMethod.COERCE).setCorners(90, -180, -90, 180) + .type("indexed") + ).get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(2L)); + + searchResponse = client().prepareSearch() + .setQuery( + geoBoundingBoxQuery("location").setValidationMethod(GeoValidationMethod.COERCE).setCorners(50, 0, -50, 360) + ).get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + searchResponse = client().prepareSearch() + .setQuery( + geoBoundingBoxQuery("location").setValidationMethod(GeoValidationMethod.COERCE).setCorners(50, 0, -50, 360) + .type("indexed") + ).get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + searchResponse = client().prepareSearch() + .setQuery( + geoBoundingBoxQuery("location").setValidationMethod(GeoValidationMethod.COERCE).setCorners(90, 0, -90, 360) + ).get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(2L)); + searchResponse = client().prepareSearch() + .setQuery( + geoBoundingBoxQuery("location").setValidationMethod(GeoValidationMethod.COERCE).setCorners(90, 0, -90, 360) + .type("indexed") + ).get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(2L)); + + // Distance query + searchResponse = client().prepareSearch() + .setQuery( + geoDistanceQuery("location").point(60.0, -20.0).distance(1800, DistanceUnit.MILES) + ).get(); + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); + } +} + diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoBoundingBoxQueryGeoPointIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoBoundingBoxQueryGeoPointIT.java new file mode 100644 index 0000000000000..3dd90b6501551 --- /dev/null +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoBoundingBoxQueryGeoPointIT.java @@ -0,0 +1,37 @@ +/* + * 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.geo; + +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; + +import java.io.IOException; + +public class GeoBoundingBoxQueryGeoPointIT extends AbstractGeoBoundingBoxQueryIT { + + @Override + public XContentBuilder getMapping() throws IOException { + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type1") + .startObject("properties").startObject("location").field("type", "geo_point"); + xContentBuilder.endObject().endObject().endObject().endObject(); + return xContentBuilder; + } +} + diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoBoundingBoxQueryGeoShapeIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoBoundingBoxQueryGeoShapeIT.java new file mode 100644 index 0000000000000..49716a76f17e6 --- /dev/null +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoBoundingBoxQueryGeoShapeIT.java @@ -0,0 +1,40 @@ +/* + * 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.geo; + +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; + +import java.io.IOException; + +public class GeoBoundingBoxQueryGeoShapeIT extends AbstractGeoBoundingBoxQueryIT { + + @Override + public XContentBuilder getMapping() throws IOException { + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type1") + .startObject("properties").startObject("location").field("type", "geo_shape"); + if (randomBoolean()) { + xContentBuilder.field("strategy", "recursive"); + } + xContentBuilder.endObject().endObject().endObject().endObject(); + return xContentBuilder; + } +} + diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoBoundingBoxQueryIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoBoundingBoxQueryIT.java deleted file mode 100644 index 6b5f4b2b22bcd..0000000000000 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoBoundingBoxQueryIT.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * 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.geo; - -import org.elasticsearch.Version; -import org.elasticsearch.action.search.SearchResponse; -import org.elasticsearch.cluster.metadata.IndexMetadata; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.index.query.GeoValidationMethod; -import org.elasticsearch.search.SearchHit; -import org.elasticsearch.test.ESIntegTestCase; -import org.elasticsearch.test.VersionUtils; - -import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; -import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; -import static org.elasticsearch.index.query.QueryBuilders.boolQuery; -import static org.elasticsearch.index.query.QueryBuilders.geoBoundingBoxQuery; -import static org.elasticsearch.index.query.QueryBuilders.termQuery; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; -import static org.hamcrest.Matchers.anyOf; -import static org.hamcrest.Matchers.equalTo; - -public class GeoBoundingBoxQueryIT extends ESIntegTestCase { - - @Override - protected boolean forbidPrivateIndexSettings() { - return false; - } - - public void testSimpleBoundingBoxTest() throws Exception { - Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, - Version.CURRENT); - Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); - XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type1") - .startObject("properties").startObject("location").field("type", "geo_point"); - xContentBuilder.endObject().endObject().endObject().endObject(); - assertAcked(prepareCreate("test").setSettings(settings).addMapping("type1", xContentBuilder)); - ensureGreen(); - - client().prepareIndex("test", "type1", "1").setSource(jsonBuilder().startObject() - .field("name", "New York") - .startObject("location").field("lat", 40.7143528).field("lon", -74.0059731).endObject() - .endObject()).get(); - - // to NY: 5.286 km - client().prepareIndex("test", "type1", "2").setSource(jsonBuilder().startObject() - .field("name", "Times Square") - .startObject("location").field("lat", 40.759011).field("lon", -73.9844722).endObject() - .endObject()).get(); - - // to NY: 0.4621 km - client().prepareIndex("test", "type1", "3").setSource(jsonBuilder().startObject() - .field("name", "Tribeca") - .startObject("location").field("lat", 40.718266).field("lon", -74.007819).endObject() - .endObject()).get(); - - // to NY: 1.055 km - client().prepareIndex("test", "type1", "4").setSource(jsonBuilder().startObject() - .field("name", "Wall Street") - .startObject("location").field("lat", 40.7051157).field("lon", -74.0088305).endObject() - .endObject()).get(); - - // to NY: 1.258 km - client().prepareIndex("test", "type1", "5").setSource(jsonBuilder().startObject() - .field("name", "Soho") - .startObject("location").field("lat", 40.7247222).field("lon", -74).endObject() - .endObject()).get(); - - // to NY: 2.029 km - client().prepareIndex("test", "type1", "6").setSource(jsonBuilder().startObject() - .field("name", "Greenwich Village") - .startObject("location").field("lat", 40.731033).field("lon", -73.9962255).endObject() - .endObject()).get(); - - // to NY: 8.572 km - client().prepareIndex("test", "type1", "7").setSource(jsonBuilder().startObject() - .field("name", "Brooklyn") - .startObject("location").field("lat", 40.65).field("lon", -73.95).endObject() - .endObject()).get(); - - client().admin().indices().prepareRefresh().get(); - - SearchResponse searchResponse = client().prepareSearch() // from NY - .setQuery(geoBoundingBoxQuery("location").setCorners(40.73, -74.1, 40.717, -73.99)) - .get(); - assertThat(searchResponse.getHits().getTotalHits().value, equalTo(2L)); - assertThat(searchResponse.getHits().getHits().length, equalTo(2)); - for (SearchHit hit : searchResponse.getHits()) { - assertThat(hit.getId(), anyOf(equalTo("1"), equalTo("3"), equalTo("5"))); - } - - searchResponse = client().prepareSearch() // from NY - .setQuery(geoBoundingBoxQuery("location").setCorners(40.73, -74.1, 40.717, -73.99).type("indexed")) - .get(); - assertThat(searchResponse.getHits().getTotalHits().value, equalTo(2L)); - assertThat(searchResponse.getHits().getHits().length, equalTo(2)); - for (SearchHit hit : searchResponse.getHits()) { - assertThat(hit.getId(), anyOf(equalTo("1"), equalTo("3"), equalTo("5"))); - } - } - - public void testLimit2BoundingBox() throws Exception { - Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, - Version.CURRENT); - Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); - XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type1") - .startObject("properties").startObject("location").field("type", "geo_point"); - xContentBuilder.endObject().endObject().endObject().endObject(); - assertAcked(prepareCreate("test").setSettings(settings).addMapping("type1", xContentBuilder)); - ensureGreen(); - - client().prepareIndex("test", "type1", "1").setSource(jsonBuilder().startObject() - .field("userid", 880) - .field("title", "Place in Stockholm") - .startObject("location").field("lat", 59.328355000000002).field("lon", 18.036842).endObject() - .endObject()) - .setRefreshPolicy(IMMEDIATE) - .get(); - - client().prepareIndex("test", "type1", "2").setSource(jsonBuilder().startObject() - .field("userid", 534) - .field("title", "Place in Montreal") - .startObject("location").field("lat", 45.509526999999999).field("lon", -73.570986000000005).endObject() - .endObject()) - .setRefreshPolicy(IMMEDIATE) - .get(); - - SearchResponse searchResponse = client().prepareSearch() - .setQuery( - boolQuery().must(termQuery("userid", 880)).filter( - geoBoundingBoxQuery("location").setCorners(74.579421999999994, 143.5, -66.668903999999998, 113.96875)) - ).get(); - assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); - searchResponse = client().prepareSearch() - .setQuery( - boolQuery().must(termQuery("userid", 880)).filter( - geoBoundingBoxQuery("location").setCorners(74.579421999999994, 143.5, -66.668903999999998, 113.96875) - .type("indexed")) - ).get(); - assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); - - searchResponse = client().prepareSearch() - .setQuery( - boolQuery().must(termQuery("userid", 534)).filter( - geoBoundingBoxQuery("location").setCorners(74.579421999999994, 143.5, -66.668903999999998, 113.96875)) - ).get(); - assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); - searchResponse = client().prepareSearch() - .setQuery( - boolQuery().must(termQuery("userid", 534)).filter( - geoBoundingBoxQuery("location").setCorners(74.579421999999994, 143.5, -66.668903999999998, 113.96875) - .type("indexed")) - ).get(); - assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); - } - - public void testCompleteLonRange() throws Exception { - Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, - Version.CURRENT); - Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); - XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type1") - .startObject("properties").startObject("location").field("type", "geo_point"); - xContentBuilder.endObject().endObject().endObject().endObject(); - assertAcked(prepareCreate("test").setSettings(settings).addMapping("type1", xContentBuilder)); - ensureGreen(); - - client().prepareIndex("test", "type1", "1").setSource(jsonBuilder().startObject() - .field("userid", 880) - .field("title", "Place in Stockholm") - .startObject("location").field("lat", 59.328355000000002).field("lon", 18.036842).endObject() - .endObject()) - .setRefreshPolicy(IMMEDIATE) - .get(); - - client().prepareIndex("test", "type1", "2").setSource(jsonBuilder().startObject() - .field("userid", 534) - .field("title", "Place in Montreal") - .startObject("location").field("lat", 45.509526999999999).field("lon", -73.570986000000005).endObject() - .endObject()) - .setRefreshPolicy(IMMEDIATE) - .get(); - - SearchResponse searchResponse = client().prepareSearch() - .setQuery( - geoBoundingBoxQuery("location").setValidationMethod(GeoValidationMethod.COERCE).setCorners(50, -180, -50, 180) - ).get(); - assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); - searchResponse = client().prepareSearch() - .setQuery( - geoBoundingBoxQuery("location").setValidationMethod(GeoValidationMethod.COERCE).setCorners(50, -180, -50, 180) - .type("indexed") - ).get(); - assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); - searchResponse = client().prepareSearch() - .setQuery( - geoBoundingBoxQuery("location").setValidationMethod(GeoValidationMethod.COERCE).setCorners(90, -180, -90, 180) - ).get(); - assertThat(searchResponse.getHits().getTotalHits().value, equalTo(2L)); - searchResponse = client().prepareSearch() - .setQuery( - geoBoundingBoxQuery("location").setValidationMethod(GeoValidationMethod.COERCE).setCorners(90, -180, -90, 180) - .type("indexed") - ).get(); - assertThat(searchResponse.getHits().getTotalHits().value, equalTo(2L)); - - searchResponse = client().prepareSearch() - .setQuery( - geoBoundingBoxQuery("location").setValidationMethod(GeoValidationMethod.COERCE).setCorners(50, 0, -50, 360) - ).get(); - assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); - searchResponse = client().prepareSearch() - .setQuery( - geoBoundingBoxQuery("location").setValidationMethod(GeoValidationMethod.COERCE).setCorners(50, 0, -50, 360) - .type("indexed") - ).get(); - assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L)); - searchResponse = client().prepareSearch() - .setQuery( - geoBoundingBoxQuery("location").setValidationMethod(GeoValidationMethod.COERCE).setCorners(90, 0, -90, 360) - ).get(); - assertThat(searchResponse.getHits().getTotalHits().value, equalTo(2L)); - searchResponse = client().prepareSearch() - .setQuery( - geoBoundingBoxQuery("location").setValidationMethod(GeoValidationMethod.COERCE).setCorners(90, 0, -90, 360) - .type("indexed") - ).get(); - assertThat(searchResponse.getHits().getTotalHits().value, equalTo(2L)); - } -} - diff --git a/server/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxQueryBuilder.java index df2f6b6e4267b..76a2218da3966 100644 --- a/server/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxQueryBuilder.java @@ -19,9 +19,6 @@ 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.ElasticsearchParseException; @@ -31,13 +28,15 @@ import org.elasticsearch.common.geo.GeoBoundingBox; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.GeoUtils; +import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.common.geo.SpatialStrategy; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.geometry.Rectangle; import org.elasticsearch.geometry.utils.Geohash; -import org.elasticsearch.index.mapper.GeoPointFieldMapper.GeoPointFieldType; +import org.elasticsearch.index.mapper.GeoShapeQueryable; import org.elasticsearch.index.mapper.MappedFieldType; import java.io.IOException; @@ -309,11 +308,12 @@ public Query doToQuery(QueryShardContext context) { if (ignoreUnmapped) { return new MatchNoDocsQuery(); } else { - throw new QueryShardException(context, "failed to find geo_point field [" + fieldName + "]"); + throw new QueryShardException(context, "failed to find geo field [" + fieldName + "]"); } } - if (!(fieldType instanceof GeoPointFieldType)) { - throw new QueryShardException(context, "field [" + fieldName + "] is not a geo_point field"); + if (!(fieldType instanceof GeoShapeQueryable)) { + throw new QueryShardException(context, + "Field [" + fieldName + "] is of unsupported type [" + fieldType.typeName() + "] for [" + NAME + "] query"); } QueryValidationException exception = checkLatLon(); @@ -338,15 +338,10 @@ public Query doToQuery(QueryShardContext context) { } } - Query query = LatLonPoint.newBoxQuery(fieldType.name(), luceneBottomRight.getLat(), luceneTopLeft.getLat(), - luceneTopLeft.getLon(), luceneBottomRight.getLon()); - if (fieldType.hasDocValues()) { - Query dvQuery = LatLonDocValuesField.newSlowBoxQuery(fieldType.name(), - luceneBottomRight.getLat(), luceneTopLeft.getLat(), - luceneTopLeft.getLon(), luceneBottomRight.getLon()); - query = new IndexOrDocValuesQuery(query, dvQuery); - } - return query; + final GeoShapeQueryable geoShapeQueryable = (GeoShapeQueryable) fieldType; + final Rectangle rectangle = + new Rectangle(luceneTopLeft.getLon(), luceneBottomRight.getLon(), luceneTopLeft.getLat(), luceneBottomRight.getLat()); + return geoShapeQueryable.geoShapeQuery(rectangle, fieldType.name(), SpatialStrategy.RECURSIVE, ShapeRelation.INTERSECTS, context); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/query/GeoDistanceQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/GeoDistanceQueryBuilder.java index f4c0da2c5256a..527e927299d51 100644 --- a/server/src/main/java/org/elasticsearch/index/query/GeoDistanceQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/GeoDistanceQueryBuilder.java @@ -19,9 +19,6 @@ 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.ParseField; @@ -30,12 +27,15 @@ import org.elasticsearch.common.geo.GeoDistance; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.GeoUtils; +import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.common.geo.SpatialStrategy; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.unit.DistanceUnit; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.index.mapper.GeoPointFieldMapper.GeoPointFieldType; +import org.elasticsearch.geometry.Circle; +import org.elasticsearch.index.mapper.GeoShapeQueryable; import org.elasticsearch.index.mapper.MappedFieldType; import java.io.IOException; @@ -232,12 +232,13 @@ protected Query doToQuery(QueryShardContext shardContext) throws IOException { if (ignoreUnmapped) { return new MatchNoDocsQuery(); } else { - throw new QueryShardException(shardContext, "failed to find geo_point field [" + fieldName + "]"); + throw new QueryShardException(shardContext, "failed to find geo field [" + fieldName + "]"); } } - if (!(fieldType instanceof GeoPointFieldType)) { - throw new QueryShardException(shardContext, "field [" + fieldName + "] is not a geo_point field"); + if (!(fieldType instanceof GeoShapeQueryable)) { + throw new QueryShardException(shardContext, + "Field [" + fieldName + "] is of unsupported type [" + fieldType.typeName() + "] for [" + NAME + "] query"); } QueryValidationException exception = checkLatLon(); @@ -249,12 +250,11 @@ protected Query doToQuery(QueryShardContext shardContext) throws IOException { GeoUtils.normalizePoint(center, true, true); } - Query query = LatLonPoint.newDistanceQuery(fieldType.name(), center.lat(), center.lon(), this.distance); - if (fieldType.hasDocValues()) { - Query dvQuery = LatLonDocValuesField.newSlowDistanceQuery(fieldType.name(), center.lat(), center.lon(), this.distance); - query = new IndexOrDocValuesQuery(query, dvQuery); - } - return query; + final GeoShapeQueryable geoShapeQueryable = (GeoShapeQueryable) fieldType; + final Circle circle = + new Circle(center.lon(), center.lat(), this.distance); + return geoShapeQueryable.geoShapeQuery(circle, fieldType.name(), + SpatialStrategy.RECURSIVE, ShapeRelation.INTERSECTS, shardContext); } @Override diff --git a/server/src/test/java/org/elasticsearch/index/query/GeoBoundingBoxQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/GeoBoundingBoxQueryBuilderTests.java index c4fa6a3cc2371..9894b7d002361 100644 --- a/server/src/test/java/org/elasticsearch/index/query/GeoBoundingBoxQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/GeoBoundingBoxQueryBuilderTests.java @@ -25,8 +25,12 @@ import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.Version; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.GeoUtils; +import org.elasticsearch.index.mapper.GeoPointFieldMapper; +import org.elasticsearch.index.mapper.GeoShapeFieldMapper; +import org.elasticsearch.index.mapper.LegacyGeoShapeFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.test.AbstractQueryTestCase; import org.elasticsearch.test.geo.RandomShapeGenerator; @@ -45,7 +49,7 @@ public class GeoBoundingBoxQueryBuilderTests extends AbstractQueryTestCase qb.toQuery(context)); - assertEquals("failed to find geo_point field [" + qb.fieldName() + "]", e.getMessage()); + assertEquals("failed to find geo field [" + qb.fieldName() + "]", e.getMessage()); } public void testBrokenCoordinateCannotBeSet() { @@ -206,10 +210,11 @@ public void testStrictnessDefault() { @Override protected void doAssertLuceneQuery(GeoBoundingBoxQueryBuilder queryBuilder, Query query, QueryShardContext context) throws IOException { - MappedFieldType fieldType = context.getFieldType(queryBuilder.fieldName()); + final MappedFieldType fieldType = context.getFieldType(queryBuilder.fieldName()); if (fieldType == null) { assertTrue("Found no indexed geo query.", query instanceof MatchNoDocsQuery); - } else if (query instanceof IndexOrDocValuesQuery) { // TODO: remove the if statement once we always use LatLonPoint + } else if (fieldType instanceof GeoPointFieldMapper.GeoPointFieldType) { + assertEquals(IndexOrDocValuesQuery.class, query.getClass()); Query indexQuery = ((IndexOrDocValuesQuery) query).getIndexQuery(); String expectedFieldName = expectedFieldName(queryBuilder.fieldName()); assertEquals(LatLonPoint.newBoxQuery(expectedFieldName, @@ -223,6 +228,12 @@ protected void doAssertLuceneQuery(GeoBoundingBoxQueryBuilder queryBuilder, Quer queryBuilder.topLeft().lat(), queryBuilder.topLeft().lon(), queryBuilder.bottomRight().lon()), dvQuery); + } else { + if (context.indexVersionCreated().before(Version.V_6_6_0)) { + assertEquals(LegacyGeoShapeFieldMapper.GeoShapeFieldType.class, fieldType.getClass()); + } else { + assertEquals(GeoShapeFieldMapper.GeoShapeFieldType.class, fieldType.getClass()); + } } } @@ -535,6 +546,6 @@ public void testIgnoreUnmapped() throws IOException { final GeoBoundingBoxQueryBuilder failingQueryBuilder = new GeoBoundingBoxQueryBuilder("unmapped").setCorners(1.0, 0.0, 0.0, 1.0); failingQueryBuilder.ignoreUnmapped(false); QueryShardException e = expectThrows(QueryShardException.class, () -> failingQueryBuilder.toQuery(shardContext)); - assertThat(e.getMessage(), containsString("failed to find geo_point field [unmapped]")); + assertThat(e.getMessage(), containsString("failed to find geo field [unmapped]")); } } diff --git a/server/src/test/java/org/elasticsearch/index/query/GeoDistanceQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/GeoDistanceQueryBuilderTests.java index dfb8211463ea5..9ef07fd078a3c 100644 --- a/server/src/test/java/org/elasticsearch/index/query/GeoDistanceQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/GeoDistanceQueryBuilderTests.java @@ -24,10 +24,15 @@ import org.apache.lucene.search.IndexOrDocValuesQuery; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; +import org.elasticsearch.Version; 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.index.mapper.GeoPointFieldMapper; +import org.elasticsearch.index.mapper.GeoShapeFieldMapper; +import org.elasticsearch.index.mapper.LegacyGeoShapeFieldMapper; +import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.test.AbstractQueryTestCase; import org.elasticsearch.test.geo.RandomShapeGenerator; import org.locationtech.spatial4j.shape.Point; @@ -42,7 +47,7 @@ public class GeoDistanceQueryBuilderTests extends AbstractQueryTestCase failingQueryBuilder.toQuery(shardContext)); - assertThat(e.getMessage(), containsString("failed to find geo_point field [unmapped]")); + assertThat(e.getMessage(), containsString("failed to find geo field [unmapped]")); } public void testParseFailsWithMultipleFields() throws IOException { diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/100_geo_point.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/100_geo_point.yml index 6803b79c79ea7..328470c806de4 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/100_geo_point.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/100_geo_point.yml @@ -86,6 +86,23 @@ setup: --- "geo bounding box query": + - do: + search: + index: locations + body: + query: + geo_bounding_box: + location_from_source: + top_left: + lat: 10 + lon: -10 + bottom_right: + lat: -10 + lon: 10 + - match: {hits.total.value: 1} + +--- +"geo shape query": - do: search: index: locations @@ -98,6 +115,20 @@ setup: coordinates: [ [ -10, 10 ], [ 10, -10 ] ] - match: {hits.total.value: 1} +--- +"geo distance query": + - do: + search: + index: locations + body: + query: + geo_distance: + distance: "2000km" + location_from_source: + lat: 0 + lon: 0 + - match: {hits.total.value: 1} + --- "bounds agg": - do: