From 5e8d752be19ee0f8563b6c6cde92661ad0755187 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 13 Dec 2018 14:38:56 +0100 Subject: [PATCH] DATAMONGO-1348 - Update documentation. Original pull request: #339. --- .../mongodb/core/query/MetricConversion.java | 2 +- .../data/mongodb/core/query/NearQuery.java | 155 +++++++++++++++- .../core/aggregation/AggregationTests.java | 4 +- .../data/mongodb/core/geo/GeoJsonTests.java | 36 +++- .../core/query/MetricConversionUnitTests.java | 22 +-- .../core/query/NearQueryUnitTests.java | 12 +- src/main/asciidoc/reference/mongodb.adoc | 171 ++++++++++++++++++ 7 files changed, 367 insertions(+), 35 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java index 1c5bc86b3..6b064f952 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java @@ -27,7 +27,7 @@ import org.springframework.data.geo.Metrics; * {@link Metric} and {@link Distance} conversions using the metric system. * * @author Mark Paluch - * @since 1.10 + * @since 2.2 */ class MetricConversion { 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 dbd99571f..052c4ae66 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 @@ -24,14 +24,147 @@ import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metric; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; -import org.springframework.lang.Nullable; -import org.springframework.data.mongodb.core.geo.GeoJson; import org.springframework.data.mongodb.core.geo.GeoJsonPoint; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** - * Builder class to build near-queries. + * Builder class to build near-queries.
+ * MongoDB {@code $geoNear} operator allows usage of a {@literal GeoJSON Point} or legacy coordinate pair. Though + * syntactically different, there's no difference between {@code near: [-73.99171, 40.738868]} and {@code near: { type: + * "Point", coordinates: [-73.99171, 40.738868] } } for the MongoDB server
+ *
+ * Please note that there is a huge difference in the distance calculation. Using the legacy format (for near) operates + * upon {@literal Radians} on an Earth like sphere, whereas the {@literal GeoJSON} format uses {@literal Meters}. The + * actual type within the document is of no concern at this point.
+ * To avoid a serious headache make sure to set the {@link Metric} to the desired unit of measure which ensures the + * distance to be calculated correctly.
+ *

+ * In other words:
+ * Assume you've got 5 Documents like the ones below
+ * + *

