diff options
author | Mihai Andrei <mihai.andrei@10gen.com> | 2021-04-08 16:19:04 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2021-04-08 16:38:04 +0000 |
commit | bbf60c1bcbc5722870edb7327fc59d4025a7644e (patch) | |
tree | f3cd71d7603722e97ca46fe0458035d5cd33292e | |
parent | 368553479c74478d15204383289d1291ad097909 (diff) | |
download | mongo-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.js | 52 | ||||
-rw-r--r-- | src/mongo/db/exec/geo_near.cpp | 21 | ||||
-rw-r--r-- | src/mongo/db/geo/geometry_container.cpp | 10 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_geo_near.cpp | 4 |
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) |