From c679dba438e411e10c00e98c1881c0af06597943 Mon Sep 17 00:00:00 2001 From: Laurent Canet Date: Sun, 13 Oct 2013 15:00:40 +0200 Subject: [PATCH] DATAMONGO-778 - Improved support for geospatial indexing with @GeoSpatialIndexed. We now support to create geospatial indices of type 2D sphere and geoHaystack using the @GeospatialIndexed annotation on fields. Original pull request #82, #104. --- .../core/index/GeoSpatialIndexType.java | 41 +++++ .../mongodb/core/index/GeoSpatialIndexed.java | 31 +++- .../mongodb/core/index/GeospatialIndex.java | 129 ++++++++++++-- .../MongoPersistentEntityIndexCreator.java | 3 + .../core/geo/GeoSpatialIndexTests.java | 164 ++++++++++++++++++ .../{IndexTests.java => IndexUnitTests.java} | 32 +++- 6 files changed, 380 insertions(+), 20 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexType.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java rename spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/{IndexTests.java => IndexUnitTests.java} (72%) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexType.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexType.java new file mode 100644 index 000000000..647d83d75 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexType.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed 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.springframework.data.mongodb.core.index; + +/** + * Geoposatial index type. + * + * @author Laurent Canet + * @author Oliver Gierke + * @since 1.4 + */ +public enum GeoSpatialIndexType { + + /** + * Simple 2-Dimensional index for legacy-format points. + */ + GEO_2D, + + /** + * 2D Index for GeoJSON-formatted data over a sphere. Only available in Mongo 2.4. + */ + GEO_2DSPHERE, + + /** + * An haystack index for grouping results over small results. + */ + GEO_HAYSTACK +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java index 484c0e653..d55fb783f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java @@ -1,11 +1,11 @@ /* - * Copyright (c) 2011 by the original author(s). + * Copyright 2010-2013 the original author or authors. * * Licensed 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 + * 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, @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.mongodb.core.index; import java.lang.annotation.ElementType; @@ -24,7 +23,8 @@ import java.lang.annotation.Target; /** * Mark a field to be indexed using MongoDB's geospatial indexing feature. * - * @author Jon Brisbin + * @author Jon Brisbin + * @author Laurent Canet */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @@ -65,4 +65,27 @@ public @interface GeoSpatialIndexed { */ int bits() default 26; + /** + * The type of the geospatial index. Default is {@link GeoSpatialIndexType#GEO_2D} + * + * @since 1.4 + * @return + */ + GeoSpatialIndexType type() default GeoSpatialIndexType.GEO_2D; + + /** + * The bucket size for {@link GeoSpatialIndexType#GEO_HAYSTACK} indexes, in coordinate units. + * + * @since 1.4 + * @return + */ + double bucketSize() default 1.0; + + /** + * The name of the additional field to use for {@link GeoSpatialIndexType#GEO_HAYSTACK} indexes + * + * @since 1.4 + * @return + */ + String additionalField() default ""; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java index b1b5bb5c8..8ddc70885 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2011 the original author or authors. + * Copyright 2010-2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core.index; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; @@ -25,14 +26,18 @@ import com.mongodb.DBObject; * * @author Jon Brisbin * @author Oliver Gierke + * @author Laurent Canet */ public class GeospatialIndex implements IndexDefinition { private final String field; private String name; - private Integer min = null; - private Integer max = null; - private Integer bits = null; + private Integer min; + private Integer max; + private Integer bits; + private GeoSpatialIndexType type = GeoSpatialIndexType.GEO_2D; + private Double bucketSize = 1.0; + private String additionalField; /** * Creates a new {@link GeospatialIndex} for the given field. @@ -40,52 +45,146 @@ public class GeospatialIndex implements IndexDefinition { * @param field must not be empty or {@literal null}. */ public GeospatialIndex(String field) { - Assert.hasText(field); + + Assert.hasText(field, "Field must have text!"); + this.field = field; } + /** + * @param name must not be {@literal null} or empty. + * @return + */ public GeospatialIndex named(String name) { + + Assert.hasText(name, "Name must have text!"); + this.name = name; return this; } + /** + * @param min + * @return + */ public GeospatialIndex withMin(int min) { this.min = Integer.valueOf(min); return this; } + /** + * @param max + * @return + */ public GeospatialIndex withMax(int max) { this.max = Integer.valueOf(max); return this; } + /** + * @param bits + * @return + */ public GeospatialIndex withBits(int bits) { this.bits = Integer.valueOf(bits); return this; } + /** + * @param type must not be {@literal null}. + * @return + */ + public GeospatialIndex typed(GeoSpatialIndexType type) { + + Assert.notNull(type, "Type must not be null!"); + + this.type = type; + return this; + } + + /** + * @param bucketSize + * @return + */ + public GeospatialIndex withBucketSize(double bucketSize) { + this.bucketSize = bucketSize; + return this; + } + + /** + * @param fieldName. + * @return + */ + public GeospatialIndex withAdditionalField(String fieldName) { + this.additionalField = fieldName; + return this; + } + public DBObject getIndexKeys() { + DBObject dbo = new BasicDBObject(); - dbo.put(field, "2d"); + + switch (type) { + + case GEO_2D: + dbo.put(field, "2d"); + break; + + case GEO_2DSPHERE: + dbo.put(field, "2dsphere"); + break; + + case GEO_HAYSTACK: + dbo.put(field, "geoHaystack"); + if (!StringUtils.hasText(additionalField)) { + throw new IllegalArgumentException("When defining geoHaystack index, an additionnal field must be defined"); + } + dbo.put(additionalField, 1); + break; + + default: + throw new IllegalArgumentException("Unsupported geospatial index " + type); + } + return dbo; } public DBObject getIndexOptions() { - if (name == null && min == null && max == null) { + + if (name == null && min == null && max == null && bucketSize == null) { return null; } + DBObject dbo = new BasicDBObject(); if (name != null) { dbo.put("name", name); } - if (min != null) { - dbo.put("min", min); - } - if (max != null) { - dbo.put("max", max); - } - if (bits != null) { - dbo.put("bits", bits); + + switch (type) { + + case GEO_2D: + + if (min != null) { + dbo.put("min", min); + } + if (max != null) { + dbo.put("max", max); + } + if (bits != null) { + dbo.put("bits", bits); + } + break; + + case GEO_2DSPHERE: + + break; + + case GEO_HAYSTACK: + + if (bucketSize != null) { + dbo.put("bucketSize", bucketSize); + } + break; } return dbo; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java index 97e909369..88ec0d3c8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java @@ -45,6 +45,7 @@ import com.mongodb.util.JSON; * @author Oliver Gierke * @author Philipp Schneider * @author Johno Crawford + * @author Laurent Canet */ public class MongoPersistentEntityIndexCreator implements ApplicationListener, MongoPersistentProperty>> { @@ -157,6 +158,8 @@ public class MongoPersistentEntityIndexCreator implements GeospatialIndex indexObject = new GeospatialIndex(persistentProperty.getFieldName()); indexObject.withMin(index.min()).withMax(index.max()); indexObject.named(StringUtils.hasText(index.name()) ? index.name() : field.getName()); + indexObject.typed(index.type()).withBucketSize(index.bucketSize()) + .withAdditionalField(index.additionalField()); String collection = StringUtils.hasText(index.collection()) ? index.collection() : entity.getCollection(); mongoDbFactory.getDb().getCollection(collection) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java new file mode 100644 index 000000000..9fa5a23d3 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java @@ -0,0 +1,164 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed 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.springframework.data.mongodb.core.geo; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.data.mongodb.config.AbstractIntegrationTests; +import org.springframework.data.mongodb.core.CollectionCallback; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.WriteResultChecking; +import org.springframework.data.mongodb.core.index.GeoSpatialIndexType; +import org.springframework.data.mongodb.core.index.GeoSpatialIndexed; + +import com.mongodb.DBCollection; +import com.mongodb.DBObject; +import com.mongodb.MongoException; +import com.mongodb.WriteConcern; + +/** + * Integration tests for geo-spatial indexing. + * + * @author Laurent Canet + * @author Oliver Gierke + */ +public class GeoSpatialIndexTests extends AbstractIntegrationTests { + + @Autowired private MongoTemplate template; + + @Before + public void setUp() { + + template.setWriteConcern(WriteConcern.FSYNC_SAFE); + template.setWriteResultChecking(WriteResultChecking.EXCEPTION); + } + + /** + * @see DATAMONGO-778 + */ + @Test + public void test2dIndex() { + + try { + template.save(new GeoSpatialEntity2D(45.2, 4.6)); + assertThat(hasIndexOfType(GeoSpatialEntity2D.class, "2d"), is(true)); + } finally { + template.dropCollection(GeoSpatialEntity2D.class); + } + } + + /** + * @see DATAMONGO-778 + */ + @Test + public void test2dSphereIndex() { + + try { + template.save(new GeoSpatialEntity2DSphere(45.2, 4.6)); + assertThat(hasIndexOfType(GeoSpatialEntity2DSphere.class, "2dsphere"), is(true)); + } finally { + template.dropCollection(GeoSpatialEntity2DSphere.class); + } + } + + /** + * @see DATAMONGO-778 + */ + @Test + public void testHaystackIndex() { + + try { + template.save(new GeoSpatialEntityHaystack(45.2, 4.6, "Paris")); + assertThat(hasIndexOfType(GeoSpatialEntityHaystack.class, "geoHaystack"), is(true)); + } finally { + template.dropCollection(GeoSpatialEntityHaystack.class); + } + } + + /** + * Returns whether an index with the given name exists for the given entity type. + * + * @param indexName + * @param entityType + * @return + */ + private boolean hasIndexOfType(Class entityType, final String type) { + + return template.execute(entityType, new CollectionCallback() { + + @SuppressWarnings("unchecked") + public Boolean doInCollection(DBCollection collection) throws MongoException, DataAccessException { + + for (DBObject indexInfo : collection.getIndexInfo()) { + + DBObject keys = (DBObject) indexInfo.get("key"); + Map keysMap = keys.toMap(); + + for (String key : keysMap.keySet()) { + Object indexType = keys.get(key); + if (type.equals(indexType)) { + return true; + } + } + } + + return false; + } + }); + } + + static class GeoSpatialEntity2D { + public String id; + @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2D) public Point location; + + public GeoSpatialEntity2D(double x, double y) { + this.location = new Point(x, y); + } + } + + static class GeoSpatialEntityHaystack { + public String id; + public String name; + @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_HAYSTACK, additionalField = "name") public Point location; + + public GeoSpatialEntityHaystack(double x, double y, String name) { + this.location = new Point(x, y); + this.name = name; + } + } + + static class GeoJsonPoint { + String type = "Point"; + double coordinates[]; + } + + static class GeoSpatialEntity2DSphere { + public String id; + @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE) public GeoJsonPoint location; + + public GeoSpatialEntity2DSphere(double x, double y) { + this.location = new GeoJsonPoint(); + this.location.coordinates = new double[] { x, y }; + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexUnitTests.java similarity index 72% rename from spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexTests.java rename to spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexUnitTests.java index e073a08d7..97384eb54 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexUnitTests.java @@ -20,11 +20,18 @@ import static org.junit.Assert.*; import org.junit.Test; import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.mongodb.core.index.GeoSpatialIndexType; import org.springframework.data.mongodb.core.index.GeospatialIndex; import org.springframework.data.mongodb.core.index.Index; import org.springframework.data.mongodb.core.index.Index.Duplicates; -public class IndexTests { +/** + * Unit tests for {@link Index}. + * + * @author Oliver Gierke + * @author Laurent Canet + */ +public class IndexUnitTests { @Test public void testWithAscendingIndex() { @@ -69,6 +76,29 @@ public class IndexTests { assertEquals("{ \"min\" : 0}", i.getIndexOptions().toString()); } + /** + * @see DATAMONGO-778 + */ + @Test + public void testGeospatialIndex2DSphere() { + + GeospatialIndex i = new GeospatialIndex("location").typed(GeoSpatialIndexType.GEO_2DSPHERE); + assertEquals("{ \"location\" : \"2dsphere\"}", i.getIndexKeys().toString()); + assertEquals("{ }", i.getIndexOptions().toString()); + } + + /** + * @see DATAMONGO-778 + */ + @Test + public void testGeospatialIndexGeoHaystack() { + + GeospatialIndex i = new GeospatialIndex("location").typed(GeoSpatialIndexType.GEO_HAYSTACK) + .withAdditionalField("name").withBucketSize(40); + assertEquals("{ \"location\" : \"geoHaystack\" , \"name\" : 1}", i.getIndexKeys().toString()); + assertEquals("{ \"bucketSize\" : 40.0}", i.getIndexOptions().toString()); + } + @Test public void ensuresPropertyOrder() {