+ *     
+ * {
+ *     "_id" : ObjectId("5c10f3735d38908db52796a5"),
+ *     "name" : "Penn Station",
+ *     "location" : { "type" : "Point", "coordinates" : [  -73.99408, 40.75057 ] }
+ * }
+ * {
+ *     "_id" : ObjectId("5c10f3735d38908db52796a6"),
+ *     "name" : "10gen Office",
+ *     "location" : { "type" : "Point", "coordinates" : [ -73.99171, 40.738868 ] }
+ * }
+ * {
+ *     "_id" : ObjectId("5c10f3735d38908db52796a9"),
+ *     "name" : "City Bakery ",
+ *     "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
+ * }
+ * {
+ *     "_id" : ObjectId("5c10f3735d38908db52796aa"),
+ *     "name" : "Splash Bar",
+ *     "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
+ * }
+ * {
+ *     "_id" : ObjectId("5c10f3735d38908db52796ab"),
+ *     "name" : "Momofuku Milk Bar",
+ *     "location" : { "type" : "Point", "coordinates" : [ -73.985839, 40.731698 ] }
+ * }
+ *      
+ * 
+ * + * Fetching all Documents within a 400 Meter radius from {@code [-73.99171, 40.738868] } would look like this using + * {@literal GeoJSON}: + * + *
+ *     
+ * {
+ *     $geoNear: {
+ *         maxDistance: 400,
+ *         num: 10,
+ *         near: { type: "Point", coordinates: [-73.99171, 40.738868] },
+ *         spherical:true,
+ *         key: "location",
+ *         distanceField: "distance"
+ *     }
+ * }
+ *
+ *     
+ * 
+ * + * resulting in the following 3 Documents. + * + *
+ *     
+ * {
+ *     "_id" : ObjectId("5c10f3735d38908db52796a6"),
+ *     "name" : "10gen Office",
+ *     "location" : { "type" : "Point", "coordinates" : [ -73.99171, 40.738868 ] }
+ *     "distance" : 0.0 // Meters
+ * }
+ * {
+ *     "_id" : ObjectId("5c10f3735d38908db52796a9"),
+ *     "name" : "City Bakery ",
+ *     "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
+ *     "distance" : 69.3582262492474 // Meters
+ * }
+ * {
+ *     "_id" : ObjectId("5c10f3735d38908db52796aa"),
+ *     "name" : "Splash Bar",
+ *     "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
+ *     "distance" : 69.3582262492474 // Meters
+ * }
+ *     
+ * 
+ * + * Using legacy coordinate pairs one operates upon radians as discussed before. Assume we use {@link Metrics#KILOMETERS} + * when constructing the geoNear command. The {@link Metric} will make sure the distance multiplier is set correctly, so + * the command is rendered like + * + *
+ *     
+ * {
+ *     $geoNear: {
+ *         maxDistance: 0.0000627142377, // 400 Meters
+ *         distanceMultiplier: 6378.137,
+ *         num: 10,
+ *         near: [-73.99171, 40.738868],
+ *         spherical:true,
+ *         key: "location",
+ *         distanceField: "distance"
+ *     }
+ * }
+ *     
+ * 
+ * + * Please note the calculated distance now uses {@literal Kilometers} instead of {@literal Meters} as unit of measure, + * so we need to take it times 1000 to match up to {@literal Meters} as in the {@literal GeoJSON} variant.
+ * Still as we've been requesting the {@link Distance} in {@link Metrics#KILOMETERS} the {@link Distance#getValue()} + * reflects exactly this. + * + *
+ *     
+ * {
+ *     "_id" : ObjectId("5c10f3735d38908db52796a6"),
+ *     "name" : "10gen Office",
+ *     "location" : { "type" : "Point", "coordinates" : [ -73.99171, 40.738868 ] }
+ *     "distance" : 0.0 // Kilometers
+ * }
+ * {
+ *     "_id" : ObjectId("5c10f3735d38908db52796a9"),
+ *     "name" : "City Bakery ",
+ *     "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
+ *     "distance" : 0.0693586286032982 // Kilometers
+ * }
+ * {
+ *     "_id" : ObjectId("5c10f3735d38908db52796aa"),
+ *     "name" : "Splash Bar",
+ *     "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
+ *     "distance" : 0.0693586286032982 // Kilometers
+ * }
+ *     
+ * 
* * @author Oliver Gierke * @author Thomas Darimont @@ -91,10 +224,14 @@ public final class NearQuery { } /** - * Creates a new {@link NearQuery} starting at the given {@link Point}. + * Creates a new {@link NearQuery} starting at the given {@link Point}.
+ * NOTE There is a difference in using {@link Point} versus {@link GeoJsonPoint}. {@link Point} + * values are rendered as coordinate pairs in the legacy format and operate upon radians, whereas the + * {@link GeoJsonPoint} uses according to its specification {@literal meters} as unit of measure. This may lead to + * different results when using a {@link Metrics#NEUTRAL neutral Metric}. * * @param point must not be {@literal null}. - * @return + * @return new instance of {@link NearQuery}. */ public static NearQuery near(Point point) { return near(point, Metrics.NEUTRAL); @@ -103,11 +240,15 @@ public final class NearQuery { /** * Creates a {@link NearQuery} starting near the given {@link Point} using the given {@link Metric} to adapt given * values to further configuration. E.g. setting a {@link #maxDistance(double)} will be interpreted as a value of the - * initially set {@link Metric}. + * initially set {@link Metric}.
+ * NOTE There is a difference in using {@link Point} versus {@link GeoJsonPoint}. {@link Point} + * values are rendered as coordinate pairs in the legacy format and operate upon radians, whereas the + * {@link GeoJsonPoint} uses according to its specification {@literal meters} as unit of measure. This may lead to + * different results when using a {@link Metrics#NEUTRAL neutral Metric}. * * @param point must not be {@literal null}. * @param metric must not be {@literal null}. - * @return + * @return new instance of {@link NearQuery}. */ public static NearQuery near(Point point, Metric metric) { return new NearQuery(point, metric); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java index b6f4c1efb..2ce342676 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java @@ -23,7 +23,6 @@ import static org.springframework.data.mongodb.core.aggregation.Fields.*; import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.test.util.IsBsonObject.*; -import com.mongodb.DBObject; import lombok.Builder; import java.io.BufferedInputStream; @@ -520,7 +519,7 @@ public class AggregationTests { /* //complex mongodb aggregation framework example from https://docs.mongodb.org/manual/tutorial/aggregation-examples/#largest-and-smallest-cities-by-state - + db.zipcodes.aggregate( { $group: { @@ -1530,7 +1529,6 @@ public class AggregationTests { assertThat((Double) firstResult.get("distance"), closeTo(73.08517, 0.00001)); } - @Test // DATAMONGO-1133 public void shouldHonorFieldAliasesForFieldReferences() { 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 3f105680f..f6ca5fa8f 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 @@ -28,6 +28,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.dao.DataAccessException; @@ -56,6 +57,7 @@ import com.mongodb.client.MongoCollection; /** * @author Christoph Strobl + * @author Mark Paluch */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration @@ -104,6 +106,36 @@ public class GeoJsonTests { assertThat(result.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS); } + @Test // DATAMONGO-1148 + public void geoNearShouldReturnDistanceCorrectlyUsingGeoJson/*which is using the meters*/() { + + NearQuery geoNear = NearQuery.near(new GeoJsonPoint(-73.99171, 40.738868), Metrics.KILOMETERS).num(10) + .maxDistance(0.4); + + GeoResults result = template.geoNear(geoNear, Venue2DSphere.class); + + assertThat(result.getContent()).hasSize(3); + assertThat(result.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS); + assertThat(result.getContent().get(0).getDistance().getValue()).isCloseTo(0.0, offset(0.000001)); + assertThat(result.getContent().get(1).getDistance().getValue()).isCloseTo(0.0693582, offset(0.000001)); + assertThat(result.getContent().get(2).getDistance().getValue()).isCloseTo(0.0693582, offset(0.000001)); + } + + @Test // DATAMONGO-1348 + public void geoNearShouldReturnDistanceCorrectly/*which is using the meters*/() { + + NearQuery geoNear = NearQuery.near(new Point(-73.99171, 40.738868), Metrics.KILOMETERS).num(10) + .maxDistance(0.4); + + GeoResults result = template.geoNear(geoNear, Venue2DSphere.class); + + assertThat(result.getContent()).hasSize(3); + assertThat(result.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS); + assertThat(result.getContent().get(0).getDistance().getValue()).isCloseTo(0.0, offset(0.000001)); + assertThat(result.getContent().get(1).getDistance().getValue()).isCloseTo(0.0693582, offset(0.000001)); + assertThat(result.getContent().get(2).getDistance().getValue()).isCloseTo(0.0693582, offset(0.000001)); + } + @Test // DATAMONGO-1135 public void geoNearWithMiles() { @@ -111,8 +143,8 @@ public class GeoJsonTests { GeoResults result = template.geoNear(geoNear, Venue2DSphere.class); - assertThat(result.getContent().size(), is(not(0))); - assertThat(result.getAverageDistance().getMetric(), is((Metric) Metrics.MILES)); + assertThat(result.getContent()).isNotEmpty(); + assertThat(result.getAverageDistance().getMetric()).isEqualTo(Metrics.MILES); } @Test // DATAMONGO-1135 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java index 303d15451..d804192c8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2019 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,10 +30,7 @@ import org.springframework.data.geo.Metrics; */ public class MetricConversionUnitTests { - /** - * @see DATAMONGO-1348 - */ - @Test + @Test // DATAMONGO-1348 public void shouldConvertMilesToMeters() { Distance distance = new Distance(1, Metrics.MILES); @@ -42,10 +39,7 @@ public class MetricConversionUnitTests { assertThat(distanceInMeters, is(closeTo(1609.3438343d, 0.000000001))); } - /** - * @see DATAMONGO-1348 - */ - @Test + @Test // DATAMONGO-1348 public void shouldConvertKilometersToMeters() { Distance distance = new Distance(1, Metrics.KILOMETERS); @@ -54,10 +48,7 @@ public class MetricConversionUnitTests { assertThat(distanceInMeters, is(closeTo(1000, 0.000000001))); } - /** - * @see DATAMONGO-1348 - */ - @Test + @Test // DATAMONGO-1348 public void shouldCalculateMetersToKilometersMultiplier() { double multiplier = MetricConversion.getMetersToMetricMultiplier(Metrics.KILOMETERS); @@ -65,10 +56,7 @@ public class MetricConversionUnitTests { assertThat(multiplier, is(closeTo(0.001, 0.000000001))); } - /** - * @see DATAMONGO-1348 - */ - @Test + @Test // DATAMONGO-1348 public void shouldCalculateMetersToMilesMultiplier() { double multiplier = MetricConversion.getMetersToMetricMultiplier(Metrics.MILES); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java index eb67e802b..b948e40e7 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java @@ -95,8 +95,9 @@ public class NearQueryUnitTests { Pageable pageable = PageRequest.of(3, 5); NearQuery query = NearQuery.near(new Point(1, 1)).with(pageable); - assertThat(query.getSkip(), is((long)pageable.getPageNumber() * pageable.getPageSize())); - assertThat((Long) query.toDocument().get("num"), is((long)(pageable.getPageNumber() + 1) * pageable.getPageSize())); + assertThat(query.getSkip(), is((long) pageable.getPageNumber() * pageable.getPageSize())); + assertThat((Long) query.toDocument().get("num"), + is((long) (pageable.getPageNumber() + 1) * pageable.getPageSize())); } @Test // DATAMONGO-445 @@ -108,7 +109,7 @@ public class NearQueryUnitTests { .query(Query.query(Criteria.where("foo").is("bar")).limit(limit).skip(skip)); assertThat(query.getSkip(), is(skip)); - assertThat((Long) query.toDocument().get("num"), is((long)limit)); + assertThat((Long) query.toDocument().get("num"), is((long) limit)); } @Test // DATAMONGO-445 @@ -120,8 +121,9 @@ public class NearQueryUnitTests { NearQuery query = NearQuery.near(new Point(1, 1)) .query(Query.query(Criteria.where("foo").is("bar")).limit(limit).skip(skip)).with(pageable); - assertThat(query.getSkip(), is((long)pageable.getPageNumber() * pageable.getPageSize())); - assertThat((Long) query.toDocument().get("num"), is((long)(pageable.getPageNumber() + 1) * pageable.getPageSize())); + assertThat(query.getSkip(), is((long) pageable.getPageNumber() * pageable.getPageSize())); + assertThat((Long) query.toDocument().get("num"), + is((long) (pageable.getPageNumber() + 1) * pageable.getPageSize())); } @Test // DATAMONGO-829 diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 3d7fa2a78..0c9bdf792 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1435,6 +1435,177 @@ repo.findByLocationWithin( <4> <4> Use the legacy format `$polygon` operator. ==== +==== Metrics and Distance calculation + +Then MongoDB `$geoNear` operator allows usage of a GeoJSON Point or legacy coordinate pairs. + +==== +[source,java] +---- +NearQuery.near(new Point(-73.99171, 40.738868)) +---- +[source,json] +---- +{ + "$geoNear": { + //... + "near": [-73.99171, 40.738868] + } +} +---- +==== +==== +[source,java] +---- +NearQuery.near(new GeoJsonPoint(-73.99171, 40.738868)) +---- +[source,json] +---- +{ + "$geoNear": { + //... + "near": { "type": "Point", "coordinates": [-73.99171, 40.738868] } + } +} + +---- +==== + +Though syntactically different the server is fine accepting both no matter what format the target Document within the collection +is using. + +WARNING: There is a huge difference in the distance calculation. Using the legacy format operates +upon _Radians_ on an Earth like sphere, whereas the GeoJSON format uses _Meters_. + +To avoid a serious headache make sure to set the `Metric` to the desired unit of measure which ensures the +distance to be calculated correctly. + +In other words: + +==== +Assume you've got 5 Documents like the ones below: +[source,json] +---- +{ + "_id" : ObjectId("5c10f3735d38908db52796a5"), + "name" : "Penn Station", + "location" : { "type" : "Point", "coordinates" : [ -73.99408, 40.75057 ] } +} +{ + "_id" : ObjectId("5c10f3735d38908db52796a6"), + "name" : "10gen Office", + "location" : { "type" : "Point", "coordinates" : [ -73.99171, 40.738868 ] } +} +{ + "_id" : ObjectId("5c10f3735d38908db52796a9"), + "name" : "City Bakery ", + "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] } +} +{ + "_id" : ObjectId("5c10f3735d38908db52796aa"), + "name" : "Splash Bar", + "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] } +} +{ + "_id" : ObjectId("5c10f3735d38908db52796ab"), + "name" : "Momofuku Milk Bar", + "location" : { "type" : "Point", "coordinates" : [ -73.985839, 40.731698 ] } +} +---- +==== + +Fetching all Documents within a 400 Meter radius from `[-73.99171, 40.738868]` would look like this using +GeoJSON: + +.GeoNear with GeoJSON +==== +[source,json] +---- +{ + "$geoNear": { + "maxDistance": 400, <1> + "num": 10, + "near": { type: "Point", coordinates: [-73.99171, 40.738868] }, + "spherical":true, <2> + "key": "location", + "distanceField": "distance" + } +} +---- +Returning the following 3 Documents: +[source,json] +---- +{ + "_id" : ObjectId("5c10f3735d38908db52796a6"), + "name" : "10gen Office", + "location" : { "type" : "Point", "coordinates" : [ -73.99171, 40.738868 ] } + "distance" : 0.0 <3> +} +{ + "_id" : ObjectId("5c10f3735d38908db52796a9"), + "name" : "City Bakery ", + "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] } + "distance" : 69.3582262492474 <3> +} +{ + "_id" : ObjectId("5c10f3735d38908db52796aa"), + "name" : "Splash Bar", + "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] } + "distance" : 69.3582262492474 <3> +} +---- +<1> Maximum distance from center point in _Meters_. +<2> GeoJSON always operates upon a sphere. +<3> Distance from center point in _Meters_. +==== + +Now, when using legacy coordinate pairs one operates upon _Radians_ as discussed before. So we use `Metrics#KILOMETERS +when constructing the `$geoNear` command. The `Metric` makes sure the distance multiplier is set correctly. + +.GeoNear with Legacy Coordinate Pairs +==== +[source,json] +---- +{ + "$geoNear": { + "maxDistance": 0.0000627142377, <1> + "distanceMultiplier": 6378.137, <2> + "num": 10, + "near": [-73.99171, 40.738868], + "spherical":true, <3> + "key": "location", + "distanceField": "distance" + } +} +---- +Returning the 3 Documents just like the GeoJSON variant: +[source,json] +---- +{ + "_id" : ObjectId("5c10f3735d38908db52796a6"), + "name" : "10gen Office", + "location" : { "type" : "Point", "coordinates" : [ -73.99171, 40.738868 ] } + "distance" : 0.0 <4> +} +{ + "_id" : ObjectId("5c10f3735d38908db52796a9"), + "name" : "City Bakery ", + "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] } + "distance" : 0.0693586286032982 <4> +} +{ + "_id" : ObjectId("5c10f3735d38908db52796aa"), + "name" : "Splash Bar", + "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] } + "distance" : 0.0693586286032982 <4> +} +---- +<1> Maximum distance from center point in _Radians_. +<2> The distance multiplier so we get _Kilometers_ as resulting distance. +<3> Make sure we operate on a 2d_sphere index. +<4> Distance from center point in _Kilometers_ - take it times 1000 to match _Meters_ of the GeoJSON variant. +==== + [[mongo.textsearch]] === Full-text Queries