From 7e74ec6b62ef14136d002ca4a11e053804c20233 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 3 Mar 2015 10:29:43 +0100 Subject: [PATCH] DATAMONGO-1110 - Add support for $minDistance. We now support $minDistance for NearQuery and Criteria. Please keep in mind that minDistance is only available for MongoDB 2.6 and better and can only be combined with $near or $nearSphere operator depending on the defined index type. Usage of $minDistance with NearQuery is only possible when a 2dsphere index is present. We also make sure $minDistance operator gets correctly nested when using GeoJSON types. It is now possible to use a Range parameter within the repository queries. This allows to define near queries like: findByLocationNear(Point point, Range distances); The lower bound of the range is treated as the minimum distance while the upper one defines the maximum distance from the given point. In case a Distance parameter is provided it will serve as maxDistance. Original pull request: #277. --- .../data/mongodb/core/query/Criteria.java | 29 +++++-- .../data/mongodb/core/query/NearQuery.java | 76 ++++++++++++++++++- .../repository/query/AbstractMongoQuery.java | 5 ++ .../query/ConvertingParameterAccessor.java | 12 ++- .../query/MongoParameterAccessor.java | 10 ++- .../repository/query/MongoParameters.java | 64 ++++++++++++++-- .../MongoParametersParameterAccessor.java | 12 ++- .../repository/query/MongoQueryCreator.java | 20 ++++- .../data/mongodb/core/geo/GeoJsonTests.java | 39 ++++++++++ .../core/geo/GeoSpatial2DSphereTests.java | 32 +++++++- .../mongodb/core/geo/GeoSpatial2DTests.java | 10 +++ .../mongodb/core/query/CriteriaTests.java | 36 +++++++++ ...tractPersonRepositoryIntegrationTests.java | 15 ++++ .../data/mongodb/repository/Person.java | 6 +- .../mongodb/repository/PersonRepository.java | 3 + ...oParametersParameterAccessorUnitTests.java | 23 +++++- .../query/MongoParametersUnitTests.java | 33 +++++++- .../query/MongoQueryCreatorUnitTests.java | 57 ++++++++++++++ .../query/StubParameterAccessor.java | 16 +++- .../reference/mongo-repositories.adoc | 15 ++++ src/main/asciidoc/reference/mongodb.adoc | 10 ++- 21 files changed, 494 insertions(+), 29 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java index 38725ff4f..8b9ddad87 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java @@ -441,7 +441,8 @@ public class Criteria implements CriteriaDefinition { */ public Criteria maxDistance(double maxDistance) { - if (createNearCriteriaForCommand("$near", maxDistance) || createNearCriteriaForCommand("$nearSphere", maxDistance)) { + if (createNearCriteriaForCommand("$near", "$maxDistance", maxDistance) + || createNearCriteriaForCommand("$nearSphere", "$maxDistance", maxDistance)) { return this; } @@ -449,6 +450,25 @@ public class Criteria implements CriteriaDefinition { return this; } + /** + * Creates a geospatial criterion using a {@literal $minDistance} operation, for use with {@literal $near} or + * {@literal $nearSphere}. + * + * @param minDistance + * @return + * @since 1.7 + */ + public Criteria minDistance(double minDistance) { + + if (createNearCriteriaForCommand("$near", "$minDistance", minDistance) + || createNearCriteriaForCommand("$nearSphere", "$minDistance", minDistance)) { + return this; + } + + criteria.put("$minDistance", minDistance); + return this; + } + /** * Creates a criterion using the {@literal $elemMatch} operator * @@ -605,7 +625,7 @@ public class Criteria implements CriteriaDefinition { } } - private boolean createNearCriteriaForCommand(String command, double maxDistance) { + private boolean createNearCriteriaForCommand(String command, String operation, double maxDistance) { if (!criteria.containsKey(command)) { return false; @@ -615,14 +635,13 @@ public class Criteria implements CriteriaDefinition { if (existingNearOperationValue instanceof DBObject) { - ((DBObject) existingNearOperationValue).put("$maxDistance", maxDistance); + ((DBObject) existingNearOperationValue).put(operation, maxDistance); return true; } else if (existingNearOperationValue instanceof GeoJson) { - BasicDBObject dbo = new BasicDBObject("$geometry", existingNearOperationValue) - .append("$maxDistance", maxDistance); + BasicDBObject dbo = new BasicDBObject("$geometry", existingNearOperationValue).append(operation, maxDistance); criteria.put(command, dbo); return true; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java index 3a96914bc..b847d192e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2014 the original author or authors. + * Copyright 2011-2015 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. @@ -40,6 +40,7 @@ public final class NearQuery { private final Point point; private Query query; private Distance maxDistance; + private Distance minDistance; private Metric metric; private boolean spherical; private Integer num; @@ -211,6 +212,63 @@ public final class NearQuery { return this; } + /** + * Sets the minimum distance results shall have from the configured origin. If a {@link Metric} was set before the + * given value will be interpreted as being a value in that metric. E.g. + * + *
+	 * NearQuery query = near(10.0, 20.0, Metrics.KILOMETERS).minDistance(150);
+	 * 
+ * + * Will set the minimum distance to 150 kilometers. + * + * @param minDistance + * @return + * @since 1.7 + */ + public NearQuery minDistance(double minDistance) { + return minDistance(new Distance(minDistance, getMetric())); + } + + /** + * Sets the minimum distance supplied in a given metric. Will normalize the distance but not reconfigure the query's + * result {@link Metric} if one was configured before. + * + * @param minDistance + * @param metric must not be {@literal null}. + * @return + * @since 1.7 + */ + public NearQuery minDistance(double minDistance, Metric metric) { + + Assert.notNull(metric); + return minDistance(new Distance(minDistance, metric)); + } + + /** + * Sets the minimum distance to the given {@link Distance}. Will set the returned {@link Metric} to be the one of the + * given {@link Distance} if no {@link Metric} was set before. + * + * @param distance must not be {@literal null}. + * @return + * @since 1.7 + */ + public NearQuery minDistance(Distance distance) { + + Assert.notNull(distance); + + if (distance.getMetric() != Metrics.NEUTRAL) { + this.spherical(true); + } + + if (this.metric == null) { + in(distance.getMetric()); + } + + this.minDistance = distance; + return this; + } + /** * Returns the maximum {@link Distance}. * @@ -220,6 +278,16 @@ public final class NearQuery { return this.maxDistance; } + /** + * Returns the maximum {@link Distance}. + * + * @return + * @since 1.7 + */ + public Distance getMinDistance() { + return this.minDistance; + } + /** * Configures a {@link CustomMetric} with the given multiplier. * @@ -352,7 +420,11 @@ public final class NearQuery { } if (maxDistance != null) { - dbObject.put("maxDistance", this.maxDistance.getNormalizedValue()); + dbObject.put("maxDistance", maxDistance.getNormalizedValue()); + } + + if (minDistance != null) { + dbObject.put("minDistance", minDistance.getNormalizedValue()); } if (metric != null) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java index 425363f1a..69c78b584 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java @@ -366,6 +366,11 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { nearQuery.maxDistance(maxDistance).in(maxDistance.getMetric()); } + Distance minDistance = accessor.getMinDistance(); + if (minDistance != null) { + nearQuery.minDistance(minDistance).in(minDistance.getMetric()); + } + Pageable pageable = accessor.getPageable(); if (pageable != null) { nearQuery.with(pageable); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java index c59b74be8..b52f97f4e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2014 the original author or authors. + * Copyright 2011-2015 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. @@ -104,6 +104,15 @@ public class ConvertingParameterAccessor implements MongoParameterAccessor { return delegate.getMaxDistance(); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getMinDistance() + */ + @Override + public Distance getMinDistance() { + return delegate.getMinDistance(); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.repository.MongoParameterAccessor#getGeoNearLocation() @@ -252,4 +261,5 @@ public class ConvertingParameterAccessor implements MongoParameterAccessor { */ Object nextConverted(MongoPersistentProperty property); } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java index 51b0f4d1c..9b107857e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2014 the original author or authors. + * Copyright 2011-2015 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. @@ -50,4 +50,12 @@ public interface MongoParameterAccessor extends ParameterAccessor { * @since 1.6 */ TextCriteria getFullText(); + + /** + * Returns a {@link Distance} to be applied to {@literal $minDistance} for MongoDB geo queries. + * + * @return + * @since 1.7 + */ + Distance getMinDistance(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java index debaab3da..bbd52d278 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2014 the original author or authors. + * Copyright 2011-2015 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. @@ -36,7 +36,8 @@ import org.springframework.data.repository.query.Parameters; */ public class MongoParameters extends Parameters { - private final Integer distanceIndex; + private final Integer minDistanceIndex; + private final Integer maxDistanceIndex; private final Integer fullTextIndex; private Integer nearIndex; @@ -51,9 +52,13 @@ public class MongoParameters extends Parameters super(method); List> parameterTypes = Arrays.asList(method.getParameterTypes()); - this.distanceIndex = parameterTypes.indexOf(Distance.class); + this.fullTextIndex = parameterTypes.indexOf(TextCriteria.class); + int[] distances = distances(parameterTypes); + this.minDistanceIndex = distances[0]; + this.maxDistanceIndex = distances[1]; + if (this.nearIndex == null && isGeoNearMethod) { this.nearIndex = getNearIndex(parameterTypes); } else if (this.nearIndex == null) { @@ -62,13 +67,14 @@ public class MongoParameters extends Parameters } private MongoParameters(List parameters, Integer distanceIndex, Integer nearIndex, - Integer fullTextIndex) { + Integer fullTextIndex, Integer minDistanceIndex) { super(parameters); - this.distanceIndex = distanceIndex; + this.maxDistanceIndex = distanceIndex; this.nearIndex = nearIndex; this.fullTextIndex = fullTextIndex; + this.minDistanceIndex = minDistanceIndex; } private final int getNearIndex(List> parameterTypes) { @@ -115,9 +121,21 @@ public class MongoParameters extends Parameters * Returns the index of a {@link Distance} parameter to be used for geo queries. * * @return + * @deprecated since 1.7. Please use {@link #getMaxDistanceParameterIndex()} instead. */ + @Deprecated public int getDistanceIndex() { - return distanceIndex; + return getMaxDistanceParameterIndex(); + } + + /** + * Returns the index of the {@link Distance} parameter to be used for max distance in geo queries. + * + * @return + * @since 1.7 + */ + public int getMaxDistanceParameterIndex() { + return maxDistanceIndex; } /** @@ -147,13 +165,45 @@ public class MongoParameters extends Parameters return this.fullTextIndex != null && this.fullTextIndex.intValue() >= 0; } + /** + * @return + * @since 1.7 + */ + public boolean hasMinDistanceParameter() { + return minDistanceIndex != null && minDistanceIndex.intValue() >= 0; + } + + /** + * @return + * @since 1.7 + */ + public int getMinDistanceParameterIndex() { + return minDistanceIndex != null ? minDistanceIndex.intValue() : -1; + } + /* * (non-Javadoc) * @see org.springframework.data.repository.query.Parameters#createFrom(java.util.List) */ @Override protected MongoParameters createFrom(List parameters) { - return new MongoParameters(parameters, this.distanceIndex, this.nearIndex, this.fullTextIndex); + return new MongoParameters(parameters, this.maxDistanceIndex, this.nearIndex, this.fullTextIndex, + this.minDistanceIndex); + } + + private int[] distances(List> paramTypes) { + + int maxDistance = paramTypes.lastIndexOf(Distance.class); + if (maxDistance == -1) { + return new int[] { -1, -1 }; + } + + int minDistance = paramTypes.indexOf(Distance.class); + if (minDistance == maxDistance) { + return new int[] { -1, maxDistance }; + } + + return new int[] { minDistance, maxDistance }; } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java index df74089fd..a38664ec3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2014 the original author or authors. + * Copyright 2011-2015 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. @@ -53,6 +53,16 @@ public class MongoParametersParameterAccessor extends ParametersParameterAccesso return index == -1 ? null : (Distance) getValue(index); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getMinDistance() + */ + @Override + public Distance getMinDistance() { + int index = method.getParameters().getMinDistanceParameterIndex(); + return index == -1 ? null : (Distance) getValue(index); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.repository.MongoParameterAccessor#getGeoNearLocation() diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index 83dd74324..aa4fb9375 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2014 the original author or authors. + * Copyright 2010-2015 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. @@ -30,6 +30,8 @@ import org.springframework.data.geo.Point; import org.springframework.data.geo.Shape; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.PersistentPropertyPath; +import org.springframework.data.mongodb.core.index.GeoSpatialIndexType; +import org.springframework.data.mongodb.core.index.GeoSpatialIndexed; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.CriteriaDefinition; @@ -206,18 +208,24 @@ class MongoQueryCreator extends AbstractQueryCreator { case NEAR: Distance distance = accessor.getMaxDistance(); + Distance minDistance = accessor.getMinDistance(); Point point = accessor.getGeoNearLocation(); point = point == null ? nextAs(parameters, Point.class) : point; + boolean isSpherical = isSpherical(property); + if (distance == null) { - return criteria.near(point); + return isSpherical ? criteria.nearSphere(point) : criteria.near(point); } else { - if (!Metrics.NEUTRAL.equals(distance.getMetric())) { + if (isSpherical || !Metrics.NEUTRAL.equals(distance.getMetric())) { criteria.nearSphere(point); } else { criteria.near(point); } criteria.maxDistance(distance.getNormalizedValue()); + if (minDistance != null) { + criteria.minDistance(minDistance.getNormalizedValue()); + } } return criteria; case WITHIN: @@ -393,4 +401,10 @@ class MongoQueryCreator extends AbstractQueryCreator { return source.replaceAll("\\*", ".*"); } + + private boolean isSpherical(MongoPersistentProperty property) { + + GeoSpatialIndexed index = property.findAnnotation(GeoSpatialIndexed.class); + return index != null && index.type().equals(GeoSpatialIndexType.GEO_2DSPHERE); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java index 0d97e73d5..01d961167 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java @@ -278,6 +278,45 @@ public class GeoJsonTests { assertThat(result.geoJsonGeometryCollection, equalTo(obj.geoJsonGeometryCollection)); } + /** + * @see DATAMONGO-1110 + */ + @Test + public void nearWithMinDistance() { + + Point point = new GeoJsonPoint(-73.99171, 40.738868); + List venues = template.find(query(where("location").near(point).minDistance(0.01)), + Venue2DSphere.class); + + assertThat(venues.size(), is(11)); + } + + /** + * @see DATAMONGO-1110 + */ + @Test + public void nearSphereWithMinDistance() { + + Point point = new GeoJsonPoint(-73.99171, 40.738868); + List venues = template.find(query(where("location").nearSphere(point).minDistance(0.01)), + Venue2DSphere.class); + + assertThat(venues.size(), is(11)); + } + + /** + * @see DATAMONGO-1135 + */ + @Test + public void nearWithMinAndMaxDistance() { + + GeoJsonPoint point = new GeoJsonPoint(-73.99171, 40.738868); + + Query query = query(where("location").near(point).minDistance(0.01).maxDistance(100)); + List venues = template.find(query, Venue2DSphere.class); + assertThat(venues.size(), is(2)); + } + private void addVenues() { template.insert(new Venue2DSphere("Penn Station", -73.99408, 40.75057)); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DSphereTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DSphereTests.java index 086397067..a51bd46b9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DSphereTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DSphereTests.java @@ -18,17 +18,24 @@ package org.springframework.data.mongodb.core.geo; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.query.Query.*; import java.util.List; import org.junit.Test; import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.geo.GeoResults; +import org.springframework.data.geo.Metric; +import org.springframework.data.geo.Metrics; +import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.IndexOperations; import org.springframework.data.mongodb.core.Venue; import org.springframework.data.mongodb.core.index.GeoSpatialIndexType; import org.springframework.data.mongodb.core.index.GeospatialIndex; import org.springframework.data.mongodb.core.index.IndexField; import org.springframework.data.mongodb.core.index.IndexInfo; +import org.springframework.data.mongodb.core.query.NearQuery; /** * @author Christoph Strobl @@ -55,6 +62,30 @@ public class GeoSpatial2DSphereTests extends AbstractGeoSpatialTests { assertThat(fields, hasItem(IndexField.geo("location"))); } + /** + * @see DATAMONGO-1110 + */ + @Test + public void geoNearWithMinDistance() { + + NearQuery geoNear = NearQuery.near(-73, 40, Metrics.KILOMETERS).num(10).minDistance(1); + + GeoResults result = template.geoNear(geoNear, Venue.class); + + assertThat(result.getContent().size(), is(not(0))); + assertThat(result.getAverageDistance().getMetric(), is((Metric) Metrics.KILOMETERS)); + } + + /** + * @see DATAMONGO-1110 + */ + @Test + public void nearSphereWithMinDistance() { + Point point = new Point(-73.99171, 40.738868); + List venues = template.find(query(where("location").nearSphere(point).minDistance(0.01)), Venue.class); + assertThat(venues.size(), is(1)); + } + @Override protected void createIndex() { template.indexOps(Venue.class).ensureIndex(new GeospatialIndex("location").typed(GeoSpatialIndexType.GEO_2DSPHERE)); @@ -64,5 +95,4 @@ public class GeoSpatial2DSphereTests extends AbstractGeoSpatialTests { protected void dropIndex() { template.indexOps(Venue.class).dropIndex("location_2dsphere"); } - } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DTests.java index e9248f63b..f72ab83cc 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DTests.java @@ -70,6 +70,16 @@ public class GeoSpatial2DTests extends AbstractGeoSpatialTests { assertThat(fields, hasItem(IndexField.geo("location"))); } + /** + * @see DATAMONGO-1110 + */ + @Test + public void nearPointWithMinDistance() { + Point point = new Point(-73.99171, 40.738868); + List venues = template.find(query(where("location").near(point).minDistance(0.01)), Venue.class); + assertThat(venues.size(), is(5)); + } + @Override protected void createIndex() { template.indexOps(Venue.class).ensureIndex(new GeospatialIndex("location").typed(GeoSpatialIndexType.GEO_2D)); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/CriteriaTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/CriteriaTests.java index e42cd5365..4244af9d2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/CriteriaTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/CriteriaTests.java @@ -211,4 +211,40 @@ public class CriteriaTests { assertThat(dbo, IsBsonObject.isBsonObject().containing("foo.$nearSphere.$maxDistance", 50D)); } + + /** + * @see DATAMONGO-1110 + */ + @Test + public void minDistanceShouldBeMappedInsideNearWhenUsedAlongWithGeoJsonType() { + + DBObject dbo = new Criteria("foo").near(new GeoJsonPoint(100, 200)).minDistance(50D).getCriteriaObject(); + + assertThat(dbo, IsBsonObject.isBsonObject().containing("foo.$near.$minDistance", 50D)); + } + + /** + * @see DATAMONGO-1110 + */ + @Test + public void minDistanceShouldBeMappedInsideNearSphereWhenUsedAlongWithGeoJsonType() { + + DBObject dbo = new Criteria("foo").nearSphere(new GeoJsonPoint(100, 200)).minDistance(50D).getCriteriaObject(); + + assertThat(dbo, IsBsonObject.isBsonObject().containing("foo.$nearSphere.$minDistance", 50D)); + } + + /** + * @see DATAMONGO-1110 + */ + @Test + public void minAndMaxDistanceShouldBeMappedInsideNearSphereWhenUsedAlongWithGeoJsonType() { + + DBObject dbo = new Criteria("foo").nearSphere(new GeoJsonPoint(100, 200)).minDistance(50D).maxDistance(100D) + .getCriteriaObject(); + + assertThat(dbo, IsBsonObject.isBsonObject().containing("foo.$nearSphere.$minDistance", 50D)); + assertThat(dbo, IsBsonObject.isBsonObject().containing("foo.$nearSphere.$maxDistance", 100D)); + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 7cdf22bcf..576f8eee3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -1165,4 +1165,19 @@ public abstract class AbstractPersonRepositoryIntegrationTests { result.close(); } } + + /** + * @see DATAMONGO-1110 + */ + @Test + public void executesGeoNearQueryForResultsCorrectlyWhenGivenMinAndMaxDistance() { + + Point point = new Point(-73.99171, 40.738868); + dave.setLocation(point); + repository.save(dave); + + GeoResults results = repository.findPersonByLocationNear(new Point(-73.99, 40.73), new Distance(0.01, + Metrics.KILOMETERS), new Distance(2000, Metrics.KILOMETERS)); + assertThat(results.getContent().isEmpty(), is(false)); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java index 1b47b2b68..4c165f739 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2013 the original author or authors. + * Copyright 2010-2015 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. @@ -21,6 +21,7 @@ import java.util.List; import java.util.Set; import org.springframework.data.geo.Point; +import org.springframework.data.mongodb.core.index.GeoSpatialIndexType; import org.springframework.data.mongodb.core.index.GeoSpatialIndexed; import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.mapping.DBRef; @@ -32,6 +33,7 @@ import org.springframework.data.mongodb.core.mapping.Field; * * @author Oliver Gierke * @author Thomas Darimont + * @author Christoph Strobl */ @Document public class Person extends Contact { @@ -49,7 +51,7 @@ public class Person extends Contact { List skills; - @GeoSpatialIndexed private Point location; + @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE) private Point location; private @Field("add") Address address; private Set
shippingAddresses; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index 9d2ee73ee..8004ca949 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -169,6 +169,9 @@ public interface PersonRepository extends MongoRepository, Query GeoResults findByLocationNear(Point point, Distance maxDistance); + /** @see DATAMONGO-1110 */ + GeoResults findPersonByLocationNear(Point point, Distance maxDistance, Distance minDistance); + GeoPage findByLocationNear(Point point, Distance maxDistance, Pageable pageable); List findByCreator(User user); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java index 7b089b044..1a9aca5b0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2014 the original author or authors. + * Copyright 2011-2015 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. @@ -96,12 +96,33 @@ public class MongoParametersParameterAccessorUnitTests { equalTo("{ \"$text\" : { \"$search\" : \"data\"}}")); } + /** + * @see DATAMONGO-1110 + */ + @Test + public void shouldDetectMinAndMaxDistance() throws NoSuchMethodException, SecurityException { + + Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class, Distance.class); + MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, context); + + Distance min = new Distance(10, Metrics.KILOMETERS); + Distance max = new Distance(20, Metrics.KILOMETERS); + + MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod, new Object[] { + new Point(10, 20), min, max }); + + assertThat(accessor.getMinDistance(), is(min)); + assertThat(accessor.getMaxDistance(), is(max)); + } + interface PersonRepository extends Repository { List findByLocationNear(Point point); List findByLocationNear(Point point, Distance distance); + List findByLocationNear(Point point, Distance minDistance, Distance maxDistance); + List findByFirstname(String firstname, TextCriteria fullText); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java index 2213bc684..6b9fb6eca 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2014 the original author or authors. + * Copyright 2011-2015 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. @@ -50,7 +50,7 @@ public class MongoParametersUnitTests { MongoParameters parameters = new MongoParameters(method, false); assertThat(parameters.getNumberOfParameters(), is(2)); - assertThat(parameters.getDistanceIndex(), is(1)); + assertThat(parameters.getMaxDistanceParameterIndex(), is(1)); assertThat(parameters.getBindableParameters().getNumberOfParameters(), is(1)); Parameter parameter = parameters.getParameter(1); @@ -121,6 +121,33 @@ public class MongoParametersUnitTests { assertThat(parameters.getParameter(parameters.getFullTextParameterIndex()).isSpecialParameter(), is(true)); } + /** + * @see DATAMONGO-1110 + */ + @Test + public void shouldFindMinAndMaxDistanceParameters() throws NoSuchMethodException, SecurityException { + + Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class, Distance.class); + MongoParameters parameters = new MongoParameters(method, false); + + assertThat(parameters.getMinDistanceParameterIndex(), is(1)); + assertThat(parameters.getMaxDistanceParameterIndex(), is(2)); + } + + /** + * @see DATAMONGO-1110 + */ + @Test + public void shouldNotHaveMinDistanceIfOnlyOneDistanceParameterPresent() throws NoSuchMethodException, + SecurityException { + + Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class); + MongoParameters parameters = new MongoParameters(method, false); + + assertThat(parameters.getMinDistanceParameterIndex(), is(-1)); + assertThat(parameters.getMaxDistanceParameterIndex(), is(1)); + } + interface PersonRepository { List findByLocationNear(Point point, Distance distance); @@ -136,5 +163,7 @@ public class MongoParametersUnitTests { GeoResults validDoubleArrays(double[] first, @Near double[] second); List findByNameAndText(String name, TextCriteria text); + + List findByLocationNear(Point point, Distance min, Distance max); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java index 769dfd3d4..6b8d6a3f1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java @@ -45,6 +45,8 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.Person; import org.springframework.data.mongodb.core.Venue; import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.index.GeoSpatialIndexType; +import org.springframework.data.mongodb.core.index.GeoSpatialIndexed; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; @@ -503,6 +505,54 @@ public class MongoQueryCreatorUnitTests { assertThat(query, is(query(where("address.geo").within(shape)))); } + /** + * @see DATAMONGO-1110 + */ + @Test + public void shouldCreateNearSphereQueryForSphericalProperty() { + + Point point = new Point(10, 20); + + PartTree tree = new PartTree("findByAddress2dSphere_GeoNear", User.class); + MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, point), context); + Query query = creator.createQuery(); + + assertThat(query, is(query(where("address2dSphere.geo").nearSphere(point)))); + } + + /** + * @see DATAMONGO-1110 + */ + @Test + public void shouldCreateNearSphereQueryForSphericalPropertyHavingDistanceWithDefaultMetric() { + + Point point = new Point(1.0, 1.0); + Distance distance = new Distance(1.0); + + PartTree tree = new PartTree("findByAddress2dSphere_GeoNear", User.class); + MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, point, distance), context); + Query query = creator.createQuery(); + + assertThat(query, is(query(where("address2dSphere.geo").nearSphere(point).maxDistance(1.0)))); + } + + /** + * @see DATAMONGO-1110 + */ + @Test + public void shouldCreateNearQueryForMinMaxDistance() { + + Point point = new Point(10, 20); + Distance min = new Distance(10); + Distance max = new Distance(20); + + PartTree tree = new PartTree("findByAddress_GeoNear", User.class); + MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, point, min, max), context); + Query query = creator.createQuery(); + + assertThat(query, is(query(where("address.geo").near(point).minDistance(10D).maxDistance(20D)))); + } + interface PersonRepository extends Repository { List findByLocationNearAndFirstname(Point location, Distance maxDistance, String firstname); @@ -517,6 +567,8 @@ public class MongoQueryCreatorUnitTests { List emailAddresses; Address address; + + Address2dSphere address2dSphere; } static class Address { @@ -524,4 +576,9 @@ public class MongoQueryCreatorUnitTests { String street; Point geo; } + + static class Address2dSphere { + String street; + @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE) Point geo; + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java index 72986a0f3..a5909af80 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2014 the original author or authors. + * Copyright 2011-2015 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. @@ -30,11 +30,13 @@ import org.springframework.data.repository.query.ParameterAccessor; * Simple {@link ParameterAccessor} that returns the given parameters unfiltered. * * @author Oliver Gierke + * @author Christoh Strobl */ class StubParameterAccessor implements MongoParameterAccessor { private final Object[] values; private Distance distance; + private Distance minDistance; /** * Creates a new {@link ConvertingParameterAccessor} backed by a {@link StubParameterAccessor} simply returning the @@ -54,7 +56,12 @@ class StubParameterAccessor implements MongoParameterAccessor { for (Object value : values) { if (value instanceof Distance) { - this.distance = (Distance) value; + if (this.distance == null) { + this.distance = (Distance) value; + } else { + this.minDistance = this.distance; + this.distance = (Distance) value; + } } } } @@ -99,6 +106,11 @@ class StubParameterAccessor implements MongoParameterAccessor { return distance; } + @Override + public Distance getMinDistance() { + return minDistance; + } + /* * (non-Javadoc) * @see org.springframework.data.repository.query.ParameterAccessor#iterator() diff --git a/src/main/asciidoc/reference/mongo-repositories.adoc b/src/main/asciidoc/reference/mongo-repositories.adoc index 45c5508d2..6f334624b 100644 --- a/src/main/asciidoc/reference/mongo-repositories.adoc +++ b/src/main/asciidoc/reference/mongo-repositories.adoc @@ -210,6 +210,14 @@ NOTE: Note that for version 1.0 we currently don't support referring to paramete | `findByLocationNear(Point point)` | `{"location" : {"$near" : [x,y]}}` +| `Near` +| `findByLocationNear(Point point, Distance max)` +| `{"location" : {"$near" : [x,y], "$maxDistance" : max}}` + +| `Near` +| `findByLocationNear(Point point, Distance min, Distance max)` +| `{"location" : {"$near" : [x,y], "$minDistance" : min, "$maxDistance" : max}}` + | `Within` | `findByLocationWithin(Circle circle)` | `{"location" : {"$geoWithin" : {"$center" : [ [x, y], distance]}}}` @@ -283,6 +291,8 @@ Distance distance = new Distance(200, Metrics.KILOMETERS); As you can see using a `Distance` equipped with a `Metric` causes `$nearSphere` clause to be added instead of a plain `$near`. Beyond that the actual distance gets calculated according to the `Metrics` used. +NOTE: Using `@GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE)` on the target property forces usage of `$nearSphere` operator. + ==== Geo-near queries [source,java] @@ -296,6 +306,11 @@ public interface PersonRepository extends MongoRepository // Metric: {'geoNear' : 'person', 'near' : [x, y], 'maxDistance' : distance, // 'distanceMultiplier' : metric.multiplier, 'spherical' : true } GeoResults findByLocationNear(Point location, Distance distance); + + // Metric: {'geoNear' : 'person', 'near' : [x, y], 'minDistance' : min, + // 'maxDistance' : max, 'distanceMultiplier' : metric.multiplier, + // 'spherical' : true } + GeoResults findByLocationNear(Point location, Distance min, Distance max); // {'geoNear' : 'location', 'near' : [x, y] } GeoResults findByLocationNear(Point location); diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 4376fd0d8..aee74a780 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1013,6 +1013,7 @@ There are also methods on the Criteria class for geospatial queries. Here is a l * `Criteria` *withinSphere* `(Circle circle)` Creates a geospatial criterion using `$geoWithin $center` operators. * `Criteria` *near* `(Point point)` Creates a geospatial criterion using a `$near `operation * `Criteria` *nearSphere* `(Point point)` Creates a geospatial criterion using `$nearSphere$center` operations. This is only available for MongoDB 1.7 and higher. +* `Criteria` *minDistance* `(double minDistance)` Creates a geospatial criterion using the `$minDistance` operation, for use with $near. * `Criteria` *maxDistance* `(double maxDistance)` Creates a geospatial criterion using the `$maxDistance` operation, for use with $near. The `Query` class has some additional methods used to provide options for the query. @@ -1111,7 +1112,7 @@ List venues = template.find(new Query(Criteria.where("location").within(box)), Venue.class); ---- -To find venues near a `Point`, the following query can be used +To find venues near a `Point`, the following queries can be used [source,java] ---- @@ -1120,6 +1121,13 @@ List venues = template.find(new Query(Criteria.where("location").near(point).maxDistance(0.01)), Venue.class); ---- +[source,java] +---- +Point point = new Point(-73.99171, 40.738868); +List venues = + template.find(new Query(Criteria.where("location").near(point).minDistance(0.01).maxDistance(100)), Venue.class); +---- + To find venues near a `Point` using spherical coordines the following query can be used [source,java]