Support for geo_bounding_box queries on geo_shape fields (#2506)
Adds support for using geo_bounding_box queries on geo_shape field types by lifting the geo_point restriction in the QueryBuilder. Bounding Box query integration tests are abstracted to test both geo_point and geo_shape types. Signed-off-by: Nicholas Walter Knize <nknize@apache.org>
This commit is contained in:
parent
d2bdcdec33
commit
bd2d9350b7
|
@ -43,6 +43,8 @@ import org.opensearch.search.SearchHit;
|
|||
import org.opensearch.test.OpenSearchIntegTestCase;
|
||||
import org.opensearch.test.VersionUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE;
|
||||
import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||
import static org.opensearch.index.query.QueryBuilders.boolQuery;
|
||||
|
@ -52,7 +54,15 @@ import static org.opensearch.test.hamcrest.OpenSearchAssertions.assertAcked;
|
|||
import static org.hamcrest.Matchers.anyOf;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
public class GeoBoundingBoxQueryIT extends OpenSearchIntegTestCase {
|
||||
abstract class AbstractGeoBoundingBoxQueryIT extends OpenSearchIntegTestCase {
|
||||
|
||||
public abstract XContentBuilder addGeoMapping(XContentBuilder parentMapping) throws IOException;
|
||||
|
||||
public XContentBuilder getMapping() throws IOException {
|
||||
XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("properties");
|
||||
mapping = addGeoMapping(mapping);
|
||||
return mapping.endObject().endObject();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean forbidPrivateIndexSettings() {
|
||||
|
@ -62,110 +72,55 @@ public class GeoBoundingBoxQueryIT extends OpenSearchIntegTestCase {
|
|||
public void testSimpleBoundingBoxTest() throws Exception {
|
||||
Version version = VersionUtils.randomIndexCompatibleVersion(random());
|
||||
Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build();
|
||||
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder()
|
||||
.startObject()
|
||||
.startObject("properties")
|
||||
.startObject("location")
|
||||
.field("type", "geo_point");
|
||||
xContentBuilder.endObject().endObject().endObject();
|
||||
XContentBuilder xContentBuilder = getMapping();
|
||||
assertAcked(prepareCreate("test").setSettings(settings).setMapping(xContentBuilder));
|
||||
ensureGreen();
|
||||
|
||||
client().prepareIndex("test")
|
||||
.setId("1")
|
||||
.setSource(
|
||||
jsonBuilder().startObject()
|
||||
.field("name", "New York")
|
||||
.startObject("location")
|
||||
.field("lat", 40.7143528)
|
||||
.field("lon", -74.0059731)
|
||||
.endObject()
|
||||
.endObject()
|
||||
)
|
||||
.setSource(jsonBuilder().startObject().field("name", "New York").field("location", "POINT(-74.0059731 40.7143528)").endObject())
|
||||
.get();
|
||||
|
||||
// to NY: 5.286 km
|
||||
client().prepareIndex("test")
|
||||
.setId("2")
|
||||
.setSource(
|
||||
jsonBuilder().startObject()
|
||||
.field("name", "Times Square")
|
||||
.startObject("location")
|
||||
.field("lat", 40.759011)
|
||||
.field("lon", -73.9844722)
|
||||
.endObject()
|
||||
.endObject()
|
||||
jsonBuilder().startObject().field("name", "Times Square").field("location", "POINT(-73.9844722 40.759011)").endObject()
|
||||
)
|
||||
.get();
|
||||
|
||||
// to NY: 0.4621 km
|
||||
client().prepareIndex("test")
|
||||
.setId("3")
|
||||
.setSource(
|
||||
jsonBuilder().startObject()
|
||||
.field("name", "Tribeca")
|
||||
.startObject("location")
|
||||
.field("lat", 40.718266)
|
||||
.field("lon", -74.007819)
|
||||
.endObject()
|
||||
.endObject()
|
||||
)
|
||||
.setSource(jsonBuilder().startObject().field("name", "Tribeca").field("location", "POINT(-74.007819 40.718266)").endObject())
|
||||
.get();
|
||||
|
||||
// to NY: 1.055 km
|
||||
client().prepareIndex("test")
|
||||
.setId("4")
|
||||
.setSource(
|
||||
jsonBuilder().startObject()
|
||||
.field("name", "Wall Street")
|
||||
.startObject("location")
|
||||
.field("lat", 40.7051157)
|
||||
.field("lon", -74.0088305)
|
||||
.endObject()
|
||||
.endObject()
|
||||
jsonBuilder().startObject().field("name", "Wall Street").field("location", "POINT(-74.0088305 40.7051157)").endObject()
|
||||
)
|
||||
.get();
|
||||
|
||||
// to NY: 1.258 km
|
||||
client().prepareIndex("test")
|
||||
.setId("5")
|
||||
.setSource(
|
||||
jsonBuilder().startObject()
|
||||
.field("name", "Soho")
|
||||
.startObject("location")
|
||||
.field("lat", 40.7247222)
|
||||
.field("lon", -74)
|
||||
.endObject()
|
||||
.endObject()
|
||||
)
|
||||
.setSource(jsonBuilder().startObject().field("name", "Soho").field("location", "POINT(-74 40.7247222)").endObject())
|
||||
.get();
|
||||
|
||||
// to NY: 2.029 km
|
||||
client().prepareIndex("test")
|
||||
.setId("6")
|
||||
.setSource(
|
||||
jsonBuilder().startObject()
|
||||
.field("name", "Greenwich Village")
|
||||
.startObject("location")
|
||||
.field("lat", 40.731033)
|
||||
.field("lon", -73.9962255)
|
||||
.endObject()
|
||||
.endObject()
|
||||
jsonBuilder().startObject().field("name", "Greenwich Village").field("location", "POINT(-73.9962255 40.731033)").endObject()
|
||||
)
|
||||
.get();
|
||||
|
||||
// to NY: 8.572 km
|
||||
client().prepareIndex("test")
|
||||
.setId("7")
|
||||
.setSource(
|
||||
jsonBuilder().startObject()
|
||||
.field("name", "Brooklyn")
|
||||
.startObject("location")
|
||||
.field("lat", 40.65)
|
||||
.field("lon", -73.95)
|
||||
.endObject()
|
||||
.endObject()
|
||||
)
|
||||
.setSource(jsonBuilder().startObject().field("name", "Brooklyn").field("location", "POINT(-73.95 40.65)").endObject())
|
||||
.get();
|
||||
|
||||
client().admin().indices().prepareRefresh().get();
|
||||
|
@ -192,12 +147,7 @@ public class GeoBoundingBoxQueryIT extends OpenSearchIntegTestCase {
|
|||
public void testLimit2BoundingBox() throws Exception {
|
||||
Version version = VersionUtils.randomIndexCompatibleVersion(random());
|
||||
Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build();
|
||||
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder()
|
||||
.startObject()
|
||||
.startObject("properties")
|
||||
.startObject("location")
|
||||
.field("type", "geo_point");
|
||||
xContentBuilder.endObject().endObject().endObject();
|
||||
XContentBuilder xContentBuilder = getMapping();
|
||||
assertAcked(prepareCreate("test").setSettings(settings).setMapping(xContentBuilder));
|
||||
ensureGreen();
|
||||
|
||||
|
@ -207,10 +157,7 @@ public class GeoBoundingBoxQueryIT extends OpenSearchIntegTestCase {
|
|||
jsonBuilder().startObject()
|
||||
.field("userid", 880)
|
||||
.field("title", "Place in Stockholm")
|
||||
.startObject("location")
|
||||
.field("lat", 59.328355000000002)
|
||||
.field("lon", 18.036842)
|
||||
.endObject()
|
||||
.field("location", "POINT(59.328355000000002 18.036842)")
|
||||
.endObject()
|
||||
)
|
||||
.setRefreshPolicy(IMMEDIATE)
|
||||
|
@ -222,10 +169,7 @@ public class GeoBoundingBoxQueryIT extends OpenSearchIntegTestCase {
|
|||
jsonBuilder().startObject()
|
||||
.field("userid", 534)
|
||||
.field("title", "Place in Montreal")
|
||||
.startObject("location")
|
||||
.field("lat", 45.509526999999999)
|
||||
.field("lon", -73.570986000000005)
|
||||
.endObject()
|
||||
.field("location", "POINT(-73.570986000000005 45.509526999999999)")
|
||||
.endObject()
|
||||
)
|
||||
.setRefreshPolicy(IMMEDIATE)
|
||||
|
@ -271,12 +215,7 @@ public class GeoBoundingBoxQueryIT extends OpenSearchIntegTestCase {
|
|||
public void testCompleteLonRange() throws Exception {
|
||||
Version version = VersionUtils.randomIndexCompatibleVersion(random());
|
||||
Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build();
|
||||
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder()
|
||||
.startObject()
|
||||
.startObject("properties")
|
||||
.startObject("location")
|
||||
.field("type", "geo_point");
|
||||
xContentBuilder.endObject().endObject().endObject();
|
||||
XContentBuilder xContentBuilder = getMapping();
|
||||
assertAcked(prepareCreate("test").setSettings(settings).setMapping(xContentBuilder));
|
||||
ensureGreen();
|
||||
|
||||
|
@ -286,10 +225,7 @@ public class GeoBoundingBoxQueryIT extends OpenSearchIntegTestCase {
|
|||
jsonBuilder().startObject()
|
||||
.field("userid", 880)
|
||||
.field("title", "Place in Stockholm")
|
||||
.startObject("location")
|
||||
.field("lat", 59.328355000000002)
|
||||
.field("lon", 18.036842)
|
||||
.endObject()
|
||||
.field("location", "POINT(18.036842 59.328355000000002)")
|
||||
.endObject()
|
||||
)
|
||||
.setRefreshPolicy(IMMEDIATE)
|
||||
|
@ -301,10 +237,7 @@ public class GeoBoundingBoxQueryIT extends OpenSearchIntegTestCase {
|
|||
jsonBuilder().startObject()
|
||||
.field("userid", 534)
|
||||
.field("title", "Place in Montreal")
|
||||
.startObject("location")
|
||||
.field("lat", 45.509526999999999)
|
||||
.field("lon", -73.570986000000005)
|
||||
.endObject()
|
||||
.field("location", "POINT(-73.570986000000005 45.509526999999999)")
|
||||
.endObject()
|
||||
)
|
||||
.setRefreshPolicy(IMMEDIATE)
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* The OpenSearch Contributors require contributions made to
|
||||
* this file be licensed under the Apache-2.0 license or a
|
||||
* compatible open source license.
|
||||
*/
|
||||
|
||||
package org.opensearch.search.geo;
|
||||
|
||||
import org.opensearch.common.xcontent.XContentBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class GeoBoundingBoxQueryGeoPointsIT extends AbstractGeoBoundingBoxQueryIT {
|
||||
|
||||
@Override
|
||||
public XContentBuilder addGeoMapping(XContentBuilder parentMapping) throws IOException {
|
||||
return parentMapping.startObject("location").field("type", "geo_point").endObject();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* The OpenSearch Contributors require contributions made to
|
||||
* this file be licensed under the Apache-2.0 license or a
|
||||
* compatible open source license.
|
||||
*/
|
||||
|
||||
package org.opensearch.search.geo;
|
||||
|
||||
import org.opensearch.common.xcontent.XContentBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class GeoBoundingBoxQueryGeoShapesIT extends AbstractGeoBoundingBoxQueryIT {
|
||||
|
||||
@Override
|
||||
public XContentBuilder addGeoMapping(XContentBuilder parentMapping) throws IOException {
|
||||
parentMapping = parentMapping.startObject("location").field("type", "geo_shape");
|
||||
if (randomBoolean()) {
|
||||
parentMapping.field("strategy", "recursive");
|
||||
}
|
||||
return parentMapping.endObject();
|
||||
}
|
||||
}
|
|
@ -32,9 +32,6 @@
|
|||
|
||||
package org.opensearch.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.opensearch.OpenSearchParseException;
|
||||
|
@ -44,13 +41,17 @@ import org.opensearch.common.ParsingException;
|
|||
import org.opensearch.common.geo.GeoBoundingBox;
|
||||
import org.opensearch.common.geo.GeoPoint;
|
||||
import org.opensearch.common.geo.GeoUtils;
|
||||
import org.opensearch.common.geo.ShapeRelation;
|
||||
import org.opensearch.common.geo.SpatialStrategy;
|
||||
import org.opensearch.common.io.stream.StreamInput;
|
||||
import org.opensearch.common.io.stream.StreamOutput;
|
||||
import org.opensearch.common.xcontent.XContentBuilder;
|
||||
import org.opensearch.common.xcontent.XContentParser;
|
||||
import org.opensearch.geometry.Rectangle;
|
||||
import org.opensearch.geometry.utils.Geohash;
|
||||
import org.opensearch.index.mapper.GeoPointFieldMapper.GeoPointFieldType;
|
||||
import org.opensearch.index.mapper.GeoPointFieldMapper;
|
||||
import org.opensearch.index.mapper.GeoShapeFieldMapper;
|
||||
import org.opensearch.index.mapper.GeoShapeQueryable;
|
||||
import org.opensearch.index.mapper.MappedFieldType;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -315,11 +316,24 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
|
|||
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 == false) {
|
||||
throw new QueryShardException(
|
||||
context,
|
||||
"type ["
|
||||
+ fieldType
|
||||
+ "] for field ["
|
||||
+ fieldName
|
||||
+ "] is not supported for ["
|
||||
+ NAME
|
||||
+ "] queries. Must be one of ["
|
||||
+ GeoPointFieldMapper.CONTENT_TYPE
|
||||
+ "] or ["
|
||||
+ GeoShapeFieldMapper.CONTENT_TYPE
|
||||
+ "]"
|
||||
);
|
||||
}
|
||||
|
||||
QueryValidationException exception = checkLatLon();
|
||||
|
@ -344,24 +358,14 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
|
|||
}
|
||||
}
|
||||
|
||||
Query query = LatLonPoint.newBoxQuery(
|
||||
fieldType.name(),
|
||||
luceneBottomRight.getLat(),
|
||||
luceneTopLeft.getLat(),
|
||||
final GeoShapeQueryable geoShapeQueryable = (GeoShapeQueryable) fieldType;
|
||||
final Rectangle rectangle = new Rectangle(
|
||||
luceneTopLeft.getLon(),
|
||||
luceneBottomRight.getLon()
|
||||
luceneBottomRight.getLon(),
|
||||
luceneTopLeft.getLat(),
|
||||
luceneBottomRight.getLat()
|
||||
);
|
||||
if (fieldType.hasDocValues()) {
|
||||
Query dvQuery = LatLonDocValuesField.newSlowBoxQuery(
|
||||
fieldType.name(),
|
||||
luceneBottomRight.getLat(),
|
||||
luceneTopLeft.getLat(),
|
||||
luceneTopLeft.getLon(),
|
||||
luceneBottomRight.getLon()
|
||||
);
|
||||
query = new IndexOrDocValuesQuery(query, dvQuery);
|
||||
}
|
||||
return query;
|
||||
return geoShapeQueryable.geoShapeQuery(rectangle, fieldType.name(), SpatialStrategy.RECURSIVE, ShapeRelation.INTERSECTS, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -40,6 +40,8 @@ import org.apache.lucene.search.Query;
|
|||
import org.opensearch.OpenSearchParseException;
|
||||
import org.opensearch.common.geo.GeoPoint;
|
||||
import org.opensearch.common.geo.GeoUtils;
|
||||
import org.opensearch.index.mapper.GeoPointFieldMapper;
|
||||
import org.opensearch.index.mapper.GeoShapeFieldMapper;
|
||||
import org.opensearch.index.mapper.MappedFieldType;
|
||||
import org.opensearch.test.AbstractQueryTestCase;
|
||||
import org.opensearch.test.geo.RandomShapeGenerator;
|
||||
|
@ -58,7 +60,7 @@ public class GeoBoundingBoxQueryBuilderTests extends AbstractQueryTestCase<GeoBo
|
|||
|
||||
@Override
|
||||
protected GeoBoundingBoxQueryBuilder doCreateTestQueryBuilder() {
|
||||
String fieldName = randomFrom(GEO_POINT_FIELD_NAME, GEO_POINT_ALIAS_FIELD_NAME);
|
||||
String fieldName = randomFrom(GEO_POINT_FIELD_NAME, GEO_POINT_ALIAS_FIELD_NAME, GEO_SHAPE_FIELD_NAME);
|
||||
GeoBoundingBoxQueryBuilder builder = new GeoBoundingBoxQueryBuilder(fieldName);
|
||||
Rectangle box = RandomShapeGenerator.xRandomRectangle(random(), RandomShapeGenerator.xRandomPoint(random()));
|
||||
|
||||
|
@ -124,7 +126,7 @@ public class GeoBoundingBoxQueryBuilderTests extends AbstractQueryTestCase<GeoBo
|
|||
GeoBoundingBoxQueryBuilder qb = createTestQueryBuilder();
|
||||
qb.ignoreUnmapped(false);
|
||||
QueryShardException e = expectThrows(QueryShardException.class, () -> 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() {
|
||||
|
@ -223,10 +225,11 @@ public class GeoBoundingBoxQueryBuilderTests extends AbstractQueryTestCase<GeoBo
|
|||
|
||||
@Override
|
||||
protected void doAssertLuceneQuery(GeoBoundingBoxQueryBuilder queryBuilder, Query query, QueryShardContext context) throws IOException {
|
||||
MappedFieldType fieldType = context.fieldMapper(queryBuilder.fieldName());
|
||||
final MappedFieldType fieldType = context.fieldMapper(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(
|
||||
|
@ -250,6 +253,8 @@ public class GeoBoundingBoxQueryBuilderTests extends AbstractQueryTestCase<GeoBo
|
|||
),
|
||||
dvQuery
|
||||
);
|
||||
} else {
|
||||
assertEquals(GeoShapeFieldMapper.GeoShapeFieldType.class, fieldType.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -572,6 +577,6 @@ public class GeoBoundingBoxQueryBuilderTests extends AbstractQueryTestCase<GeoBo
|
|||
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]"));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue