Browse Source

Add CONTAINS relation to geo_shape query

At the time of geo_shape query conception, CONTAINS was not yet a supported spatial operation in Lucene. Since it is now available this commit adds ShapeRelation.CONTAINS to GeoShapeQuery. Randomized testing is included and documentation is updated.
Nicholas Knize 10 years ago
parent
commit
0d349854d3

+ 2 - 1
core/src/main/java/org/elasticsearch/common/geo/ShapeRelation.java

@@ -34,7 +34,8 @@ public enum ShapeRelation implements Writeable<ShapeRelation>{
 
     INTERSECTS("intersects"),
     DISJOINT("disjoint"),
-    WITHIN("within");
+    WITHIN("within"),
+    CONTAINS("contains");
 
     private final String relationName;
 

+ 2 - 0
core/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java

@@ -361,6 +361,8 @@ public class GeoShapeQueryBuilder extends AbstractQueryBuilder<GeoShapeQueryBuil
             return new SpatialArgs(SpatialOperation.Intersects, shape.build());
         case WITHIN:
             return new SpatialArgs(SpatialOperation.IsWithin, shape.build());
+        case CONTAINS:
+            return new SpatialArgs(SpatialOperation.Contains, shape.build());
         default:
             throw new IllegalArgumentException("invalid relation [" + relation + "]");
         }

+ 86 - 132
core/src/test/java/org/elasticsearch/search/geo/GeoShapeIntegrationIT.java → core/src/test/java/org/elasticsearch/search/geo/GeoShapeQueryTests.java

@@ -19,25 +19,20 @@
 
 package org.elasticsearch.search.geo;
 
-import org.apache.lucene.util.LuceneTestCase;
+import com.spatial4j.core.shape.Rectangle;
 import org.elasticsearch.action.get.GetResponse;
 import org.elasticsearch.action.search.SearchResponse;
-import org.elasticsearch.cluster.ClusterState;
-import org.elasticsearch.cluster.routing.IndexShardRoutingTable;
 import org.elasticsearch.common.geo.ShapeRelation;
+import org.elasticsearch.common.geo.builders.EnvelopeBuilder;
 import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder;
 import org.elasticsearch.common.geo.builders.ShapeBuilder;
 import org.elasticsearch.common.geo.builders.ShapeBuilders;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentFactory;
-import org.elasticsearch.index.IndexService;
-import org.elasticsearch.index.mapper.MappedFieldType;
 import org.elasticsearch.index.mapper.MapperParsingException;
-import org.elasticsearch.index.mapper.geo.GeoShapeFieldMapper;
 import org.elasticsearch.index.query.GeoShapeQueryBuilder;
 import org.elasticsearch.index.query.QueryBuilders;
-import org.elasticsearch.indices.IndicesService;
-import org.elasticsearch.test.ESIntegTestCase;
+import org.elasticsearch.test.ESSingleNodeTestCase;
 import org.elasticsearch.test.geo.RandomShapeGenerator;
 
 import java.io.IOException;
@@ -46,25 +41,25 @@ import java.util.Locale;
 import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
 import static org.elasticsearch.index.query.QueryBuilders.geoIntersectionQuery;
 import static org.elasticsearch.index.query.QueryBuilders.geoShapeQuery;
-import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
+import static org.elasticsearch.test.geo.RandomShapeGenerator.createGeometryCollectionWithin;
+import static org.elasticsearch.test.geo.RandomShapeGenerator.xRandomPoint;
+import static org.elasticsearch.test.geo.RandomShapeGenerator.xRandomRectangle;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse;
-import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.instanceOf;
-import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.*;
 
