Browse Source

[GEO] Add WKT Support to GeoBoundingBoxQueryBuilder

Add WKT BBOX parsing support to GeoBoundingBoxQueryBuilder.
Nicholas Knize 7 years ago
parent
commit
5ed25f1e12

+ 25 - 0
docs/reference/query-dsl/geo-bounding-box-query.asciidoc

@@ -180,6 +180,31 @@ GET /_search
 --------------------------------------------------
 // CONSOLE
 
+[float]
+===== Bounding Box as Well-Known Text (WKT)
+
+[source,js]
+--------------------------------------------------
+GET /_search
+{
+    "query": {
+        "bool" : {
+            "must" : {
+                "match_all" : {}
+            },
+            "filter" : {
+                "geo_bounding_box" : {
+                    "pin.location" : {
+                        "wkt" : "BBOX (-74.1, -71.12, 40.73, 40.01)"
+                    }
+                }
+            }
+        }
+    }
+}
+--------------------------------------------------
+// CONSOLE
+
 [float]
 ===== Geohash
 

+ 17 - 4
server/src/main/java/org/elasticsearch/common/geo/parsers/GeoWKTParser.java

@@ -63,6 +63,12 @@ public class GeoWKTParser {
 
     public static ShapeBuilder parse(XContentParser parser)
             throws IOException, ElasticsearchParseException {
+        return parseExpectedType(parser, null);
+    }
+
+    /** throws an exception if the parsed geometry type does not match the expected shape type */
+    public static ShapeBuilder parseExpectedType(XContentParser parser, final GeoShapeType shapeType)
+            throws IOException, ElasticsearchParseException {
         FastStringReader reader = new FastStringReader(parser.text());
         try {
             // setup the tokenizer; configured to read words w/o numbers
@@ -77,7 +83,7 @@ public class GeoWKTParser {
             tokenizer.wordChars('.', '.');
             tokenizer.whitespaceChars(0, ' ');
             tokenizer.commentChar('#');
-            ShapeBuilder builder = parseGeometry(tokenizer);
+            ShapeBuilder builder = parseGeometry(tokenizer, shapeType);
             checkEOF(tokenizer);
             return builder;
         } finally {
@@ -86,8 +92,14 @@ public class GeoWKTParser {
     }
 
     /** parse geometry from the stream tokenizer */
-    private static ShapeBuilder parseGeometry(StreamTokenizer stream) throws IOException, ElasticsearchParseException {
+    private static ShapeBuilder parseGeometry(StreamTokenizer stream, GeoShapeType shapeType)
+            throws IOException, ElasticsearchParseException {
         final GeoShapeType type = GeoShapeType.forName(nextWord(stream));
+        if (shapeType != null && shapeType != GeoShapeType.GEOMETRYCOLLECTION) {
+            if (type.wktName().equals(shapeType.wktName()) == false) {
+                throw new ElasticsearchParseException("Expected geometry type [{}] but found [{}]", shapeType, type);
+            }
+        }
         switch (type) {
             case POINT:
                 return parsePoint(stream);
@@ -228,9 +240,10 @@ public class GeoWKTParser {
         if (nextEmptyOrOpen(stream).equals(EMPTY)) {
             return null;
         }
-        GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(parseGeometry(stream));
+        GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(
+            parseGeometry(stream, GeoShapeType.GEOMETRYCOLLECTION));
         while (nextCloserOrComma(stream).equals(COMMA)) {
-            builder.shape(parseGeometry(stream));
+            builder.shape(parseGeometry(stream, null));
         }
         return builder;
     }

+ 83 - 52
server/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxQueryBuilder.java

@@ -31,7 +31,10 @@ import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.ParsingException;
 import org.elasticsearch.common.geo.GeoHashUtils;
 import org.elasticsearch.common.geo.GeoPoint;
+import org.elasticsearch.common.geo.GeoShapeType;
 import org.elasticsearch.common.geo.GeoUtils;
+import org.elasticsearch.common.geo.builders.EnvelopeBuilder;
+import org.elasticsearch.common.geo.parsers.GeoWKTParser;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.xcontent.XContentBuilder;
@@ -62,7 +65,6 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
 
     private static final ParseField TYPE_FIELD = new ParseField("type");
     private static final ParseField VALIDATION_METHOD_FIELD = new ParseField("validation_method");
-    private static final ParseField FIELD_FIELD = new ParseField("field");
     private static final ParseField TOP_FIELD = new ParseField("top");
     private static final ParseField BOTTOM_FIELD = new ParseField("bottom");
     private static final ParseField LEFT_FIELD = new ParseField("left");
@@ -72,6 +74,8 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
     private static final ParseField TOP_RIGHT_FIELD = new ParseField("top_right");
     private static final ParseField BOTTOM_LEFT_FIELD = new ParseField("bottom_left");
     private static final ParseField IGNORE_UNMAPPED_FIELD = new ParseField("ignore_unmapped");
+    private static final ParseField WKT_FIELD = new ParseField("wkt");
+
 
     /** Name of field holding geo coordinates to compute the bounding box on.*/
     private final String fieldName;
@@ -378,11 +382,6 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
     public static GeoBoundingBoxQueryBuilder fromXContent(XContentParser parser) throws IOException {
         String fieldName = null;
 
-        double top = Double.NaN;
-        double bottom = Double.NaN;
-        double left = Double.NaN;
-        double right = Double.NaN;
-
         float boost = AbstractQueryBuilder.DEFAULT_BOOST;
         String queryName = null;
         String currentFieldName = null;
@@ -390,56 +389,18 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
         GeoValidationMethod validationMethod = null;
         boolean ignoreUnmapped = DEFAULT_IGNORE_UNMAPPED;
 
-        GeoPoint sparse = new GeoPoint();
-
+        Rectangle bbox = null;
         String type = "memory";
 
         while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
             if (token == XContentParser.Token.FIELD_NAME) {
                 currentFieldName = parser.currentName();
             } else if (token == XContentParser.Token.START_OBJECT) {
-                fieldName = currentFieldName;
-
-                while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
-                    if (token == XContentParser.Token.FIELD_NAME) {
-                        currentFieldName = parser.currentName();
-                        token = parser.nextToken();
-                        if (FIELD_FIELD.match(currentFieldName)) {
-                            fieldName = parser.text();
-                        } else if (TOP_FIELD.match(currentFieldName)) {
-                            top = parser.doubleValue();
-                        } else if (BOTTOM_FIELD.match(currentFieldName)) {
-                            bottom = parser.doubleValue();
-                        } else if (LEFT_FIELD.match(currentFieldName)) {
-                            left = parser.doubleValue();
-                        } else if (RIGHT_FIELD.match(currentFieldName)) {
-                            right = parser.doubleValue();
-                        } else {
-                            if (TOP_LEFT_FIELD.match(currentFieldName)) {
-                                GeoUtils.parseGeoPoint(parser, sparse);
-                                top = sparse.getLat();
-                                left = sparse.getLon();
-                            } else if (BOTTOM_RIGHT_FIELD.match(currentFieldName)) {
-                                GeoUtils.parseGeoPoint(parser, sparse);
-                                bottom = sparse.getLat();
-                                right = sparse.getLon();
-                            } else if (TOP_RIGHT_FIELD.match(currentFieldName)) {
-                                GeoUtils.parseGeoPoint(parser, sparse);
-                                top = sparse.getLat();
-                                right = sparse.getLon();
-                            } else if (BOTTOM_LEFT_FIELD.match(currentFieldName)) {
-                                GeoUtils.parseGeoPoint(parser, sparse);
-                                bottom = sparse.getLat();
-                                left = sparse.getLon();
-                            } else {
-                                throw new ElasticsearchParseException("failed to parse [{}] query. unexpected field [{}]",
-                                        NAME, currentFieldName);
-                            }
-                        }
-                    } else {
-                        throw new ElasticsearchParseException("failed to parse [{}] query. field name expected but [{}] found",
-                                NAME, token);
-                    }
+                try {
+                    bbox = parseBoundingBox(parser);
+                    fieldName = currentFieldName;
+                } catch (Exception e) {
+                    throw new ElasticsearchParseException("failed to parse [{}] query. [{}]", NAME, e.getMessage());
                 }
             } else if (token.isValue()) {
                 if (AbstractQueryBuilder.NAME_FIELD.match(currentFieldName)) {
@@ -459,8 +420,13 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
             }
         }
 
-        final GeoPoint topLeft = sparse.reset(top, left);  //just keep the object
-        final GeoPoint bottomRight = new GeoPoint(bottom, right);
+        if (bbox == null) {
+            throw new ElasticsearchParseException("failed to parse [{}] query. bounding box not provided", NAME);
+        }
+
+        final GeoPoint topLeft = new GeoPoint(bbox.maxLat, bbox.minLon);  //just keep the object
+        final GeoPoint bottomRight = new GeoPoint(bbox.minLat, bbox.maxLon);
+
         GeoBoundingBoxQueryBuilder builder = new GeoBoundingBoxQueryBuilder(fieldName);
         builder.setCorners(topLeft, bottomRight);
         builder.queryName(queryName);
@@ -493,4 +459,69 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
     public String getWriteableName() {
         return NAME;
     }
+
+    public static Rectangle parseBoundingBox(XContentParser parser) throws IOException, ElasticsearchParseException {
+        XContentParser.Token token = parser.currentToken();
+        if (token != XContentParser.Token.START_OBJECT) {
+            throw new ElasticsearchParseException("failed to parse bounding box. Expected start object but found [{}]", token);
+        }
+
+        double top = Double.NaN;
+        double bottom = Double.NaN;
+        double left = Double.NaN;
+        double right = Double.NaN;
+
+        String currentFieldName;
+        GeoPoint sparse = new GeoPoint();
+        EnvelopeBuilder envelope = null;
+
+        while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+            if (token == XContentParser.Token.FIELD_NAME) {
+                currentFieldName = parser.currentName();
+                token = parser.nextToken();
+                if (WKT_FIELD.match(currentFieldName)) {
+                    envelope = (EnvelopeBuilder)(GeoWKTParser.parseExpectedType(parser, GeoShapeType.ENVELOPE));
+                } else if (TOP_FIELD.match(currentFieldName)) {
+                    top = parser.doubleValue();
+                } else if (BOTTOM_FIELD.match(currentFieldName)) {
+                    bottom = parser.doubleValue();
+                } else if (LEFT_FIELD.match(currentFieldName)) {
+                    left = parser.doubleValue();
+                } else if (RIGHT_FIELD.match(currentFieldName)) {
+                    right = parser.doubleValue();
+                } else {
+                    if (TOP_LEFT_FIELD.match(currentFieldName)) {
+                        GeoUtils.parseGeoPoint(parser, sparse);
+                        top = sparse.getLat();
+                        left = sparse.getLon();
+                    } else if (BOTTOM_RIGHT_FIELD.match(currentFieldName)) {
+                        GeoUtils.parseGeoPoint(parser, sparse);
+                        bottom = sparse.getLat();
+                        right = sparse.getLon();
+                    } else if (TOP_RIGHT_FIELD.match(currentFieldName)) {
+                        GeoUtils.parseGeoPoint(parser, sparse);
+                        top = sparse.getLat();
+                        right = sparse.getLon();
+                    } else if (BOTTOM_LEFT_FIELD.match(currentFieldName)) {
+                        GeoUtils.parseGeoPoint(parser, sparse);
+                        bottom = sparse.getLat();
+                        left = sparse.getLon();
+                    } else {
+                        throw new ElasticsearchParseException("failed to parse bounding box. unexpected field [{}]", currentFieldName);
+                    }
+                }
+            } else {
+                throw new ElasticsearchParseException("failed to parse bounding box. field name expected but [{}] found", token);
+            }
+        }
+        if (envelope != null) {
+            if ((Double.isNaN(top) || Double.isNaN(bottom) || Double.isNaN(left) || Double.isNaN(right)) == false) {
+                throw new ElasticsearchParseException("failed to parse bounding box. Conflicting definition found "
+                    + "using well-known text and explicit corners.");
+            }
+            org.locationtech.spatial4j.shape.Rectangle r = envelope.build();
+            return new Rectangle(r.getMinY(), r.getMaxY(), r.getMinX(), r.getMaxX());
+        }
+        return new Rectangle(bottom, top, left, right);
+    }
 }

+ 12 - 0
server/src/test/java/org/elasticsearch/common/geo/GeoWKTShapeParserTests.java

@@ -39,6 +39,7 @@ import org.elasticsearch.common.geo.builders.ShapeBuilder;
 import org.elasticsearch.common.geo.parsers.GeoWKTParser;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.test.geo.RandomShapeGenerator;
 import org.locationtech.spatial4j.exception.InvalidShapeException;
 import org.locationtech.spatial4j.shape.Rectangle;
@@ -51,6 +52,8 @@ import java.util.ArrayList;
 import java.util.List;
 
 import static org.elasticsearch.common.geo.builders.ShapeBuilder.SPATIAL_CONTEXT;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasToString;
 
 /**
  * Tests for {@code GeoWKTShapeParser}
@@ -252,4 +255,13 @@ public class GeoWKTShapeParserTests extends BaseGeoParsingTestCase {
             assertExpected(gcb.build(), gcb);
         }
     }
+
+    public void testUnexpectedShapeException() throws IOException {
+        XContentBuilder builder = toWKTContent(new PointBuilder(-1, 2), false);
+        XContentParser parser = createParser(builder);
+        parser.nextToken();
+        ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class,
+            () -> GeoWKTParser.parseExpectedType(parser, GeoShapeType.POLYGON));
+        assertThat(e, hasToString(containsString("Expected geometry type [polygon] but found [point]")));
+    }
 }

+ 44 - 0
server/src/test/java/org/elasticsearch/index/query/GeoBoundingBoxQueryBuilderTests.java

@@ -406,6 +406,50 @@ public class GeoBoundingBoxQueryBuilderTests extends AbstractQueryTestCase<GeoBo
         assertEquals(json, GeoExecType.MEMORY, parsed.type());
     }
 
+    public void testFromWKT() throws IOException {
+        String wkt =
+            "{\n" +
+                "  \"geo_bounding_box\" : {\n" +
+                "    \"pin.location\" : {\n" +
+                "      \"wkt\" : \"BBOX (-74.1, -71.12, 40.73, 40.01)\"\n" +
+                "    },\n" +
+                "    \"validation_method\" : \"STRICT\",\n" +
+                "    \"type\" : \"MEMORY\",\n" +
+                "    \"ignore_unmapped\" : false,\n" +
+                "    \"boost\" : 1.0\n" +
+                "  }\n" +
+                "}";
+
+        // toXContent generates the query in geojson only; for now we need to test against the expected
+        // geojson generated content
+        String expectedJson =
+            "{\n" +
+                "  \"geo_bounding_box\" : {\n" +
+                "    \"pin.location\" : {\n" +
+                "      \"top_left\" : [ -74.1, 40.73 ],\n" +
+                "      \"bottom_right\" : [ -71.12, 40.01 ]\n" +
+                "    },\n" +
+                "    \"validation_method\" : \"STRICT\",\n" +
+                "    \"type\" : \"MEMORY\",\n" +
+                "    \"ignore_unmapped\" : false,\n" +
+                "    \"boost\" : 1.0\n" +
+                "  }\n" +
+                "}";
+
+        // parse with wkt
+        GeoBoundingBoxQueryBuilder parsed = (GeoBoundingBoxQueryBuilder) parseQuery(wkt);
+        // check the builder's generated geojson content against the expected json output
+        checkGeneratedJson(expectedJson, parsed);
+        double delta = 0d;
+        assertEquals(expectedJson, "pin.location", parsed.fieldName());
+        assertEquals(expectedJson, -74.1, parsed.topLeft().getLon(), delta);
+        assertEquals(expectedJson, 40.73, parsed.topLeft().getLat(), delta);
+        assertEquals(expectedJson, -71.12, parsed.bottomRight().getLon(), delta);
+        assertEquals(expectedJson, 40.01, parsed.bottomRight().getLat(), delta);
+        assertEquals(expectedJson, 1.0, parsed.boost(), delta);
+        assertEquals(expectedJson, GeoExecType.MEMORY, parsed.type());
+    }
+
     @Override
     public void testMustRewrite() throws IOException {
         assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);