summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMihai Andrei <mihai.andrei@10gen.com>2021-04-08 16:19:04 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2021-04-08 16:38:04 +0000
commitbbf60c1bcbc5722870edb7327fc59d4025a7644e (patch)
treef3cd71d7603722e97ca46fe0458035d5cd33292e
parent368553479c74478d15204383289d1291ad097909 (diff)
downloadmongo-bbf60c1bcbc5722870edb7327fc59d4025a7644e.tar.gz
SERVER-52953 $geoNear does not always match coordinate given to 'near' when maxDistance is set to 0
(cherry picked from commit 72bf0123af2075be083c6d6ad1a65658e0d499b1)
-rw-r--r--jstests/core/geo_near_point_query.js52
-rw-r--r--src/mongo/db/exec/geo_near.cpp21
-rw-r--r--src/mongo/db/geo/geometry_container.cpp10
-rw-r--r--src/mongo/db/pipeline/document_source_geo_near.cpp4
4 files changed, 81 insertions, 6 deletions
diff --git a/jstests/core/geo_near_point_query.js b/jstests/core/geo_near_point_query.js
new file mode 100644
index 00000000000..5331ce4228f
--- /dev/null
+++ b/jstests/core/geo_near_point_query.js
@@ -0,0 +1,52 @@
+/**
+ * Verifies that $geoNear correctly matches the point given to the 'near' parameter when the
+ * 'maxDistance' parameter is set to 0.
+ */
+(function() {
+ "use strict";
+ const collName = jsTestName();
+ const coll = db[collName];
+ coll.drop();
+
+ const docs = [
+ {"location": {"type": "Point", "coordinates": [-90, 45.1]}},
+ {"location": {"type": "Point", "coordinates": [-90.0001, 45.1]}},
+ {"location": {"type": "Point", "coordinates": [-90, 45.00001]}},
+ {"location": {"type": "Point", "coordinates": [-90, 45.01]}},
+ {"location": {"type": "Point", "coordinates": [90, -45]}},
+ {"location": {"type": "Point", "coordinates": [90.00007, -45]}},
+ {"location": {"type": "Point", "coordinates": [90, 45]}},
+ {"location": {"type": "Point", "coordinates": [90, 45.1]}},
+ {"location": {"type": "Point", "coordinates": [90, 45.01]}},
+
+ ];
+ assert.commandWorked(coll.insert(docs));
+ assert.commandWorked(coll.createIndex({location: "2dsphere"}));
+
+ for (let i = 0; i < docs.length; ++i) {
+ const doc = docs[i];
+ // We test a distance of 0 to verify that point queries work correctly as well as a small,
+ // non-zero distance of 0.001 to verify that the distance computation used in constructing
+ // an S2Cap doesn't underflow.
+ const distances = [0, 0.001];
+ for (let j = 0; j < distances.length; ++j) {
+ const dist = distances[j];
+ const pipeline = [
+ {
+ $geoNear: {
+ near: doc["location"],
+ maxDistance: dist,
+ spherical: true,
+ distanceField: "dist.calculated",
+ includeLocs: "dist.location"
+ }
+ },
+ {$project: {_id: 0, location: 1}}
+ ];
+ const result = coll.aggregate(pipeline).toArray();
+ assert.eq(1, result.length, tojson(doc));
+ const item = result[0];
+ assert.eq(doc, item);
+ }
+ }
+})();
diff --git a/src/mongo/db/exec/geo_near.cpp b/src/mongo/db/exec/geo_near.cpp
index cb1eb1cc57e..c61fb2f07ae 100644
--- a/src/mongo/db/exec/geo_near.cpp
+++ b/src/mongo/db/exec/geo_near.cpp
@@ -832,12 +832,27 @@ S2Region* buildS2Region(const R2Annulus& sphereBounds) {
regions.push_back(new S2Cap(innerCap));
}
+ // 'kEpsilon' is about 9 times the double-precision roundoff relative error.
+ const double kEpsilon = 1e-15;
+
// We only need to max bound if this is not a full search of the Earth
// Using the constant here is important since we use the min of kMaxEarthDistance
// and the actual bounds passed in to set up the search area.
- if (outer < kMaxEarthDistanceInMeters) {
- S2Cap outerCap = S2Cap::FromAxisAngle(latLng.ToPoint(),
- S1Angle::Radians(outer / kRadiusOfEarthInMeters));
+ if ((outer * (1 + kEpsilon)) < kMaxEarthDistanceInMeters) {
+ // SERVER-52953: The cell covering returned by S2 may have a matching point along its
+ // boundary. In certain cases, this boundary point is not contained within the covering,
+ // which means that this search will not match said point. As such, we avoid this issue by
+ // finding a covering for the region expanded over a very small radius because this covering
+ // is guaranteed to contain the boundary point.
+ auto angle = S1Angle::Radians((outer * (1 + kEpsilon)) / kRadiusOfEarthInMeters);
+ S2Cap outerCap = S2Cap::FromAxisAngle(latLng.ToPoint(), angle);
+
+ // If 'outer' is sufficiently small, the computation of the S2Cap's height from 'angle' may
+ // underflow, resulting in a height less than 'kEpsilon' and an empty cap. As such, we
+ // guarantee that 'outerCap' has a height of at least 'kEpsilon'.
+ if (outerCap.height() < kEpsilon) {
+ outerCap = S2Cap::FromAxisHeight(latLng.ToPoint(), kEpsilon);
+ }
regions.push_back(new S2Cap(outerCap));
}
diff --git a/src/mongo/db/geo/geometry_container.cpp b/src/mongo/db/geo/geometry_container.cpp
index 8f80723483f..b3b787c7994 100644
--- a/src/mongo/db/geo/geometry_container.cpp
+++ b/src/mongo/db/geo/geometry_container.cpp
@@ -1290,7 +1290,15 @@ double GeometryContainer::minDistance(const PointWithCRS& otherPoint) const {
double minDistance = -1;
if (NULL != _point) {
- minDistance = S2Distance::distanceRad(otherPoint.point, _point->point);
+ // SERVER-52953: Calculating the distance between identical points can sometimes result
+ // in a small positive value due to a loss of floating point precision on certain
+ // platforms. As such, we perform a simple equality check to guarantee that equivalent
+ // points will always produce a distance of 0.
+ if (_point->point == otherPoint.point) {
+ minDistance = 0;
+ } else {
+ minDistance = S2Distance::distanceRad(otherPoint.point, _point->point);
+ }
} else if (NULL != _line) {
minDistance = S2Distance::minDistanceRad(otherPoint.point, _line->line);
} else if (NULL != _polygon) {
diff --git a/src/mongo/db/pipeline/document_source_geo_near.cpp b/src/mongo/db/pipeline/document_source_geo_near.cpp
index a3c4bd81dd6..962ea5addc9 100644
--- a/src/mongo/db/pipeline/document_source_geo_near.cpp
+++ b/src/mongo/db/pipeline/document_source_geo_near.cpp
@@ -126,7 +126,7 @@ Value DocumentSourceGeoNear::serialize(boost::optional<ExplainOptions::Verbosity
result.setField("limit", Value(limit));
- if (maxDistance > 0)
+ if (maxDistance >= 0)
result.setField("maxDistance", Value(maxDistance));
if (minDistance > 0)
@@ -158,7 +158,7 @@ BSONObj DocumentSourceGeoNear::buildGeoNearCmd() const {
geoNear.append("num", limit); // called limit in toBson
- if (maxDistance > 0)
+ if (maxDistance >= 0)
geoNear.append("maxDistance", maxDistance);
if (minDistance > 0)