-public class GeoShapeIntegrationIT extends ESIntegTestCase {
+public class GeoShapeQueryTests extends ESSingleNodeTestCase {
     public void testNullShape() throws Exception {
         String mapping = XContentFactory.jsonBuilder().startObject().startObject("type1")
                 .startObject("properties").startObject("location")
                 .field("type", "geo_shape")
                 .endObject().endObject()
                 .endObject().endObject().string();
-        assertAcked(prepareCreate("test").addMapping("type1", mapping));
+        client().admin().indices().prepareCreate("test").addMapping("type1", mapping).execute().actionGet();
         ensureGreen();
 
-        indexRandom(false, client().prepareIndex("test", "type1", "aNullshape").setSource("{\"location\": null}"));
+        client().prepareIndex("test", "type1", "aNullshape").setSource("{\"location\": null}").setRefresh(true)
+                .execute().actionGet();
         GetResponse result = client().prepareGet("test", "type1", "aNullshape").execute().actionGet();
         assertThat(result.getField("location"), nullValue());
     }
@@ -76,30 +71,28 @@ public class GeoShapeIntegrationIT extends ESIntegTestCase {
                 .field("tree", "quadtree")
                 .endObject().endObject()
                 .endObject().endObject().string();
-        assertAcked(prepareCreate("test").addMapping("type1", mapping));
+        client().admin().indices().prepareCreate("test").addMapping("type1", mapping).execute().actionGet();
         ensureGreen();
 
-        indexRandom(true,
-
-                client().prepareIndex("test", "type1", "1").setSource(jsonBuilder().startObject()
-                        .field("name", "Document 1")
-                        .startObject("location")
-                        .field("type", "point")
-                        .startArray("coordinates").value(-30).value(-30).endArray()
-                        .endObject()
-                        .endObject()),
+        client().prepareIndex("test", "type1", "1").setSource(jsonBuilder().startObject()
+                .field("name", "Document 1")
+                .startObject("location")
+                .field("type", "point")
+                .startArray("coordinates").value(-30).value(-30).endArray()
+                .endObject()
+                .endObject()).setRefresh(true).execute().actionGet();
 
-                client().prepareIndex("test", "type1", "2").setSource(jsonBuilder().startObject()
-                        .field("name", "Document 2")
-                        .startObject("location")
-                        .field("type", "point")
-                        .startArray("coordinates").value(-45).value(-50).endArray()
-                        .endObject()
-                        .endObject()));
+        client().prepareIndex("test", "type1", "2").setSource(jsonBuilder().startObject()
+                .field("name", "Document 2")
+                .startObject("location")
+                .field("type", "point")
+                .startArray("coordinates").value(-45).value(-50).endArray()
+                .endObject()
+                .endObject()).setRefresh(true).execute().actionGet();
 
         ShapeBuilder shape = ShapeBuilders.newEnvelope().topLeft(-45, 45).bottomRight(45, -45);
 
-        SearchResponse searchResponse = client().prepareSearch()
+        SearchResponse searchResponse = client().prepareSearch("test").setTypes("type1")
                 .setQuery(geoIntersectionQuery("location", shape))
                 .execute().actionGet();
 
@@ -108,7 +101,7 @@ public class GeoShapeIntegrationIT extends ESIntegTestCase {
         assertThat(searchResponse.getHits().hits().length, equalTo(1));
         assertThat(searchResponse.getHits().getAt(0).id(), equalTo("1"));
 
-        searchResponse = client().prepareSearch()
+        searchResponse = client().prepareSearch("test").setTypes("type1")
                 .setQuery(geoShapeQuery("location", shape))
                 .execute().actionGet();
 
@@ -125,10 +118,10 @@ public class GeoShapeIntegrationIT extends ESIntegTestCase {
                 .field("tree", "quadtree")
                 .endObject().endObject()
                 .endObject().endObject().string();
-        assertAcked(prepareCreate("test").addMapping("type1", mapping));
+        client().admin().indices().prepareCreate("test").addMapping("type1", mapping).execute().actionGet();
         ensureGreen();
 
-        indexRandom(true, client().prepareIndex("test", "type1", "blakely").setSource(jsonBuilder().startObject()
+        client().prepareIndex("test", "type1", "blakely").setSource(jsonBuilder().startObject()
                 .field("name", "Blakely Island")
                 .startObject("location")
                 .field("type", "polygon")
@@ -139,14 +132,13 @@ public class GeoShapeIntegrationIT extends ESIntegTestCase {
                 .startArray().value(-122.83).value(48.57).endArray() // close the polygon
                 .endArray().endArray()
                 .endObject()
-                .endObject()));
-
+                .endObject()).setRefresh(true).execute().actionGet();
 
         ShapeBuilder query = ShapeBuilders.newEnvelope().topLeft(-122.88, 48.62).bottomRight(-122.82, 48.54);
 
         // This search would fail if both geoshape indexing and geoshape filtering
         // used the bottom-level optimization in SpatialPrefixTree#recursiveGetNodes.
-        SearchResponse searchResponse = client().prepareSearch()
+        SearchResponse searchResponse = client().prepareSearch("test").setTypes("type1")
                 .setQuery(geoIntersectionQuery("location", query))
                 .execute().actionGet();
 
@@ -163,24 +155,23 @@ public class GeoShapeIntegrationIT extends ESIntegTestCase {
                 .field("tree", "quadtree")
                 .endObject().endObject()
                 .endObject().endObject().string();
-        assertAcked(prepareCreate("test").addMapping("type1", mapping));
+        client().admin().indices().prepareCreate("test").addMapping("type1", mapping).execute().actionGet();
         createIndex("shapes");
         ensureGreen();
 
         ShapeBuilder shape = ShapeBuilders.newEnvelope().topLeft(-45, 45).bottomRight(45, -45);
 
-        indexRandom(true,
-            client().prepareIndex("shapes", "shape_type", "Big_Rectangle").setSource(jsonBuilder().startObject()
-                .field("shape", shape).endObject()),
-            client().prepareIndex("test", "type1", "1").setSource(jsonBuilder().startObject()
+        client().prepareIndex("shapes", "shape_type", "Big_Rectangle").setSource(jsonBuilder().startObject()
+                .field("shape", shape).endObject()).setRefresh(true).execute().actionGet();
+        client().prepareIndex("test", "type1", "1").setSource(jsonBuilder().startObject()
                 .field("name", "Document 1")
                 .startObject("location")
                 .field("type", "point")
                 .startArray("coordinates").value(-30).value(-30).endArray()
                 .endObject()
-                .endObject()));
+                .endObject()).setRefresh(true).execute().actionGet();
 
-        SearchResponse searchResponse = client().prepareSearch("test")
+        SearchResponse searchResponse = client().prepareSearch("test").setTypes("type1")
                 .setQuery(geoIntersectionQuery("location", "Big_Rectangle", "shape_type"))
                 .execute().actionGet();
 
@@ -220,17 +211,17 @@ public class GeoShapeIntegrationIT extends ESIntegTestCase {
 
     public void testShapeFetchingPath() throws Exception {
         createIndex("shapes");
-        assertAcked(prepareCreate("test").addMapping("type", "location", "type=geo_shape"));
+        client().admin().indices().prepareCreate("test").addMapping("type", "location", "type=geo_shape").execute().actionGet();
 
         String location = "\"location\" : {\"type\":\"polygon\", \"coordinates\":[[[-10,-10],[10,-10],[10,10],[-10,10],[-10,-10]]]}";
-        indexRandom(true,
-                client().prepareIndex("shapes", "type", "1")
+
+        client().prepareIndex("shapes", "type", "1")
                 .setSource(
                         String.format(
                                 Locale.ROOT, "{ %s, \"1\" : { %s, \"2\" : { %s, \"3\" : { %s } }} }", location, location, location, location
                         )
-                ),
-                client().prepareIndex("test", "type", "1")
+                ).setRefresh(true).execute().actionGet();
+        client().prepareIndex("test", "type", "1")
                 .setSource(jsonBuilder().startObject().startObject("location")
                         .field("type", "polygon")
                         .startArray("coordinates").startArray()
@@ -240,8 +231,7 @@ public class GeoShapeIntegrationIT extends ESIntegTestCase {
                         .startArray().value(-20).value(20).endArray()
                         .startArray().value(-20).value(-20).endArray()
                         .endArray().endArray()
-                        .endObject().endObject()));
-        ensureSearchable("test", "shapes");
+                        .endObject().endObject()).setRefresh(true).execute().actionGet();
 
         GeoShapeQueryBuilder filter = QueryBuilders.geoShapeQuery("location", "1", "type").relation(ShapeRelation.INTERSECTS)
                 .indexedShapeIndex("shapes")
@@ -305,27 +295,52 @@ public class GeoShapeIntegrationIT extends ESIntegTestCase {
 
         logger.info("Created Random GeometryCollection containing " + gcb.numShapes() + " shapes");
 
-        createIndex("randshapes");
-        assertAcked(prepareCreate("test").addMapping("type", "location", "type=geo_shape"));
+        client().admin().indices().prepareCreate("test").addMapping("type", "location", "type=geo_shape")
+                .execute().actionGet();
 
         XContentBuilder docSource = gcb.toXContent(jsonBuilder().startObject().field("location"), null).endObject();
-        indexRandom(true, client().prepareIndex("test", "type", "1").setSource(docSource));
-
-        ensureSearchable("test");
+        client().prepareIndex("test", "type", "1").setSource(docSource).setRefresh(true).execute().actionGet();
 
         ShapeBuilder filterShape = (gcb.getShapeAt(randomIntBetween(0, gcb.numShapes() - 1)));
 
         GeoShapeQueryBuilder filter = QueryBuilders.geoShapeQuery("location", filterShape);
         filter.relation(ShapeRelation.INTERSECTS);
-        SearchResponse result = client().prepareSearch("test").setQuery(QueryBuilders.matchAllQuery())
+        SearchResponse result = client().prepareSearch("test").setTypes("type").setQuery(QueryBuilders.matchAllQuery())
                 .setPostFilter(filter).get();
         assertSearchResponse(result);
         assertHitCount(result, 1);
     }
 
+    public void testContainsShapeQuery() throws Exception {
+        // Create a random geometry collection.
+        Rectangle mbr = xRandomRectangle(getRandom(), xRandomPoint(getRandom()));
+        GeometryCollectionBuilder gcb = createGeometryCollectionWithin(getRandom(), mbr);
+
+        client().admin().indices().prepareCreate("test").addMapping("type", "location", "type=geo_shape")
+                .execute().actionGet();
+
+        XContentBuilder docSource = gcb.toXContent(jsonBuilder().startObject().field("location"), null).endObject();
+        client().prepareIndex("test", "type", "1").setSource(docSource).setRefresh(true).execute().actionGet();
+
+        // index the mbr of the collection
+        EnvelopeBuilder env = new EnvelopeBuilder().topLeft(mbr.getMinX(), mbr.getMaxY()).bottomRight(mbr.getMaxX(), mbr.getMinY());
+        docSource = env.toXContent(jsonBuilder().startObject().field("location"), null).endObject();
+        client().prepareIndex("test", "type", "2").setSource(docSource).setRefresh(true).execute().actionGet();
+
+        ShapeBuilder filterShape = (gcb.getShapeAt(randomIntBetween(0, gcb.numShapes() - 1)));
+        GeoShapeQueryBuilder filter = QueryBuilders.geoShapeQuery("location", filterShape)
+                .relation(ShapeRelation.INTERSECTS);
+        SearchResponse response = client().prepareSearch("test").setTypes("type").setQuery(QueryBuilders.matchAllQuery())
+                .setPostFilter(filter).get();
+        assertSearchResponse(response);
+
+        assertThat(response.getHits().totalHits(), greaterThan(0L));
+    }
+
     public void testShapeFilterWithDefinedGeoCollection() throws Exception {
         createIndex("shapes");
-        assertAcked(prepareCreate("test").addMapping("type", "location", "type=geo_shape"));
+        client().admin().indices().prepareCreate("test").addMapping("type", "location", "type=geo_shape")
+                .execute().actionGet();
 
         XContentBuilder docSource = jsonBuilder().startObject().startObject("location")
                 .field("type", "geometrycollection")
@@ -349,10 +364,8 @@ public class GeoShapeIntegrationIT extends ESIntegTestCase {
                 .endObject()
                 .endArray()
                 .endObject().endObject();
-        indexRandom(true,
-                client().prepareIndex("test", "type", "1")
-                .setSource(docSource));
-        ensureSearchable("test");
+        client().prepareIndex("test", "type", "1")
+                .setSource(docSource).setRefresh(true).execute().actionGet();
 
         GeoShapeQueryBuilder filter = QueryBuilders.geoShapeQuery(
                 "location",
@@ -360,7 +373,7 @@ public class GeoShapeIntegrationIT extends ESIntegTestCase {
                         .polygon(
                                 ShapeBuilders.newPolygon().point(99.0, -1.0).point(99.0, 3.0).point(103.0, 3.0).point(103.0, -1.0)
                                         .point(99.0, -1.0))).relation(ShapeRelation.INTERSECTS);
-        SearchResponse result = client().prepareSearch("test").setQuery(QueryBuilders.matchAllQuery())
+        SearchResponse result = client().prepareSearch("test").setTypes("type").setQuery(QueryBuilders.matchAllQuery())
                 .setPostFilter(filter).get();
         assertSearchResponse(result);
         assertHitCount(result, 1);
@@ -369,7 +382,7 @@ public class GeoShapeIntegrationIT extends ESIntegTestCase {
                 ShapeBuilders.newGeometryCollection().polygon(
                         ShapeBuilders.newPolygon().point(199.0, -11.0).point(199.0, 13.0).point(193.0, 13.0).point(193.0, -11.0)
                                 .point(199.0, -11.0))).relation(ShapeRelation.INTERSECTS);
-        result = client().prepareSearch("test").setQuery(QueryBuilders.matchAllQuery())
+        result = client().prepareSearch("test").setTypes("type").setQuery(QueryBuilders.matchAllQuery())
                 .setPostFilter(filter).get();
         assertSearchResponse(result);
         assertHitCount(result, 0);
@@ -378,65 +391,12 @@ public class GeoShapeIntegrationIT extends ESIntegTestCase {
                         .polygon(
                                 ShapeBuilders.newPolygon().point(199.0, -11.0).point(199.0, 13.0).point(193.0, 13.0).point(193.0, -11.0)
                                         .point(199.0, -11.0))).relation(ShapeRelation.INTERSECTS);
-        result = client().prepareSearch("test").setQuery(QueryBuilders.matchAllQuery())
+        result = client().prepareSearch("test").setTypes("type").setQuery(QueryBuilders.matchAllQuery())
                 .setPostFilter(filter).get();
         assertSearchResponse(result);
         assertHitCount(result, 1);
     }
 
-    /**
-     * Test that orientation parameter correctly persists across cluster restart
-     */
-    public void testOrientationPersistence() throws Exception {
-        String idxName = "orientation";
-        String mapping = XContentFactory.jsonBuilder().startObject().startObject("shape")
-                .startObject("properties").startObject("location")
-                .field("type", "geo_shape")
-                .field("orientation", "left")
-                .endObject().endObject()
-                .endObject().endObject().string();
-
-        // create index
-        assertAcked(prepareCreate(idxName).addMapping("shape", mapping));
-
-        mapping = XContentFactory.jsonBuilder().startObject().startObject("shape")
-                .startObject("properties").startObject("location")
-                .field("type", "geo_shape")
-                .field("orientation", "right")
-                .endObject().endObject()
-                .endObject().endObject().string();
-
-        assertAcked(prepareCreate(idxName+"2").addMapping("shape", mapping));
-        ensureGreen(idxName, idxName+"2");
-
-        internalCluster().fullRestart();
-        ensureGreen(idxName, idxName+"2");
-
-        // left orientation test
-        IndicesService indicesService = internalCluster().getInstance(IndicesService.class, findNodeName(idxName));
-        IndexService indexService = indicesService.indexService(idxName);
-        MappedFieldType fieldType = indexService.mapperService().smartNameFieldType("location");
-        assertThat(fieldType, instanceOf(GeoShapeFieldMapper.GeoShapeFieldType.class));
-
-        GeoShapeFieldMapper.GeoShapeFieldType gsfm = (GeoShapeFieldMapper.GeoShapeFieldType)fieldType;
-        ShapeBuilder.Orientation orientation = gsfm.orientation();
-        assertThat(orientation, equalTo(ShapeBuilder.Orientation.CLOCKWISE));
-        assertThat(orientation, equalTo(ShapeBuilder.Orientation.LEFT));
-        assertThat(orientation, equalTo(ShapeBuilder.Orientation.CW));
-
-        // right orientation test
-        indicesService = internalCluster().getInstance(IndicesService.class, findNodeName(idxName+"2"));
-        indexService = indicesService.indexService(idxName+"2");
-        fieldType = indexService.mapperService().smartNameFieldType("location");
-        assertThat(fieldType, instanceOf(GeoShapeFieldMapper.GeoShapeFieldType.class));
-
-        gsfm = (GeoShapeFieldMapper.GeoShapeFieldType)fieldType;
-        orientation = gsfm.orientation();
-        assertThat(orientation, equalTo(ShapeBuilder.Orientation.COUNTER_CLOCKWISE));
-        assertThat(orientation, equalTo(ShapeBuilder.Orientation.RIGHT));
-        assertThat(orientation, equalTo(ShapeBuilder.Orientation.CCW));
-    }
-
     public void testPointsOnly() throws Exception {
         String mapping = XContentFactory.jsonBuilder().startObject().startObject("type1")
                 .startObject("properties").startObject("location")
@@ -448,31 +408,25 @@ public class GeoShapeIntegrationIT extends ESIntegTestCase {
                 .endObject().endObject()
                 .endObject().endObject().string();
 
-        assertAcked(prepareCreate("geo_points_only").addMapping("type1", mapping));
+        client().admin().indices().prepareCreate("geo_points_only").addMapping("type1", mapping).execute().actionGet();
         ensureGreen();
 
         ShapeBuilder shape = RandomShapeGenerator.createShape(random());
         try {
-            index("geo_points_only", "type1", "1", jsonBuilder().startObject().field("location", shape).endObject());
+            client().prepareIndex("geo_points_only", "type1", "1")
+                    .setSource(jsonBuilder().startObject().field("location", shape).endObject())
+                    .setRefresh(true).execute().actionGet();
         } catch (MapperParsingException e) {
             // RandomShapeGenerator created something other than a POINT type, verify the correct exception is thrown
             assertThat(e.getCause().getMessage(), containsString("is configured for points only"));
             return;
         }
 
-        refresh();
         // test that point was inserted
-        SearchResponse response = client().prepareSearch()
+        SearchResponse response = client().prepareSearch("geo_points_only").setTypes("type1")
                 .setQuery(geoIntersectionQuery("location", shape))
                 .execute().actionGet();
 
         assertEquals(1, response.getHits().getTotalHits());
     }
-
-    private String findNodeName(String index) {
-        ClusterState state = client().admin().cluster().prepareState().get().getState();
-        IndexShardRoutingTable shard = state.getRoutingTable().index(index).shard(0);
-        String nodeId = shard.assignedShards().get(0).currentNodeId();
-        return state.getNodes().get(nodeId).name();
-    }
 }

+ 2 - 1
core/src/test/java/org/elasticsearch/test/geo/RandomShapeGenerator.java

@@ -40,6 +40,7 @@ import org.elasticsearch.common.geo.builders.PointBuilder;
 import org.elasticsearch.common.geo.builders.PointCollection;
 import org.elasticsearch.common.geo.builders.PolygonBuilder;
 import org.elasticsearch.common.geo.builders.ShapeBuilder;
+import org.elasticsearch.search.geo.GeoShapeQueryTests;
 import org.junit.Assert;
 
 import java.util.Random;
@@ -155,7 +156,7 @@ public class RandomShapeGenerator extends RandomGeoGenerator {
     /**
      * Creates a random shape useful for randomized testing, NOTE: exercise caution when using this to build random GeometryCollections
      * as creating a large random number of random shapes can result in massive resource consumption
-     * see: {@link org.elasticsearch.search.geo.GeoShapeIntegrationIT#testShapeFilterWithRandomGeoCollection}
+     * see: {@link GeoShapeQueryTests#testShapeFilterWithRandomGeoCollection}
      *
      * The following options are included
      * @param nearPoint Create a shape near a provided point

+ 15 - 2
docs/reference/query-dsl/geo-shape-query.asciidoc

@@ -50,7 +50,8 @@ The following query will find the point using the Elasticsearch's
                         "shape": {
                             "type": "envelope",
                             "coordinates" : [[13.0, 53.0], [14.0, 52.0]]
-                        }
+                        },
+                        "relation": "within"
                     }
                 }
             }
@@ -61,7 +62,7 @@ The following query will find the point using the Elasticsearch's
 
 ==== Pre-Indexed Shape
 
-The Filter also supports using a shape which has already been indexed in
+The Query also supports using a shape which has already been indexed in
 another index and/or index type. This is particularly useful for when
 you have a pre-defined list of shapes which are useful to your
 application and you want to reference this using a logical name (for
@@ -101,3 +102,15 @@ shape:
 }
 --------------------------------------------------
 
+==== Spatial Relations
+
+The Query supports the following spatial relations:
+
+* `INTERSECTS` - (default) Return all documents whose `geo_shape` field
+intersects the query geometry.
+* `DISJOINT` - Return all documents whose `geo_shape` field
+has nothing in common with the query geometry.
+* `WITHIN` - Return all documents whose `geo_shape` field
+is within the query geometry.
+* `CONTAINS` - Return all documents whose `geo_shape` field
+contains the query geometry.

+ 108 - 0
plugins/lang-groovy/src/test/java/org/elasticsearch/messy/tests/GeoShapeIntegrationTests.java

@@ -0,0 +1,108 @@
+/*
+ * 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.messy.tests;
+
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.routing.IndexShardRoutingTable;
+import org.elasticsearch.common.geo.builders.ShapeBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.index.IndexService;
+import org.elasticsearch.index.mapper.MappedFieldType;
+import org.elasticsearch.index.mapper.geo.GeoShapeFieldMapper;
+import org.elasticsearch.indices.IndicesService;
+import org.elasticsearch.plugins.Plugin;
+import org.elasticsearch.script.groovy.GroovyPlugin;
+import org.elasticsearch.test.ESIntegTestCase;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.instanceOf;
+
+/**
+ */
+public class GeoShapeIntegrationTests extends ESIntegTestCase {
+
+    @Override
+    protected Collection<Class<? extends Plugin>> nodePlugins() {
+        return Collections.singleton(GroovyPlugin.class);
+    }
+
+    /**
+     * Test that orientation parameter correctly persists across cluster restart
+     */
+    public void testOrientationPersistence() throws Exception {
+        String idxName = "orientation";
+        String mapping = XContentFactory.jsonBuilder().startObject().startObject("shape")
+                .startObject("properties").startObject("location")
+                .field("type", "geo_shape")
+                .field("orientation", "left")
+                .endObject().endObject()
+                .endObject().endObject().string();
+
+        // create index
+        assertAcked(prepareCreate(idxName).addMapping("shape", mapping));
+
+        mapping = XContentFactory.jsonBuilder().startObject().startObject("shape")
+                .startObject("properties").startObject("location")
+                .field("type", "geo_shape")
+                .field("orientation", "right")
+                .endObject().endObject()
+                .endObject().endObject().string();
+
+        assertAcked(prepareCreate(idxName+"2").addMapping("shape", mapping));
+        ensureGreen(idxName, idxName+"2");
+
+        internalCluster().fullRestart();
+        ensureGreen(idxName, idxName+"2");
+
+        // left orientation test
+        IndicesService indicesService = internalCluster().getInstance(IndicesService.class, findNodeName(idxName));
+        IndexService indexService = indicesService.indexService(idxName);
+        MappedFieldType fieldType = indexService.mapperService().smartNameFieldType("location");
+        assertThat(fieldType, instanceOf(GeoShapeFieldMapper.GeoShapeFieldType.class));
+
+        GeoShapeFieldMapper.GeoShapeFieldType gsfm = (GeoShapeFieldMapper.GeoShapeFieldType)fieldType;
+        ShapeBuilder.Orientation orientation = gsfm.orientation();
+        assertThat(orientation, equalTo(ShapeBuilder.Orientation.CLOCKWISE));
+        assertThat(orientation, equalTo(ShapeBuilder.Orientation.LEFT));
+        assertThat(orientation, equalTo(ShapeBuilder.Orientation.CW));
+
+        // right orientation test
+        indicesService = internalCluster().getInstance(IndicesService.class, findNodeName(idxName+"2"));
+        indexService = indicesService.indexService(idxName+"2");
+        fieldType = indexService.mapperService().smartNameFieldType("location");
+        assertThat(fieldType, instanceOf(GeoShapeFieldMapper.GeoShapeFieldType.class));
+
+        gsfm = (GeoShapeFieldMapper.GeoShapeFieldType)fieldType;
+        orientation = gsfm.orientation();
+        assertThat(orientation, equalTo(ShapeBuilder.Orientation.COUNTER_CLOCKWISE));
+        assertThat(orientation, equalTo(ShapeBuilder.Orientation.RIGHT));
+        assertThat(orientation, equalTo(ShapeBuilder.Orientation.CCW));
+    }
+
+    private String findNodeName(String index) {
+        ClusterState state = client().admin().cluster().prepareState().get().getState();
+        IndexShardRoutingTable shard = state.getRoutingTable().index(index).shard(0);
+        String nodeId = shard.assignedShards().get(0).currentNodeId();
+        return state.getNodes().get(nodeId).name();
+    }
+}