diff options
-rw-r--r-- | etc/backports_required_for_multiversion_tests.yml | 2 | ||||
-rw-r--r-- | jstests/core/geo_near_point_query.js | 51 | ||||
-rw-r--r-- | src/mongo/db/exec/geo_near.cpp | 21 | ||||
-rw-r--r-- | src/mongo/db/geo/geometry_container.cpp | 10 |
4 files changed, 80 insertions, 4 deletions
diff --git a/etc/backports_required_for_multiversion_tests.yml b/etc/backports_required_for_multiversion_tests.yml index a22586a0777..47ddd16346d 100644 --- a/etc/backports_required_for_multiversion_tests.yml +++ b/etc/backports_required_for_multiversion_tests.yml @@ -100,6 +100,8 @@ all: test_file: jstests/replsets/rollback_reconstructs_transactions_prepared_before_stable.js - ticket: SERVER-54366 test_file: jstests/replsets/force_shutdown_primary.js + - ticket: SERVER-52953 + test_file: jstests/core/geo_near_point_query.js # Tests that should only be excluded from particular suites should be listed under that suite. suites: diff --git a/jstests/core/geo_near_point_query.js b/jstests/core/geo_near_point_query.js new file mode 100644 index 00000000000..d3faba9c29e --- /dev/null +++ b/jstests/core/geo_near_point_query.js @@ -0,0 +1,51 @@ +/** + * Verifies that $geoNear correctly matches the point given to the 'near' parameter when the + * 'maxDistance' parameter is set to 0. + * + * @tags: [backport_required_multiversion] + */ +(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 (const doc of docs) { + // 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. + for (const dist of [0, 0.001]) { + 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 3a41a7a2538..b8b6670d10a 100644 --- a/src/mongo/db/exec/geo_near.cpp +++ b/src/mongo/db/exec/geo_near.cpp @@ -813,12 +813,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 97ae2533fc8..a622323df64 100644 --- a/src/mongo/db/geo/geometry_container.cpp +++ b/src/mongo/db/geo/geometry_container.cpp @@ -1288,7 +1288,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) { |