Browse Source

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.
pull/109/head
Laurent Canet 12 years ago committed by Oliver Gierke
parent
commit
c679dba438
  1. 41
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexType.java
  2. 31
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java
  3. 129
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java
  4. 3
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java
  5. 164
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java
  6. 32
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexUnitTests.java

41
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
}

31
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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * 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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.data.mongodb.core.index; package org.springframework.data.mongodb.core.index;
import java.lang.annotation.ElementType; 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. * Mark a field to be indexed using MongoDB's geospatial indexing feature.
* *
* @author Jon Brisbin <jbrisbin@vmware.com> * @author Jon Brisbin
* @author Laurent Canet
*/ */
@Target(ElementType.FIELD) @Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@ -65,4 +65,27 @@ public @interface GeoSpatialIndexed {
*/ */
int bits() default 26; 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 "";
} }

129
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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; package org.springframework.data.mongodb.core.index;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.mongodb.BasicDBObject; import com.mongodb.BasicDBObject;
import com.mongodb.DBObject; import com.mongodb.DBObject;
@ -25,14 +26,18 @@ import com.mongodb.DBObject;
* *
* @author Jon Brisbin * @author Jon Brisbin
* @author Oliver Gierke * @author Oliver Gierke
* @author Laurent Canet
*/ */
public class GeospatialIndex implements IndexDefinition { public class GeospatialIndex implements IndexDefinition {
private final String field; private final String field;
private String name; private String name;
private Integer min = null; private Integer min;
private Integer max = null; private Integer max;
private Integer bits = null; 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. * 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}. * @param field must not be empty or {@literal null}.
*/ */
public GeospatialIndex(String field) { public GeospatialIndex(String field) {
Assert.hasText(field);
Assert.hasText(field, "Field must have text!");
this.field = field; this.field = field;
} }
/**
* @param name must not be {@literal null} or empty.
* @return
*/
public GeospatialIndex named(String name) { public GeospatialIndex named(String name) {
Assert.hasText(name, "Name must have text!");
this.name = name; this.name = name;
return this; return this;
} }
/**
* @param min
* @return
*/
public GeospatialIndex withMin(int min) { public GeospatialIndex withMin(int min) {
this.min = Integer.valueOf(min); this.min = Integer.valueOf(min);
return this; return this;
} }
/**
* @param max
* @return
*/
public GeospatialIndex withMax(int max) { public GeospatialIndex withMax(int max) {
this.max = Integer.valueOf(max); this.max = Integer.valueOf(max);
return this; return this;
} }
/**
* @param bits
* @return
*/
public GeospatialIndex withBits(int bits) { public GeospatialIndex withBits(int bits) {
this.bits = Integer.valueOf(bits); this.bits = Integer.valueOf(bits);
return this; 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() { public DBObject getIndexKeys() {
DBObject dbo = new BasicDBObject(); 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; return dbo;
} }
public DBObject getIndexOptions() { public DBObject getIndexOptions() {
if (name == null && min == null && max == null) {
if (name == null && min == null && max == null && bucketSize == null) {
return null; return null;
} }
DBObject dbo = new BasicDBObject(); DBObject dbo = new BasicDBObject();
if (name != null) { if (name != null) {
dbo.put("name", name); dbo.put("name", name);
} }
if (min != null) {
dbo.put("min", min); switch (type) {
}
if (max != null) { case GEO_2D:
dbo.put("max", max);
} if (min != null) {
if (bits != null) { dbo.put("min", min);
dbo.put("bits", bits); }
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; return dbo;
} }

3
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 Oliver Gierke
* @author Philipp Schneider * @author Philipp Schneider
* @author Johno Crawford * @author Johno Crawford
* @author Laurent Canet
*/ */
public class MongoPersistentEntityIndexCreator implements public class MongoPersistentEntityIndexCreator implements
ApplicationListener<MappingContextEvent<MongoPersistentEntity<?>, MongoPersistentProperty>> { ApplicationListener<MappingContextEvent<MongoPersistentEntity<?>, MongoPersistentProperty>> {
@ -157,6 +158,8 @@ public class MongoPersistentEntityIndexCreator implements
GeospatialIndex indexObject = new GeospatialIndex(persistentProperty.getFieldName()); GeospatialIndex indexObject = new GeospatialIndex(persistentProperty.getFieldName());
indexObject.withMin(index.min()).withMax(index.max()); indexObject.withMin(index.min()).withMax(index.max());
indexObject.named(StringUtils.hasText(index.name()) ? index.name() : field.getName()); 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(); String collection = StringUtils.hasText(index.collection()) ? index.collection() : entity.getCollection();
mongoDbFactory.getDb().getCollection(collection) mongoDbFactory.getDb().getCollection(collection)

164
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<Boolean>() {
@SuppressWarnings("unchecked")
public Boolean doInCollection(DBCollection collection) throws MongoException, DataAccessException {
for (DBObject indexInfo : collection.getIndexInfo()) {
DBObject keys = (DBObject) indexInfo.get("key");
Map<String, Object> 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 };
}
}
}

32
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexTests.java → 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.junit.Test;
import org.springframework.data.domain.Sort.Direction; 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.GeospatialIndex;
import org.springframework.data.mongodb.core.index.Index; import org.springframework.data.mongodb.core.index.Index;
import org.springframework.data.mongodb.core.index.Index.Duplicates; 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 @Test
public void testWithAscendingIndex() { public void testWithAscendingIndex() {
@ -69,6 +76,29 @@ public class IndexTests {
assertEquals("{ \"min\" : 0}", i.getIndexOptions().toString()); 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 @Test
public void ensuresPropertyOrder() { public void ensuresPropertyOrder() {
Loading…
Cancel
Save