diff options
113 files changed, 2548 insertions, 2426 deletions
diff --git a/buildscripts/resmokeconfig/suites/concurrency_sharded_causal_consistency_and_balancer.yml b/buildscripts/resmokeconfig/suites/concurrency_sharded_causal_consistency_and_balancer.yml index ad37d18622b..31b27153d44 100644 --- a/buildscripts/resmokeconfig/suites/concurrency_sharded_causal_consistency_and_balancer.yml +++ b/buildscripts/resmokeconfig/suites/concurrency_sharded_causal_consistency_and_balancer.yml @@ -40,10 +40,6 @@ selector: - jstests/concurrency/fsm_workloads/map_reduce_replace_nonexistent.js - jstests/concurrency/fsm_workloads/map_reduce_replace_remove.js - # Disabled due to SERVER-13364, 'The geoNear command doesn't handle shard versioning, so a - # concurrent chunk migration may cause duplicate or missing results' - - jstests/concurrency/fsm_workloads/yield_geo_near_dedup.js - # Disabled due to MongoDB restrictions and/or workload restrictions # These workloads sometimes trigger 'Could not lock auth data update lock' diff --git a/buildscripts/resmokeconfig/suites/concurrency_sharded_replication_with_balancer.yml b/buildscripts/resmokeconfig/suites/concurrency_sharded_replication_with_balancer.yml index 1dcd16d7350..dc5161df8e8 100644 --- a/buildscripts/resmokeconfig/suites/concurrency_sharded_replication_with_balancer.yml +++ b/buildscripts/resmokeconfig/suites/concurrency_sharded_replication_with_balancer.yml @@ -37,10 +37,6 @@ selector: - jstests/concurrency/fsm_workloads/map_reduce_replace_nonexistent.js - jstests/concurrency/fsm_workloads/map_reduce_replace_remove.js - # Disabled due to SERVER-13364, 'The geoNear command doesn't handle shard versioning, so a - # concurrent chunk migration may cause duplicate or missing results' - - jstests/concurrency/fsm_workloads/yield_geo_near_dedup.js - # Disabled due to MongoDB restrictions and/or workload restrictions # These workloads sometimes trigger 'Could not lock auth data update lock' diff --git a/buildscripts/resmokeconfig/suites/concurrency_sharded_with_stepdowns.yml b/buildscripts/resmokeconfig/suites/concurrency_sharded_with_stepdowns.yml index 1ac613d9aa7..44a614fda26 100644 --- a/buildscripts/resmokeconfig/suites/concurrency_sharded_with_stepdowns.yml +++ b/buildscripts/resmokeconfig/suites/concurrency_sharded_with_stepdowns.yml @@ -152,8 +152,6 @@ selector: - jstests/concurrency/fsm_workloads/update_multifield_multiupdate.js - jstests/concurrency/fsm_workloads/update_multifield_multiupdate_noindex.js - jstests/concurrency/fsm_workloads/update_ordered_bulk_inc.js - - jstests/concurrency/fsm_workloads/yield_geo_near.js - - jstests/concurrency/fsm_workloads/yield_geo_near_dedup.js - jstests/concurrency/fsm_workloads/yield_id_hack.js # Uses non retryable commands. @@ -177,6 +175,7 @@ selector: exclude_with_any_tags: - uses_transactions - requires_replication + - requires_non_retryable_writes executor: archive: diff --git a/buildscripts/resmokeconfig/suites/concurrency_sharded_with_stepdowns_and_balancer.yml b/buildscripts/resmokeconfig/suites/concurrency_sharded_with_stepdowns_and_balancer.yml index 4d3d5be805e..c7c360a362e 100644 --- a/buildscripts/resmokeconfig/suites/concurrency_sharded_with_stepdowns_and_balancer.yml +++ b/buildscripts/resmokeconfig/suites/concurrency_sharded_with_stepdowns_and_balancer.yml @@ -37,10 +37,6 @@ selector: - jstests/concurrency/fsm_workloads/map_reduce_replace_nonexistent.js - jstests/concurrency/fsm_workloads/map_reduce_replace_remove.js - # Disabled due to SERVER-13364, 'The geoNear command doesn't handle shard versioning, so a - # concurrent chunk migration may cause duplicate or missing results' - - jstests/concurrency/fsm_workloads/yield_geo_near_dedup.js - # Disabled due to MongoDB restrictions and/or workload restrictions # These workloads sometimes trigger 'Could not lock auth data update lock' @@ -158,7 +154,6 @@ selector: - jstests/concurrency/fsm_workloads/update_multifield_multiupdate.js - jstests/concurrency/fsm_workloads/update_multifield_multiupdate_noindex.js - jstests/concurrency/fsm_workloads/update_ordered_bulk_inc.js - - jstests/concurrency/fsm_workloads/yield_geo_near.js - jstests/concurrency/fsm_workloads/yield_id_hack.js # Uses non retryable commands. @@ -181,6 +176,7 @@ selector: exclude_with_any_tags: - uses_transactions - requires_replication + - requires_non_retryable_writes executor: archive: diff --git a/buildscripts/resmokeconfig/suites/replica_sets_initsync_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_initsync_jscore_passthrough.yml index 297d53936db..8122140e2cc 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_initsync_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_initsync_jscore_passthrough.yml @@ -69,7 +69,6 @@ selector: - jstests/core/profile_distinct.js - jstests/core/profile_find.js - jstests/core/profile_findandmodify.js - - jstests/core/profile_geonear.js - jstests/core/profile_getmore.js - jstests/core/profile_insert.js - jstests/core/profile_sampling.js diff --git a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_jscore_passthrough.yml index f5d2c288232..bbdfa6024b5 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_jscore_passthrough.yml @@ -37,6 +37,7 @@ selector: - jstests/core/find_getmore_bsonsize.js - jstests/core/find_getmore_cmd.js - jstests/core/find9.js + - jstests/core/geonear_key.js - jstests/core/index_big1.js - jstests/core/index_bigkeys.js - jstests/core/index_decimal.js diff --git a/jstests/aggregation/bugs/server6530.js b/jstests/aggregation/bugs/server6530.js index 22d26143e39..36a5d3deb3f 100644 --- a/jstests/aggregation/bugs/server6530.js +++ b/jstests/aggregation/bugs/server6530.js @@ -1,6 +1,32 @@ -// server-6530: disallow $near queries in $match operations -load('jstests/aggregation/extras/utils.js'); +/** + * Test that $near queries are disallowed in $match stages. + */ +(function() { + "use strict"; + load("jstests/aggregation/extras/utils.js"); -assertErrorCode(db.foo, {$match: {$near: [0, 0]}}, ErrorCodes.BadValue); -assertErrorCode(db.foo, {$match: {$nearSphere: [2, 2]}}, ErrorCodes.BadValue); -assertErrorCode(db.foo, {$match: {$geoNear: [2, 2]}}, ErrorCodes.BadValue); + const coll = db.getCollection("no_near_in_match"); + coll.drop(); + + // Create indexes that could satisfy various $near queries. + assert.commandWorked(coll.createIndex({point2d: "2d"})); + assert.commandWorked(coll.createIndex({point2dsphere: "2dsphere"})); + + // Populate the collection so that successful queries can return at least one result. + assert.writeOK(coll.insert({point2d: [0.25, 0.35]})); + assert.writeOK(coll.insert({point2dsphere: [0.25, 0.35]})); + + const nearQuery = {point2d: {$near: [0, 0]}}; + const nearSphereQuery = {point2dsphere: {$nearSphere: [0, 0]}}; + const geoNearQuery = {point2d: {$geoNear: [0, 0]}}; + + // Test that normal finds return a result. + assert.eq(1, coll.find(nearQuery).count()); + assert.eq(1, coll.find(nearSphereQuery).count()); + assert.eq(1, coll.find(geoNearQuery).count()); + + // Test that we refuse to run $match with a near query. + assertErrorCode(coll, {$match: nearQuery}, ErrorCodes.BadValue); + assertErrorCode(coll, {$match: nearSphereQuery}, ErrorCodes.BadValue); + assertErrorCode(coll, {$match: geoNearQuery}, ErrorCodes.BadValue); +}()); diff --git a/jstests/aggregation/bugs/server7781.js b/jstests/aggregation/bugs/server7781.js index 5ed2d58a10e..9f227513c87 100644 --- a/jstests/aggregation/bugs/server7781.js +++ b/jstests/aggregation/bugs/server7781.js @@ -16,32 +16,37 @@ () => db[coll].aggregate( [{$match: {x: 1}}, {$geoNear: {near: [1, 1], spherical: true, distanceField: 'dis'}}])); - function checkOutput(cmdOut, aggOut, expectedNum) { - assert.commandWorked(cmdOut, "geoNear command"); - - // the output arrays are accessed differently - cmdOut = cmdOut.results; - aggOut = aggOut.toArray(); - - assert.eq(cmdOut.length, expectedNum); - assert.eq(aggOut.length, expectedNum); - - var allSame = true; - var massaged; // massage geoNear command output to match output from agg pipeline - for (var i = 0; i < cmdOut.length; i++) { - massaged = {}; - Object.extend(massaged, cmdOut[i].obj, /*deep=*/true); - massaged.stats = {'dis': cmdOut[i].dis, 'loc': cmdOut[i].loc}; - - if (!friendlyEqual(massaged, aggOut[i])) { - allSame = false; // don't bail yet since we want to print all differences - print("Difference detected at index " + i + " of " + expectedNum); - print("from geoNear command:" + tojson(massaged)); - print("from aggregate command:" + tojson(aggOut[i])); - } - } - - assert(allSame); + const kDistanceField = "dis"; + const kIncludeLocsField = "loc"; + + /** + * Tests the output of the $geoNear command. This function expects a document with the following + * fields: + * - 'geoNearSpec' is the specification for a $geoNear aggregation stage. + * - 'limit' is an integer limiting the number of pipeline results. + * - 'batchSize', if specified, is the batchSize to use for the aggregation. + */ + function testGeoNearStageOutput({geoNearSpec, limit, batchSize}) { + const aggOptions = batchSize ? {batchSize: batchSize} : {}; + const result = + db[coll].aggregate([{$geoNear: geoNearSpec}, {$limit: limit}], aggOptions).toArray(); + const errmsg = () => tojson(result); + + // Verify that we got the expected number of results. + assert.eq(result.length, limit, errmsg); + + // Run though the array, checking for proper sort order and sane computed distances. + result.reduce((lastDist, curDoc) => { + const curDist = curDoc[kDistanceField]; + + // Verify that distances are in increasing order. + assert.lte(lastDist, curDist, errmsg); + + // Verify that the computed distance is correct. + const computed = Geo.sphereDistance(geoNearSpec["near"], curDoc[kIncludeLocsField]); + assert.close(computed, curDist, errmsg); + return curDist; + }, 0); } // We use this to generate points. Using a single global to avoid reseting RNG in each pass. @@ -85,67 +90,28 @@ db[coll].ensureIndex({loc: indexType}); - // test with defaults - var queryPoint = pointMaker.mkPt(0.25); // stick to center of map - var geoCmd = {geoNear: coll, near: queryPoint, includeLocs: true, spherical: true}; - var aggCmd = { - $geoNear: { - near: queryPoint, - includeLocs: 'stats.loc', - distanceField: 'stats.dis', - spherical: true - } - }; - checkOutput(db.runCommand(geoCmd), db[coll].aggregate(aggCmd), 100); - - // test with num - queryPoint = pointMaker.mkPt(0.25); - geoCmd.num = 75; - geoCmd.near = queryPoint; - aggCmd.$geoNear.num = 75; - aggCmd.$geoNear.near = queryPoint; - checkOutput(db.runCommand(geoCmd), db[coll].aggregate(aggCmd), 75); - - // test with limit instead of num (they mean the same thing, but want to test both) - queryPoint = pointMaker.mkPt(0.25); - geoCmd.near = queryPoint; - delete geoCmd.num; - geoCmd.limit = 70; - aggCmd.$geoNear.near = queryPoint; - delete aggCmd.$geoNear.num; - aggCmd.$geoNear.limit = 70; - checkOutput(db.runCommand(geoCmd), db[coll].aggregate(aggCmd), 70); - - // test spherical - queryPoint = pointMaker.mkPt(0.25); - geoCmd.spherical = true; - geoCmd.near = queryPoint; - aggCmd.$geoNear.spherical = true; - aggCmd.$geoNear.near = queryPoint; - checkOutput(db.runCommand(geoCmd), db[coll].aggregate(aggCmd), 70); - - // test $geoNear + $limit coalescing - queryPoint = pointMaker.mkPt(0.25); - geoCmd.num = 40; - geoCmd.near = queryPoint; - aggCmd.$geoNear.near = queryPoint; - var aggArr = [aggCmd, {$limit: 50}, {$limit: 60}, {$limit: 40}]; - checkOutput(db.runCommand(geoCmd), db[coll].aggregate(aggArr), 40); - - // Test $geoNear with an initial batchSize of 0. Regression test for SERVER-20935. - queryPoint = pointMaker.mkPt(0.25); - geoCmd.spherical = true; - geoCmd.near = queryPoint; - geoCmd.limit = 70; - delete geoCmd.num; - aggCmd.$geoNear.spherical = true; - aggCmd.$geoNear.near = queryPoint; - aggCmd.$geoNear.limit = 70; - delete aggCmd.$geoNear.num; - var cmdRes = db[coll].runCommand("aggregate", {pipeline: [aggCmd], cursor: {batchSize: 0}}); - assert.commandWorked(cmdRes); - var cmdCursor = new DBCommandCursor(db, cmdRes, 0); - checkOutput(db.runCommand(geoCmd), cmdCursor, 70); + // Test $geoNear with spherical coordinates. + testGeoNearStageOutput({ + geoNearSpec: { + near: pointMaker.mkPt(0.25), + distanceField: kDistanceField, + includeLocs: kIncludeLocsField, + spherical: true, + }, + limit: 100 + }); + + // Test $geoNear with an initial batchSize of 1. + testGeoNearStageOutput({ + geoNearSpec: { + near: pointMaker.mkPt(0.25), + distanceField: kDistanceField, + includeLocs: kIncludeLocsField, + spherical: true, + }, + limit: 70, + batchSize: 1 + }); } test(db, false, '2d'); diff --git a/jstests/aggregation/mongos_merge.js b/jstests/aggregation/mongos_merge.js index d81f6e91cc8..13ab1b2431e 100644 --- a/jstests/aggregation/mongos_merge.js +++ b/jstests/aggregation/mongos_merge.js @@ -205,7 +205,8 @@ assertMergeOnMongoS({ testName: "agg_mongos_merge_geo_near", pipeline: [ - {$geoNear: {near: [0, 0], distanceField: "distance", spherical: true, limit: 300}} + {$geoNear: {near: [0, 0], distanceField: "distance", spherical: true}}, + {$limit: 300} ], allowDiskUse: allowDiskUse, expectedCount: 300 @@ -409,7 +410,7 @@ assertMergeOnMongoS({ testName: "agg_mongos_merge_all_mongos_runnable_stages", pipeline: [ - {$geoNear: {near: [0, 0], distanceField: "distance", spherical: true, limit: 400}}, + {$geoNear: {near: [0, 0], distanceField: "distance", spherical: true}}, {$sort: {a: 1}}, {$skip: 150}, {$limit: 150}, diff --git a/jstests/aggregation/sources/geonear/distancefield_and_includelocs.js b/jstests/aggregation/sources/geonear/distancefield_and_includelocs.js new file mode 100644 index 00000000000..540a3f61caf --- /dev/null +++ b/jstests/aggregation/sources/geonear/distancefield_and_includelocs.js @@ -0,0 +1,148 @@ +/** + * Tests the behavior of the $geoNear stage by varying 'distanceField' and 'includeLocs' + * (specifically, by specifying nested fields, overriding existing fields, and so on). + */ +(function() { + "use strict"; + + const coll = db.getCollection("geonear_distancefield_and_includelocs"); + coll.drop(); + + /** + * Runs an aggregation with a $geoNear stage using 'geoSpec' and an optional $project stage + * using 'projSpec'. Returns the first result; that is, the result closest to the "near" point. + */ + function firstGeoNearResult(geoSpec, projSpec) { + geoSpec.spherical = true; + const pipeline = [{$geoNear: geoSpec}, {$limit: 1}]; + if (projSpec) { + pipeline.push({$project: projSpec}); + } + + const res = coll.aggregate(pipeline).toArray(); + assert.eq(1, res.length, tojson(res)); + return res[0]; + } + + // Use documents with a variety of different fields: scalars, arrays, legacy points and GeoJSON + // objects. + const docWithLegacyPoint = { + _id: "legacy", + geo: [1, 1], + ptForNearQuery: [1, 1], + scalar: "foo", + arr: [{a: 1, b: 1}, {a: 2, b: 2}], + }; + const docWithGeoPoint = { + _id: "point", + geo: {type: "Point", coordinates: [1, 0]}, + ptForNearQuery: [1, 0], + scalar: "bar", + arr: [{a: 3, b: 3}, {a: 4, b: 4}], + }; + const docWithGeoLine = { + _id: "linestring", + geo: {type: "LineString", coordinates: [[0, 0], [-1, -1]]}, + ptForNearQuery: [-1, -1], + scalar: "baz", + arr: [{a: 5, b: 5}, {a: 6, b: 6}], + }; + + // We test with a 2dsphere index, since 2d indexes can't support GeoJSON objects. + assert.commandWorked(coll.createIndex({geo: "2dsphere"})); + + // Populate the collection. + assert.writeOK(coll.insert(docWithLegacyPoint)); + assert.writeOK(coll.insert(docWithGeoPoint)); + assert.writeOK(coll.insert(docWithGeoLine)); + + [docWithLegacyPoint, docWithGeoPoint, docWithGeoLine].forEach(doc => { + const docPlusNewFields = (newDoc) => Object.extend(Object.extend({}, doc), newDoc); + + // + // Tests for "distanceField". + // + const expectedDistance = 0; + + // Test that "distanceField" can be computed in a new field. + assert.docEq(firstGeoNearResult({near: doc.ptForNearQuery, distanceField: "newField"}), + docPlusNewFields({newField: expectedDistance})); + + // Test that "distanceField" can be computed in a new nested field. + assert.docEq(firstGeoNearResult({near: doc.ptForNearQuery, distanceField: "nested.field"}), + docPlusNewFields({nested: {field: expectedDistance}})); + + // Test that "distanceField" can overwrite an existing scalar field. + assert.docEq(firstGeoNearResult({near: doc.ptForNearQuery, distanceField: "scalar"}), + docPlusNewFields({scalar: expectedDistance})); + + // Test that "distanceField" can completely overwrite an existing array field. + assert.docEq(firstGeoNearResult({near: doc.ptForNearQuery, distanceField: "arr"}), + docPlusNewFields({arr: expectedDistance})); + + // TODO (SERVER-35561): When "includeLocs" shares a path prefix with an existing field, the + // fields are overwritten, even if they could be preserved. + assert.docEq(firstGeoNearResult({near: doc.ptForNearQuery, distanceField: "arr.b"}), + docPlusNewFields({arr: {b: expectedDistance}})); + + // + // Tests for both "includeLocs" and "distanceField". + // + + // Test that "distanceField" and "includeLocs" can both be specified. + assert.docEq(firstGeoNearResult( + {near: doc.ptForNearQuery, distanceField: "dist", includeLocs: "loc"}), + docPlusNewFields({dist: expectedDistance, loc: doc.geo})); + + // Test that "distanceField" and "includeLocs" can be the same path. The result is arbitrary + // ("includeLocs" wins). + assert.docEq( + firstGeoNearResult( + {near: doc.ptForNearQuery, distanceField: "newField", includeLocs: "newField"}), + docPlusNewFields({newField: doc.geo})); + + // Test that "distanceField" and "includeLocs" are both preserved when their paths share a + // prefix but do not conflict. + assert.docEq( + firstGeoNearResult( + {near: doc.ptForNearQuery, distanceField: "comp.dist", includeLocs: "comp.loc"}), + docPlusNewFields({comp: {dist: expectedDistance, loc: doc.geo}})); + + // + // Tests for "includeLocs" only. Project out the distance field. + // + const removeDistFieldProj = {d: 0}; + + // Test that "includeLocs" can be computed in a new field. + assert.docEq(firstGeoNearResult( + {near: doc.ptForNearQuery, distanceField: "d", includeLocs: "newField"}, + removeDistFieldProj), + docPlusNewFields({newField: doc.geo})); + + // Test that "includeLocs" can be computed in a new nested field. + assert.docEq( + firstGeoNearResult( + {near: doc.ptForNearQuery, distanceField: "d", includeLocs: "nested.field"}, + removeDistFieldProj), + docPlusNewFields({nested: {field: doc.geo}})); + + // Test that "includeLocs" can overwrite an existing scalar field. + assert.docEq(firstGeoNearResult( + {near: doc.ptForNearQuery, distanceField: "d", includeLocs: "scalar"}, + removeDistFieldProj), + docPlusNewFields({scalar: doc.geo})); + + // Test that "includeLocs" can completely overwrite an existing array field. + assert.docEq( + firstGeoNearResult({near: doc.ptForNearQuery, distanceField: "d", includeLocs: "arr"}, + removeDistFieldProj), + docPlusNewFields({arr: doc.geo})); + + // TODO (SERVER-35561): When "includeLocs" shares a path prefix with an existing field, the + // fields are overwritten, even if they could be preserved. + assert.docEq( + firstGeoNearResult({near: doc.ptForNearQuery, distanceField: "d", includeLocs: "arr.a"}, + removeDistFieldProj), + docPlusNewFields({arr: {a: doc.geo}})); + }); +}()); diff --git a/jstests/aggregation/sources/geonear/mindistance_and_maxdistance.js b/jstests/aggregation/sources/geonear/mindistance_and_maxdistance.js new file mode 100644 index 00000000000..7c5e6c750f3 --- /dev/null +++ b/jstests/aggregation/sources/geonear/mindistance_and_maxdistance.js @@ -0,0 +1,99 @@ +/** + * Tests the behavior of the $geoNear stage with varying values of 'minDistance' and 'maxDistance'. + */ +(function() { + "use strict"; + + const coll = db.getCollection("geonear_mindistance_maxdistance"); + + const kMaxDistance = Math.PI * 2.0; + + // Test points that are exactly at the "near" point, close to the point, and far from the point. + // Distances are purposely chosen to be small so that distances in meters and radians are close. + const origin = {pt: [0, 0]}; + const near = {pt: [0.23, -0.32]}; + const far = {pt: [5.9, 0.0]}; + + ["2d", "2dsphere"].forEach(geoType => { + jsTestLog(`Testing $geoNear with index {pt: "${geoType}"}`); + coll.drop(); + + // Create the desired index type and populate the collection. + assert.commandWorked(coll.createIndex({pt: geoType})); + [origin, near, far].forEach(doc => { + doc.distFromOrigin = (geoType === "2dsphere") ? Geo.sphereDistance(doc.pt, origin.pt) + : Geo.distance(doc.pt, origin.pt); + assert.commandWorked(coll.insert(doc)); + }); + + /** + * Helper function that runs a $geoNear aggregation near the origin, setting the minimum + * and/or maximum search distance using the object 'minMaxOpts', and asserting that the + * results match 'expected'. + */ + function assertGeoNearResults(minMaxOpts, expected) { + const geoNearStage = { + $geoNear: Object.extend( + {near: origin.pt, distanceField: "dist", spherical: (geoType === "2dsphere")}, + minMaxOpts) + }; + const projStage = {$project: {_id: 0, dist: 0}}; + const res = coll.aggregate([geoNearStage, projStage]).toArray(); + assert.eq( + res, + expected, + () => `Unexpected results from ${tojson(geoNearStage)} using a ${geoType} index`); + } + + // If no minimum nor maximum distance is set, all points are returned. + assertGeoNearResults({}, [origin, near, far]); + + // + // Tests for minDistance. + // + + // Negative values and non-numeric values are illegal. + assert.throws(() => assertGeoNearResults({minDistance: -1.1})); + assert.throws(() => assertGeoNearResults({minDistance: "3.2"})); + + // A minimum distance of 0 returns all points. + assertGeoNearResults({minDistance: -0.0}, [origin, near, far]); + assertGeoNearResults({minDistance: 0.0}, [origin, near, far]); + + // Larger minimum distances exclude closer points. + assertGeoNearResults({minDistance: (near.distFromOrigin / 2)}, [near, far]); + assertGeoNearResults({minDistance: (far.distFromOrigin / 2)}, [far]); + assertGeoNearResults({minDistance: kMaxDistance}, []); + + // + // Tests for maxDistance. + // + + // Negative values and non-numeric values are illegal. + assert.throws(() => assertGeoNearResults({maxDistance: -1.1})); + assert.throws(() => assertGeoNearResults({maxDistance: "3.2"})); + + // A maximum distance of 0 returns only the origin. + assertGeoNearResults({maxDistance: 0.0}, [origin]); + assertGeoNearResults({maxDistance: -0.0}, [origin]); + + // Larger maximum distances include more points. + assertGeoNearResults({maxDistance: (near.distFromOrigin + 0.01)}, [origin, near]); + assertGeoNearResults({maxDistance: (far.distFromOrigin + 0.01)}, [origin, near, far]); + + // + // Tests for minDistance and maxDistance together. + // + + // Cast a wide net and all points should be returned. + assertGeoNearResults({minDistance: 0.0, maxDistance: kMaxDistance}, [origin, near, far]); + + // A narrower range excludes the origin and the far point. + assertGeoNearResults( + {minDistance: (near.distFromOrigin / 2), maxDistance: (near.distFromOrigin + 0.01)}, + [near]); + + // An impossible range is legal but returns no results. + assertGeoNearResults({minDistance: 3.0, maxDistance: 1.0}, []); + }); +}()); diff --git a/jstests/auth/lib/commands_lib.js b/jstests/auth/lib/commands_lib.js index 9a9b724e015..329659e295d 100644 --- a/jstests/auth/lib/commands_lib.js +++ b/jstests/auth/lib/commands_lib.js @@ -1719,6 +1719,35 @@ var authCommandsLib = { ] }, { + testname: "aggregate_geoNear", + command: { + aggregate: "coll", + cursor: {}, + pipeline: [{$geoNear: {near: [50, 50], distanceField: "dist"}}] + }, + setup: (db) => { + db.coll.drop(); + assert.commandWorked(db.coll.createIndex({loc: "2d"})); + assert.commandWorked(db.coll.insert({loc: [45.32, 51.12]})); + }, + teardown: (db) => { + db.coll.drop(); + }, + testcases: [ + { + runOnDb: firstDbName, + roles: roles_read, + privileges: [{resource: {db: firstDbName, collection: "coll"}, actions: ["find"]}] + }, + { + runOnDb: secondDbName, + roles: roles_readAny, + privileges: + [{resource: {db: secondDbName, collection: "coll"}, actions: ["find"]}] + }, + ], + }, + { testname: "buildInfo", command: {buildInfo: 1}, testcases: [ @@ -3903,30 +3932,6 @@ var authCommandsLib = { ] }, { - testname: "geoNear", - command: {geoNear: "x", near: [50, 50], num: 1}, - setup: function(db) { - db.x.drop(); - db.x.save({loc: [50, 50]}); - db.x.ensureIndex({loc: "2d"}); - }, - teardown: function(db) { - db.x.drop(); - }, - testcases: [ - { - runOnDb: firstDbName, - roles: roles_read, - privileges: [{resource: {db: firstDbName, collection: "x"}, actions: ["find"]}] - }, - { - runOnDb: secondDbName, - roles: roles_readAny, - privileges: [{resource: {db: secondDbName, collection: "x"}, actions: ["find"]}] - } - ] - }, - { testname: "geoSearch", command: {geoSearch: "x", near: [50, 50], maxDistance: 6, limit: 1, search: {}}, skipSharded: true, diff --git a/jstests/concurrency/fsm_workloads/yield_geo_near.js b/jstests/concurrency/fsm_workloads/yield_geo_near.js index 3ed79835906..b025b7ec23b 100644 --- a/jstests/concurrency/fsm_workloads/yield_geo_near.js +++ b/jstests/concurrency/fsm_workloads/yield_geo_near.js @@ -1,35 +1,40 @@ 'use strict'; /* - * yield_geo_near.js (extends yield.js) - * - * Intersperse geo $near queries with updates and deletes of documents they may match. + * Intersperses $geoNear aggregations with updates and deletes of documents they may match. + * @tags: [requires_non_retryable_writes] */ load('jstests/concurrency/fsm_libs/extend_workload.js'); // for extendWorkload load('jstests/concurrency/fsm_workloads/yield.js'); // for $config var $config = extendWorkload($config, function($config, $super) { - - /* - * Use geo $near query to find points near the origin. Note this should be done using the - * geoNear command, rather than a $near query, as the $near query doesn't work in a sharded - * environment. Unfortunately this means we cannot batch the request. - */ $config.states.query = function geoNear(db, collName) { // This distance gets about 80 docs around the origin. There is one doc inserted // every 1m^2 and the area scanned by a 5m radius is PI*(5m)^2 ~ 79. - var maxDistance = 5; + const maxDistance = 5; + const cursor = db[collName].aggregate([{ + $geoNear: { + near: [0, 0], + distanceField: "dist", + maxDistance: maxDistance, + } + }]); - var res = db.runCommand({geoNear: collName, near: [0, 0], maxDistance: maxDistance}); - assertWhenOwnColl.commandWorked(res); // Could fail if more than 1 2d index. + // We only run the verification when workloads are run on separate collections, since the + // aggregation may fail if we don't have exactly one 2d index to use. assertWhenOwnColl(function verifyResults() { - var results = res.results; - var prevDoc = {dis: 0}; // distance should never be less than 0 - for (var i = 0; i < results.length; i++) { - var doc = results[i]; - assertAlways.lte(NumberInt(doc.dis), maxDistance); // satisfies query - assertAlways.lte(prevDoc.dis, doc.dis); // returned in the correct order - prevDoc = doc; + // We manually verify the results ourselves rather than calling advanceCursor(). In the + // event of a failure, the aggregation cursor cannot support explain(). + let lastDistanceSeen = 0; + while (cursor.hasNext()) { + const doc = cursor.next(); + assertAlways.lte(doc.dist, + maxDistance, + `dist in ${tojson(doc)} exceeds max allowable $geoNear distance`); + assertAlways.lte(lastDistanceSeen, + doc.dist, + `dist in ${tojson(doc)} is not less than the previous distance`); + lastDistanceSeen = doc.dist; } }); }; diff --git a/jstests/concurrency/fsm_workloads/yield_geo_near_dedup.js b/jstests/concurrency/fsm_workloads/yield_geo_near_dedup.js index 9f3476873f7..ac338902911 100644 --- a/jstests/concurrency/fsm_workloads/yield_geo_near_dedup.js +++ b/jstests/concurrency/fsm_workloads/yield_geo_near_dedup.js @@ -1,9 +1,8 @@ 'use strict'; /* - * yield_geo_near_dedup.js (extends yield_geo_near.js) - * - * Intersperse geo $near queries with updates of non-geo fields to test deduplication. + * Intersperses $geoNear aggregations with updates of non-geo fields to test deduplication. + * @tags: [requires_non_retryable_writes] */ load('jstests/concurrency/fsm_libs/extend_workload.js'); // for extendWorkload load('jstests/concurrency/fsm_workloads/yield_geo_near.js'); // for $config @@ -43,27 +42,31 @@ var $config = extendWorkload($config, function($config, $super) { $config.states.query = function geoNear(db, collName) { // This distance gets about 80 docs around the origin. There is one doc inserted // every 1m^2 and the area scanned by a 5m radius is PI*(5m)^2 ~ 79. - var maxDistance = 5; + const maxDistance = 5; + const cursor = db[collName].aggregate([{ + $geoNear: { + near: [0, 0], + distanceField: "dist", + maxDistance: maxDistance, + spherical: true, + } + }]); - var res = db.runCommand( - {geoNear: collName, near: [0, 0], maxDistance: maxDistance, spherical: true}); - assertWhenOwnColl.commandWorked(res); + // We only run the verification when workloads are run on separate collections, since the + // aggregation may fail if we don't have exactly one 2d index to use. assertWhenOwnColl(function verifyResults() { - var results = res.results; - var seenObjs = []; - for (var i = 0; i < results.length; i++) { - var doc = results[i].obj; + const seenObjs = []; + while (cursor.hasNext()) { + const doc = cursor.next(); // The pair (_id, timesInserted) is the smallest set of attributes that uniquely // identifies a document. - var objToSearchFor = {_id: doc._id, timesInserted: doc.timesInserted}; - var found = seenObjs.some(function(obj) { - return bsonWoCompare(obj, objToSearchFor) === 0; - }); + const objToSearch = {_id: doc._id, timesInserted: doc.timesInserted}; + const found = seenObjs.some(obj => bsonWoCompare(obj, objToSearch) === 0); assertWhenOwnColl(!found, - 'geoNear command returned the document ' + tojson(doc) + - ' multiple times: ' + tojson(seenObjs)); - seenObjs.push(objToSearchFor); + `$geoNear returned document ${tojson(doc)} multiple ` + + `times: ${tojson(seenObjs)}`); + seenObjs.push(objToSearch); } }); }; diff --git a/jstests/core/collation.js b/jstests/core/collation.js index 8458db5e4b3..44eea63d21c 100644 --- a/jstests/core/collation.js +++ b/jstests/core/collation.js @@ -1306,153 +1306,99 @@ } // - // Collation tests for geoNear. + // Collation tests for the $geoNear aggregation stage. // - // geoNear should return "collection doesn't exist" error when collation specified and - // collection does not exist. + // $geoNear should fail when collation is specified but the collection does not exist. coll.drop(); - assert.commandFailed(db.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {str: "ABC"}, + assert.commandFailedWithCode(db.runCommand({ + aggregate: coll.getName(), + cursor: {}, + pipeline: [{ + $geoNear: { + near: {type: "Point", coordinates: [0, 0]}, + distanceField: "dist", + } + }], collation: {locale: "en_US", strength: 2} - })); + }), + ErrorCodes.NamespaceNotFound); - // geoNear should return correct results when collation specified and string predicate not - // indexed. + // $geoNear rejects the now-deprecated "collation" option. coll.drop(); assert.writeOK(coll.insert({geo: {type: "Point", coordinates: [0, 0]}, str: "abc"})); + assert.commandFailedWithCode(db.runCommand({ + aggregate: coll.getName(), + cursor: {}, + pipeline: [{ + $geoNear: { + near: {type: "Point", coordinates: [0, 0]}, + distanceField: "dist", + collation: {locale: "en_US"}, + } + }], + }), + 40227); + + const geoNearStage = { + $geoNear: { + near: {type: "Point", coordinates: [0, 0]}, + distanceField: "dist", + spherical: true, + query: {str: "ABC"} + } + }; + + // $geoNear should return correct results when collation specified and string predicate not + // indexed. assert.commandWorked(coll.ensureIndex({geo: "2dsphere"})); - assert.eq(0, - assert - .commandWorked(db.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {str: "ABC"} - })) - .results.length); - assert.eq(1, - assert - .commandWorked(db.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {str: "ABC"}, - collation: {locale: "en_US", strength: 2} - })) - .results.length); - - // geoNear should return correct results when no collation specified and string predicate + assert.eq(0, coll.aggregate([geoNearStage]).itcount()); + assert.eq( + 1, coll.aggregate([geoNearStage], {collation: {locale: "en_US", strength: 2}}).itcount()); + + // $geoNear should return correct results when no collation specified and string predicate // indexed. assert.commandWorked(coll.dropIndexes()); assert.commandWorked(coll.ensureIndex({geo: "2dsphere", str: 1})); - assert.eq(0, - assert - .commandWorked(db.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {str: "ABC"} - })) - .results.length); - assert.eq(1, - assert - .commandWorked(db.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {str: "ABC"}, - collation: {locale: "en_US", strength: 2} - })) - .results.length); - - // geoNear should return correct results when collation specified and collation on index is + assert.eq(0, coll.aggregate([geoNearStage]).itcount()); + assert.eq( + 1, coll.aggregate([geoNearStage], {collation: {locale: "en_US", strength: 2}}).itcount()); + + // $geoNear should return correct results when collation specified and collation on index is // incompatible with string predicate. assert.commandWorked(coll.dropIndexes()); assert.commandWorked( coll.ensureIndex({geo: "2dsphere", str: 1}, {collation: {locale: "en_US", strength: 3}})); - assert.eq(0, - assert - .commandWorked(db.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {str: "ABC"} - })) - .results.length); - assert.eq(1, - assert - .commandWorked(db.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {str: "ABC"}, - collation: {locale: "en_US", strength: 2} - })) - .results.length); - - // geoNear should return correct results when collation specified and collation on index is + assert.eq(0, coll.aggregate([geoNearStage]).itcount()); + assert.eq( + 1, coll.aggregate([geoNearStage], {collation: {locale: "en_US", strength: 2}}).itcount()); + + // $geoNear should return correct results when collation specified and collation on index is // compatible with string predicate. assert.commandWorked(coll.dropIndexes()); assert.commandWorked( coll.ensureIndex({geo: "2dsphere", str: 1}, {collation: {locale: "en_US", strength: 2}})); - assert.eq(0, - assert - .commandWorked(db.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {str: "ABC"} - })) - .results.length); - assert.eq(1, - assert - .commandWorked(db.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {str: "ABC"}, - collation: {locale: "en_US", strength: 2} - })) - .results.length); - - // geoNear should return correct results when no collation specified and collection has a + assert.eq(0, coll.aggregate([geoNearStage]).itcount()); + assert.eq( + 1, coll.aggregate([geoNearStage], {collation: {locale: "en_US", strength: 2}}).itcount()); + + // $geoNear should return correct results when no collation specified and collection has a // default collation. coll.drop(); assert.commandWorked( db.createCollection(coll.getName(), {collation: {locale: "en_US", strength: 2}})); assert.commandWorked(coll.ensureIndex({geo: "2dsphere"})); assert.writeOK(coll.insert({geo: {type: "Point", coordinates: [0, 0]}, str: "abc"})); - assert.eq(1, - assert - .commandWorked(db.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {str: "ABC"} - })) - .results.length); - - // geoNear should return correct results when "simple" collation specified and collection has + assert.eq(1, coll.aggregate([geoNearStage]).itcount()); + + // $geoNear should return correct results when "simple" collation specified and collection has // a default collation. coll.drop(); assert.commandWorked( db.createCollection(coll.getName(), {collation: {locale: "en_US", strength: 2}})); assert.commandWorked(coll.ensureIndex({geo: "2dsphere"})); assert.writeOK(coll.insert({geo: {type: "Point", coordinates: [0, 0]}, str: "abc"})); - assert.eq(0, - assert - .commandWorked(db.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {str: "ABC"}, - collation: {locale: "simple"} - })) - .results.length); + assert.eq(0, coll.aggregate([geoNearStage], {collation: {locale: "simple"}}).itcount()); // // Collation tests for find with $nearSphere. diff --git a/jstests/core/commands_namespace_parsing.js b/jstests/core/commands_namespace_parsing.js index c3ba15e37a9..ca7f287c6cb 100644 --- a/jstests/core/commands_namespace_parsing.js +++ b/jstests/core/commands_namespace_parsing.js @@ -100,10 +100,6 @@ isNotFullyQualified, isNotAdminCommand); - // Test geoNear fails with an invalid collection name. - assertFailsWithInvalidNamespacesForField( - "geoNear", {geoNear: "", near: [0.0, 0.0]}, isNotFullyQualified, isNotAdminCommand); - if (!isMongos) { // Test geoSearch fails with an invalid collection name. assertFailsWithInvalidNamespacesForField( diff --git a/jstests/core/expr.js b/jstests/core/expr.js index 541c32a4bdc..5a5284474d2 100644 --- a/jstests/core/expr.js +++ b/jstests/core/expr.js @@ -172,32 +172,38 @@ } // - // $expr in geoNear. + // $expr in the $geoNear stage. // coll.drop(); assert.writeOK(coll.insert({geo: {type: "Point", coordinates: [0, 0]}, a: 0})); assert.commandWorked(coll.ensureIndex({geo: "2dsphere"})); assert.eq(1, - assert - .commandWorked(db.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {$expr: {$eq: ["$a", 0]}} - })) - .results.length); - assert.commandFailed(db.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {$expr: {$eq: ["$a", "$$unbound"]}} + coll.aggregate({ + $geoNear: { + near: {type: "Point", coordinates: [0, 0]}, + distanceField: "dist", + spherical: true, + query: {$expr: {$eq: ["$a", 0]}} + } + }) + .toArray() + .length); + assert.throws(() => coll.aggregate({ + $geoNear: { + near: {type: "Point", coordinates: [0, 0]}, + distanceField: "dist", + spherical: true, + query: {$expr: {$eq: ["$a", "$$unbound"]}} + } })); - assert.commandFailed(db.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {$expr: {$divide: [1, "$a"]}} + assert.throws(() => coll.aggregate({ + $geoNear: { + near: {type: "Point", coordinates: [0, 0]}, + distanceField: "dist", + spherical: true, + query: {$expr: {$divide: [1, "$a"]}} + } })); // diff --git a/jstests/core/geo2.js b/jstests/core/geo2.js index 558e6c81099..e9fe6239677 100644 --- a/jstests/core/geo2.js +++ b/jstests/core/geo2.js @@ -16,8 +16,6 @@ assert.eq(t.count(), n - 1); t.ensureIndex({loc: "2d"}); -fast = db.runCommand({geoNear: t.getName(), near: [50, 50], num: 10}); - function a(cur) { var total = 0; var outof = 0; @@ -29,9 +27,7 @@ function a(cur) { return total / outof; } -assert.close(fast.stats.avgDistance, a(t.find({loc: {$near: [50, 50]}}).limit(10)), "B1"); assert.close(1.33333, a(t.find({loc: {$near: [50, 50]}}).limit(3)), "B2"); -assert.close(fast.stats.avgDistance, a(t.find({loc: {$near: [50, 50]}}).limit(10)), "B3"); printjson(t.find({loc: {$near: [50, 50]}}).explain()); diff --git a/jstests/core/geo3.js b/jstests/core/geo3.js index de8e1b2bf6a..e1afa5da5eb 100644 --- a/jstests/core/geo3.js +++ b/jstests/core/geo3.js @@ -1,81 +1,77 @@ // @tags: [requires_fastcount] -t = db.geo3; -t.drop(); - -n = 1; -arr = []; -for (var x = -100; x < 100; x += 2) { - for (var y = -100; y < 100; y += 2) { - arr.push({_id: n++, loc: [x, y], a: Math.abs(x) % 5, b: Math.abs(y) % 5}); +(function() { + t = db.geo3; + t.drop(); + + n = 1; + arr = []; + for (var x = -100; x < 100; x += 2) { + for (var y = -100; y < 100; y += 2) { + arr.push({_id: n++, loc: [x, y], a: Math.abs(x) % 5, b: Math.abs(y) % 5}); + } } -} -t.insert(arr); -assert.eq(t.count(), 100 * 100); -assert.eq(t.count(), n - 1); - -t.ensureIndex({loc: "2d"}); - -fast = db.runCommand({geoNear: t.getName(), near: [50, 50], num: 10}); - -// test filter - -filtered1 = db.runCommand({geoNear: t.getName(), near: [50, 50], num: 10, query: {a: 2}}); -assert.eq(10, filtered1.results.length, "B1"); -filtered1.results.forEach(function(z) { - assert.eq(2, z.obj.a, "B2: " + tojson(z)); -}); -// printjson( filtered1.stats ); - -function avgA(q, len) { - if (!len) - len = 10; - var realq = {loc: {$near: [50, 50]}}; - if (q) - Object.extend(realq, q); - var as = t.find(realq).limit(len).map(function(z) { - return z.a; - }); - assert.eq(len, as.length, "length in avgA"); - return Array.avg(as); -} - -function testFiltering(msg) { - assert.gt(2, avgA({}), msg + " testFiltering 1 "); - assert.eq(2, avgA({a: 2}), msg + " testFiltering 2 "); - assert.eq(4, avgA({a: 4}), msg + " testFiltering 3 "); -} - -testFiltering("just loc"); - -assert.commandWorked(t.dropIndex({loc: "2d"})); -assert.commandWorked(t.ensureIndex({loc: "2d", a: 1})); - -filtered2 = db.runCommand({geoNear: t.getName(), near: [50, 50], num: 10, query: {a: 2}}); -assert.eq(10, filtered2.results.length, "B3"); -filtered2.results.forEach(function(z) { - assert.eq(2, z.obj.a, "B4: " + tojson(z)); -}); - -assert.eq(filtered1.stats.avgDistance, filtered2.stats.avgDistance, "C1"); -assert.gt(filtered1.stats.objectsLoaded, filtered2.stats.objectsLoaded, "C3"); - -testFiltering("loc and a"); - -assert.commandWorked(t.dropIndex({loc: "2d", a: 1})); -assert.commandWorked(t.ensureIndex({loc: "2d", b: 1})); - -testFiltering("loc and b"); - -q = { - loc: {$near: [50, 50]} -}; -assert.eq(100, t.find(q).limit(100).itcount(), "D1"); -assert.eq(100, t.find(q).limit(100).size(), "D2"); - -assert.eq(20, t.find(q).limit(20).itcount(), "D3"); -assert.eq(20, t.find(q).limit(20).size(), "D4"); - -// SERVER-14039 Wrong limit after skip with $nearSphere, 2d index -assert.eq(10, t.find(q).skip(10).limit(10).itcount(), "D5"); -assert.eq(10, t.find(q).skip(10).limit(10).size(), "D6"); + t.insert(arr); + assert.eq(t.count(), 100 * 100); + assert.eq(t.count(), n - 1); + + t.ensureIndex({loc: "2d"}); + + // Test the "query" parameter in $geoNear. + + let res = t.aggregate([ + {$geoNear: {near: [50, 50], distanceField: "dist", query: {a: 2}}}, + {$limit: 10}, + ]).toArray(); + assert.eq(10, res.length, tojson(res)); + res.forEach(doc => assert.eq(2, doc.a, tojson(doc))); + + function avgA(q, len) { + if (!len) + len = 10; + var realq = {loc: {$near: [50, 50]}}; + if (q) + Object.extend(realq, q); + var as = t.find(realq).limit(len).map(function(z) { + return z.a; + }); + assert.eq(len, as.length, "length in avgA"); + return Array.avg(as); + } + + function testFiltering(msg) { + assert.gt(2, avgA({}), msg + " testFiltering 1 "); + assert.eq(2, avgA({a: 2}), msg + " testFiltering 2 "); + assert.eq(4, avgA({a: 4}), msg + " testFiltering 3 "); + } + + testFiltering("just loc"); + + assert.commandWorked(t.dropIndex({loc: "2d"})); + assert.commandWorked(t.ensureIndex({loc: "2d", a: 1})); + + res = t.aggregate([ + {$geoNear: {near: [50, 50], distanceField: "dist", query: {a: 2}}}, + {$limit: 10}, + ]).toArray(); + assert.eq(10, res.length, "B3"); + res.forEach(doc => assert.eq(2, doc.a, tojson(doc))); + + testFiltering("loc and a"); + + assert.commandWorked(t.dropIndex({loc: "2d", a: 1})); + assert.commandWorked(t.ensureIndex({loc: "2d", b: 1})); + + testFiltering("loc and b"); + + q = {loc: {$near: [50, 50]}}; + assert.eq(100, t.find(q).limit(100).itcount(), "D1"); + assert.eq(100, t.find(q).limit(100).size(), "D2"); + + assert.eq(20, t.find(q).limit(20).itcount(), "D3"); + assert.eq(20, t.find(q).limit(20).size(), "D4"); + + // SERVER-14039 Wrong limit after skip with $nearSphere, 2d index + assert.eq(10, t.find(q).skip(10).limit(10).itcount(), "D5"); + assert.eq(10, t.find(q).skip(10).limit(10).size(), "D6"); +}()); diff --git a/jstests/core/geo5.js b/jstests/core/geo5.js deleted file mode 100644 index bbaa84c1d17..00000000000 --- a/jstests/core/geo5.js +++ /dev/null @@ -1,17 +0,0 @@ -t = db.geo5; -t.drop(); - -t.insert({p: [0, 0]}); -t.ensureIndex({p: "2d"}); - -res = t.runCommand("geoNear", {near: [1, 1]}); -assert.eq(1, res.results.length, "A1"); - -t.insert({p: [1, 1]}); -t.insert({p: [-1, -1]}); -res = t.runCommand("geoNear", {near: [50, 50]}); -assert.eq(3, res.results.length, "A2"); - -t.insert({p: [-1, -1]}); -res = t.runCommand("geoNear", {near: [50, 50]}); -assert.eq(4, res.results.length, "A3"); diff --git a/jstests/core/geo_array2.js b/jstests/core/geo_array2.js index 6195e038de3..bd9a8507999 100644 --- a/jstests/core/geo_array2.js +++ b/jstests/core/geo_array2.js @@ -40,9 +40,10 @@ for (var t = 0; t < 2; t++) { // Do near check var nearResults = - db.runCommand({geoNear: "geoarray2", near: center, num: count, query: {type: type}}) - .results; - // printjson( nearResults ) + db.geoarray2 + .find({loc: {$near: center}, type: type}, {dis: {$meta: "geoNearDistance"}}) + .limit(count) + .toArray(); var objsFound = {}; var lastResult = 0; @@ -51,12 +52,10 @@ for (var t = 0; t < 2; t++) { assert.gt(1.5, nearResults[k].dis); // Distances should be increasing assert.lte(lastResult, nearResults[k].dis); - // Objs should be of the right type - assert.eq(type, nearResults[k].obj.type); lastResult = nearResults[k].dis; - var objKey = "" + nearResults[k].obj._id; + var objKey = "" + nearResults[k]._id; if (objKey in objsFound) objsFound[objKey]++; diff --git a/jstests/core/geo_borders.js b/jstests/core/geo_borders.js index f8a94d997dd..7f7a3eee13b 100644 --- a/jstests/core/geo_borders.js +++ b/jstests/core/geo_borders.js @@ -184,22 +184,25 @@ assert.eq(4, t.find({loc: {$near: offCenter, $maxDistance: step * 1.9}}).count() // Command Tests // ************** // Make sure we can get all nearby points to point in range -assert.eq(overallMax, db.runCommand({geoNear: "borders", near: offCenter}).results[0].obj.loc.y); +assert.eq(overallMax, + t.aggregate({$geoNear: {near: offCenter, distanceField: "d"}}).toArray()[0].loc.y); // Make sure we can get all nearby points to point on boundary -assert.eq(overallMin, db.runCommand({geoNear: "borders", near: onBoundsNeg}).results[0].obj.loc.y); +assert.eq(overallMin, + t.aggregate({$geoNear: {near: onBoundsNeg, distanceField: "d"}}).toArray()[0].loc.y); // Make sure we can't get all nearby points to point over boundary -// TODO: SERVER-9986 clean up wrapping rules for different CRS queries - not sure this is an error -/* -assert.commandFailed( db.runCommand( { geoNear : "borders", near : offBounds } )); -*/ - -// Make sure we can't get all nearby points to point on max boundary -assert.commandWorked(db.runCommand({geoNear: "borders", near: onBounds})); +assert.commandFailedWithCode(db.runCommand({ + aggregate: "borders", + cursor: {}, + pipeline: [{$geoNear: {near: offBounds, distanceField: "d"}}] +}), + 16433); +assert.eq(numItems, t.aggregate({$geoNear: {near: onBounds, distanceField: "d"}}).toArray().length); // Make sure we can get all nearby points within one step (4 points in top // corner) -assert.eq( - 4, - db.runCommand({geoNear: "borders", near: offCenter, maxDistance: step * 1.5}).results.length); +assert.eq(4, + t.aggregate({$geoNear: {near: offCenter, maxDistance: step * 1.5, distanceField: "d"}}) + .toArray() + .length); diff --git a/jstests/core/geo_center_sphere2.js b/jstests/core/geo_center_sphere2.js index 29865844813..761cb5b7403 100644 --- a/jstests/core/geo_center_sphere2.js +++ b/jstests/core/geo_center_sphere2.js @@ -126,36 +126,24 @@ for (var test = 0; test < numTests; test++) { distance = minNewDistance; } - // geoNear - results = db.runCommand({ - geoNear: "sphere", - near: startPoint, - maxDistance: radius, - num: 2 * pointsIn, - spherical: true - }).results; - - /* - printjson( results ); - - for ( var j = 0; j < results[0].obj.loc.length; j++ ) { - var newDistance = Geo.sphereDistance( startPoint, results[0].obj.loc[j] ) - if( newDistance <= radius ) print( results[0].obj.loc[j] + " : " + newDistance ) - } - */ - - assert.eq(docsIn, results.length); + // Test $geoNear. + results = t.aggregate({ + $geoNear: { + near: startPoint, + distanceField: "dis", + maxDistance: radius, + spherical: true, + } + }).toArray(); + assert.eq(docsIn, results.length, tojson(results)); var distance = 0; for (var i = 0; i < results.length; i++) { var retDistance = results[i].dis; - // print( "Dist from : " + results[i].loc + " to " + startPoint + " is " - // + retDistance + " vs " + radius ) - var distInObj = false; - for (var j = 0; j < results[i].obj.loc.length && distInObj == false; j++) { - var newDistance = Geo.sphereDistance(startPoint, results[i].obj.loc[j]); + for (var j = 0; j < results[i].loc.length && distInObj == false; j++) { + var newDistance = Geo.sphereDistance(startPoint, results[i].loc[j]); distInObj = (newDistance >= retDistance - 0.0001 && newDistance <= retDistance + 0.0001); } diff --git a/jstests/core/geo_mindistance.js b/jstests/core/geo_mindistance.js index fe694ea2c0e..92ccc617cf5 100644 --- a/jstests/core/geo_mindistance.js +++ b/jstests/core/geo_mindistance.js @@ -1,6 +1,5 @@ -// Test $minDistance option for $near and $nearSphere queries, and geoNear command. SERVER-9395. -// -// @tags: [requires_fastcount] +// Test $minDistance option for $near and $nearSphere queries, and the $geoNear aggregation stage. +// @tags: [requires_fastcount, requires_getmore] (function() { "use strict"; @@ -24,16 +23,20 @@ * the existing $maxDistance option to test the newer $minDistance option's behavior. */ function n_docs_within(radius_km) { - // geoNear's distances are in meters for geoJSON points. - var cmdResult = db.runCommand({ - geoNear: t.getName(), - near: {type: 'Point', coordinates: [0, 0]}, - spherical: true, - maxDistance: radius_km * km, - num: 1000 - }); - - return cmdResult.results.length; + // $geoNear's distances are in meters for geoJSON points. + return t + .aggregate([ + { + $geoNear: { + near: {type: 'Point', coordinates: [0, 0]}, + distanceField: "dis", + spherical: true, + maxDistance: radius_km * km, + } + }, + {$limit: 1000} + ]) + .itcount(); } // @@ -133,62 +136,68 @@ n_bw500_and_1000_count); // - // Test geoNear command with GeoJSON point. Distances are in meters. + // Test $geoNear aggregation stage with GeoJSON point. Distances are in meters. // - var cmdResult = db.runCommand({ - geoNear: t.getName(), - near: {type: 'Point', coordinates: [0, 0]}, - minDistance: 1400 * km, - spherical: true // spherical required for 2dsphere index - }); + let geoNearCount = t.aggregate({ + $geoNear: { + near: {type: 'Point', coordinates: [0, 0]}, + minDistance: 1400 * km, + spherical: true, + distanceField: "d", + } + }).itcount(); assert.eq(n_docs - n_docs_within(1400), - cmdResult.results.length, + geoNearCount, "Expected " + (n_docs - n_docs_within(1400)) + - " points geoNear (0, 0) with $minDistance 1400 km, got " + - cmdResult.results.length); - - cmdResult = db.runCommand({ - geoNear: t.getName(), - near: {type: 'Point', coordinates: [0, 0]}, - minDistance: 500 * km, - maxDistance: 1000 * km, - spherical: true - }); + " points geoNear (0, 0) with $minDistance 1400 km, got " + geoNearCount); + + geoNearCount = t.aggregate({ + $geoNear: { + near: {type: 'Point', coordinates: [0, 0]}, + minDistance: 500 * km, + maxDistance: 1000 * km, + spherical: true, + distanceField: "d", + } + }).itcount(); assert.eq(n_docs_within(1000) - n_docs_within(500), - cmdResult.results.length, + geoNearCount, "Expected " + (n_docs_within(1000) - n_docs_within(500)) + " points geoNear (0, 0) with $minDistance 500 km and $maxDistance 1000 km, got " + - cmdResult.results.length); + geoNearCount); // - // Test geoNear command with legacy point. Distances are in radians. + // Test $geoNear aggregation stage with legacy point. Distances are in radians. // - cmdResult = db.runCommand({ - geoNear: t.getName(), - near: legacyPoint, - minDistance: metersToRadians(1400 * km), - spherical: true // spherical required for 2dsphere index - }); + geoNearCount = t.aggregate({ + $geoNear: { + near: legacyPoint, + minDistance: metersToRadians(1400 * km), + spherical: true, + distanceField: "d", + } + }).itcount(); assert.eq(n_docs - n_docs_within(1400), - cmdResult.results.length, + geoNearCount, "Expected " + (n_docs - n_docs_within(1400)) + - " points geoNear (0, 0) with $minDistance 1400 km, got " + - cmdResult.results.length); - - cmdResult = db.runCommand({ - geoNear: t.getName(), - near: legacyPoint, - minDistance: metersToRadians(500 * km), - maxDistance: metersToRadians(1000 * km), - spherical: true - }); + " points geoNear (0, 0) with $minDistance 1400 km, got " + geoNearCount); + + geoNearCount = t.aggregate({ + $geoNear: { + near: legacyPoint, + minDistance: metersToRadians(500 * km), + maxDistance: metersToRadians(1000 * km), + spherical: true, + distanceField: "d", + } + }).itcount(); assert.eq(n_docs_within(1000) - n_docs_within(500), - cmdResult.results.length, + geoNearCount, "Expected " + (n_docs_within(1000) - n_docs_within(500)) + " points geoNear (0, 0) with $minDistance 500 km and $maxDistance 1000 km, got " + - cmdResult.results.length); + geoNearCount); t.drop(); assert.commandWorked(t.createIndex({loc: "2d"})); @@ -204,32 +213,42 @@ assert.eq(3, t.find({loc: {$nearSphere: [0, 0]}}).itcount()); assert.eq(1, t.find({loc: {$nearSphere: [0, 0], $minDistance: deg2rad(41.5)}}).itcount()); - // Test minDistance for 2d index with geoNear command and spherical=false. - cmdResult = db.runCommand({geoNear: t.getName(), near: [0, 0], spherical: false}); - assert.commandWorked(cmdResult); - assert.eq(3, cmdResult.results.length); - assert.eq(40, cmdResult.results[0].dis); - assert.eq(41, cmdResult.results[1].dis); - assert.eq(42, cmdResult.results[2].dis); - - cmdResult = - db.runCommand({geoNear: t.getName(), near: [0, 0], minDistance: 41.5, spherical: false}); - assert.commandWorked(cmdResult); - assert.eq(1, cmdResult.results.length); - assert.eq(42, cmdResult.results[0].dis); - - // Test minDistance for 2d index with geoNear command and spherical=true. Distances are in + // Test minDistance for 2d index with $geoNear stage and spherical=false. + let cmdResult = + t.aggregate({$geoNear: {near: [0, 0], spherical: false, distanceField: "dis"}}).toArray(); + assert.eq(3, cmdResult.length); + assert.eq(40, cmdResult[0].dis); + assert.eq(41, cmdResult[1].dis); + assert.eq(42, cmdResult[2].dis); + + cmdResult = t.aggregate({ + $geoNear: { + near: [0, 0], + minDistance: 41.5, + spherical: false, + distanceField: "dis", + } + }).toArray(); + assert.eq(1, cmdResult.length); + assert.eq(42, cmdResult[0].dis); + + // Test minDistance for 2d index with $geoNear stage and spherical=true. Distances are in // radians. - cmdResult = db.runCommand({geoNear: t.getName(), near: [0, 0], spherical: true}); - assert.commandWorked(cmdResult); - assert.eq(3, cmdResult.results.length); - assertApproxEqual(deg2rad(40), cmdResult.results[0].dis, 1e-3); - assertApproxEqual(deg2rad(41), cmdResult.results[1].dis, 1e-3); - assertApproxEqual(deg2rad(42), cmdResult.results[2].dis, 1e-3); - - cmdResult = db.runCommand( - {geoNear: t.getName(), near: [0, 0], minDistance: deg2rad(41.5), spherical: true}); - assert.commandWorked(cmdResult); - assert.eq(1, cmdResult.results.length); - assertApproxEqual(deg2rad(42), cmdResult.results[0].dis, 1e-3); + cmdResult = + t.aggregate({$geoNear: {near: [0, 0], spherical: true, distanceField: "dis"}}).toArray(); + assert.eq(3, cmdResult.length); + assertApproxEqual(deg2rad(40), cmdResult[0].dis, 1e-3); + assertApproxEqual(deg2rad(41), cmdResult[1].dis, 1e-3); + assertApproxEqual(deg2rad(42), cmdResult[2].dis, 1e-3); + + cmdResult = t.aggregate({ + $geoNear: { + near: [0, 0], + minDistance: deg2rad(41.5), + spherical: true, + distanceField: "dis", + } + }).toArray(); + assert.eq(1, cmdResult.length); + assertApproxEqual(deg2rad(42), cmdResult[0].dis, 1e-3); }()); diff --git a/jstests/core/geo_nearwithin.js b/jstests/core/geo_nearwithin.js index 69eaac51ffe..a63871c3195 100644 --- a/jstests/core/geo_nearwithin.js +++ b/jstests/core/geo_nearwithin.js @@ -1,39 +1,40 @@ -// Test geoNear + $within. -t = db.geo_nearwithin; -t.drop(); - -points = 10; -for (var x = -points; x < points; x += 1) { - for (var y = -points; y < points; y += 1) { - t.insert({geo: [x, y]}); - } -} - -t.ensureIndex({geo: "2d"}); - -resNear = db.runCommand( - {geoNear: t.getName(), near: [0, 0], query: {geo: {$within: {$center: [[0, 0], 1]}}}}); -assert.eq(resNear.results.length, 5); -resNear = db.runCommand( - {geoNear: t.getName(), near: [0, 0], query: {geo: {$within: {$center: [[0, 0], 0]}}}}); -assert.eq(resNear.results.length, 1); -resNear = db.runCommand( - {geoNear: t.getName(), near: [0, 0], query: {geo: {$within: {$center: [[1, 0], 0.5]}}}}); -assert.eq(resNear.results.length, 1); -resNear = db.runCommand( - {geoNear: t.getName(), near: [0, 0], query: {geo: {$within: {$center: [[1, 0], 1.5]}}}}); -assert.eq(resNear.results.length, 9); - -// We want everything distance >1 from us but <1.5 -// These points are (-+1, -+1) -resNear = db.runCommand({ - geoNear: t.getName(), - near: [0, 0], - query: { - $and: [ - {geo: {$within: {$center: [[0, 0], 1.5]}}}, - {geo: {$not: {$within: {$center: [[0, 0], 1]}}}} - ] +// Test $near + $within. +(function() { + t = db.geo_nearwithin; + t.drop(); + + points = 10; + for (var x = -points; x < points; x += 1) { + for (var y = -points; y < points; y += 1) { + assert.commandWorked(t.insert({geo: [x, y]})); + } } -}); -assert.eq(resNear.results.length, 4); + + assert.commandWorked(t.ensureIndex({geo: "2d"})); + + const runQuery = (center) => + t.find({$and: [{geo: {$near: [0, 0]}}, {geo: {$within: {$center: center}}}]}).toArray(); + + resNear = runQuery([[0, 0], 1]); + assert.eq(resNear.length, 5); + + resNear = runQuery([[0, 0], 0]); + assert.eq(resNear.length, 1); + + resNear = runQuery([[1, 0], 0.5]); + assert.eq(resNear.length, 1); + + resNear = runQuery([[1, 0], 1.5]); + assert.eq(resNear.length, 9); + + // We want everything distance >1 from us but <1.5 + // These points are (-+1, -+1) + resNear = t.find({ + $and: [ + {geo: {$near: [0, 0]}}, + {geo: {$within: {$center: [[0, 0], 1.5]}}}, + {geo: {$not: {$within: {$center: [[0, 0], 1]}}}} + ] + }).toArray(); + assert.eq(resNear.length, 4); +}()); diff --git a/jstests/core/geo_oob_sphere.js b/jstests/core/geo_oob_sphere.js index 40249766355..57857e383bd 100644 --- a/jstests/core/geo_oob_sphere.js +++ b/jstests/core/geo_oob_sphere.js @@ -27,11 +27,20 @@ assert.throws(function() { t.find({loc: {$within: {$centerSphere: [[-180, -91], 0.25]}}}).count(); }); -var res; -res = - db.runCommand({geoNear: "geooobsphere", near: [179, -91], maxDistance: 0.25, spherical: true}); -assert.commandFailed(res); -printjson(res); +// In a spherical geometry, this point is out-of-bounds. +assert.commandFailedWithCode(t.runCommand("find", {filter: {loc: {$nearSphere: [179, -91]}}}), + 17444); +assert.commandFailedWithCode(t.runCommand("aggregate", { + cursor: {}, + pipeline: [{ + $geoNear: { + near: [179, -91], + distanceField: "dis", + spherical: true, + } + }] +}), + 17444); // TODO: SERVER-9986 - it's not clear that throwing is correct behavior here // res = db.runCommand({ geoNear : "geooobsphere", near : [30, 89], maxDistance : 0.25, spherical : diff --git a/jstests/core/geo_operator_crs.js b/jstests/core/geo_operator_crs.js index 89fb7ca8585..b2cc8fe0439 100644 --- a/jstests/core/geo_operator_crs.js +++ b/jstests/core/geo_operator_crs.js @@ -3,56 +3,56 @@ // // Tests that the correct CRSes are used for geo queries (based on input geometry) // +(function() { + var coll = db.geo_operator_crs; + coll.drop(); -var coll = db.geo_operator_crs; -coll.drop(); + // + // Test 2dsphere index + // -// -// Test 2dsphere index -// + assert.commandWorked(coll.ensureIndex({geo: "2dsphere"})); -assert.commandWorked(coll.ensureIndex({geo: "2dsphere"})); + var legacyZeroPt = [0, 0]; + var jsonZeroPt = {type: "Point", coordinates: [0, 0]}; + var legacy90Pt = [90, 0]; + var json90Pt = {type: "Point", coordinates: [90, 0]}; -var legacyZeroPt = [0, 0]; -var jsonZeroPt = {type: "Point", coordinates: [0, 0]}; -var legacy90Pt = [90, 0]; -var json90Pt = {type: "Point", coordinates: [90, 0]}; + assert.writeOK(coll.insert({geo: json90Pt})); -assert.writeOK(coll.insert({geo: json90Pt})); + var earthRadiusMeters = 6378.1 * 1000; + var result = null; -var earthRadiusMeters = 6378.1 * 1000; -var result = null; + const runQuery = (point) => + coll.find({geo: {$nearSphere: point}}, {dis: {$meta: "geoNearDistance"}}).toArray(); -result = coll.getDB().runCommand({geoNear: coll.getName(), near: legacyZeroPt, spherical: true}); -assert.commandWorked(result); -assert.close(result.results[0].dis, Math.PI / 2); + result = runQuery(legacyZeroPt); + assert.close(result[0].dis, Math.PI / 2); -result = coll.getDB().runCommand({geoNear: coll.getName(), near: jsonZeroPt, spherical: true}); -assert.commandWorked(result); -assert.close(result.results[0].dis, (Math.PI / 2) * earthRadiusMeters); + result = runQuery(jsonZeroPt); + assert.close(result[0].dis, (Math.PI / 2) * earthRadiusMeters); -assert.writeOK(coll.remove({})); -assert.commandWorked(coll.dropIndexes()); + assert.writeOK(coll.remove({})); + assert.commandWorked(coll.dropIndexes()); -// -// Test 2d Index -// + // + // Test 2d Index + // -assert.commandWorked(coll.ensureIndex({geo: "2d"})); + assert.commandWorked(coll.ensureIndex({geo: "2d"})); -assert.writeOK(coll.insert({geo: legacy90Pt})); + assert.writeOK(coll.insert({geo: legacy90Pt})); -result = coll.getDB().runCommand({geoNear: coll.getName(), near: legacyZeroPt, spherical: true}); -assert.commandWorked(result); -assert.close(result.results[0].dis, Math.PI / 2); + result = runQuery(legacyZeroPt); + assert.close(result[0].dis, Math.PI / 2); -// GeoJSON not supported unless there's a 2dsphere index + // GeoJSON not supported unless there's a 2dsphere index -// -// Test with a 2d and 2dsphere index -// + // + // Test with a 2d and 2dsphere index using the aggregation $geoNear stage. + // -assert.commandWorked(coll.ensureIndex({geo: "2dsphere"})); -result = coll.getDB().runCommand({geoNear: coll.getName(), near: jsonZeroPt, spherical: true}); -assert.commandWorked(result); -assert.close(result.results[0].dis, (Math.PI / 2) * earthRadiusMeters); + assert.commandWorked(coll.ensureIndex({geo: "2dsphere"})); + result = coll.aggregate({$geoNear: {near: jsonZeroPt, distanceField: "dis"}}).toArray(); + assert.close(result[0].dis, (Math.PI / 2) * earthRadiusMeters); +}()); diff --git a/jstests/core/geo_s2near.js b/jstests/core/geo_s2near.js index 0306a60adb9..d0a591d45e6 100644 --- a/jstests/core/geo_s2near.js +++ b/jstests/core/geo_s2near.js @@ -1,117 +1,143 @@ // @tags: [requires_getmore] -// Test 2dsphere near search, called via find and geoNear. -t = db.geo_s2near; -t.drop(); +// Test 2dsphere near search, called via find and $geoNear. +(function() { + t = db.geo_s2near; + t.drop(); -// Make sure that geoNear gives us back loc -goldenPoint = { - type: "Point", - coordinates: [31.0, 41.0] -}; -t.insert({geo: goldenPoint}); -t.ensureIndex({geo: "2dsphere"}); -resNear = db.runCommand( - {geoNear: t.getName(), near: [30, 40], num: 1, spherical: true, includeLocs: true}); -assert.eq(resNear.results[0].loc, goldenPoint); + // Make sure that geoNear gives us back loc + goldenPoint = {type: "Point", coordinates: [31.0, 41.0]}; + t.insert({geo: goldenPoint}); + t.ensureIndex({geo: "2dsphere"}); + resNear = + t.aggregate([ + {$geoNear: {near: [30, 40], distanceField: "d", spherical: true, includeLocs: "loc"}}, + {$limit: 1} + ]).toArray(); + assert.eq(resNear.length, 1, tojson(resNear)); + assert.eq(resNear[0].loc, goldenPoint); -// FYI: -// One degree of long @ 0 is 111km or so. -// One degree of lat @ 0 is 110km or so. -lat = 0; -lng = 0; -points = 10; -for (var x = -points; x < points; x += 1) { - for (var y = -points; y < points; y += 1) { - t.insert({geo: {"type": "Point", "coordinates": [lng + x / 1000.0, lat + y / 1000.0]}}); + // FYI: + // One degree of long @ 0 is 111km or so. + // One degree of lat @ 0 is 110km or so. + lat = 0; + lng = 0; + points = 10; + for (var x = -points; x < points; x += 1) { + for (var y = -points; y < points; y += 1) { + t.insert({geo: {"type": "Point", "coordinates": [lng + x / 1000.0, lat + y / 1000.0]}}); + } } -} -origin = { - "type": "Point", - "coordinates": [lng, lat] -}; + origin = {"type": "Point", "coordinates": [lng, lat]}; -t.ensureIndex({geo: "2dsphere"}); + t.ensureIndex({geo: "2dsphere"}); -// Near only works when the query is a point. -someline = { - "type": "LineString", - "coordinates": [[40, 5], [41, 6]] -}; -somepoly = { - "type": "Polygon", - "coordinates": [[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]] -}; -assert.throws(function() { - return t.find({"geo": {"$near": {"$geometry": someline}}}).count(); -}); -assert.throws(function() { - return t.find({"geo": {"$near": {"$geometry": somepoly}}}).count(); -}); -assert.throws(function() { - return db.runCommand({geoNear: t.getName(), near: someline, spherical: true}).results.length; -}); -assert.throws(function() { - return db.runCommand({geoNear: t.getName(), near: somepoly, spherical: true}).results.length; -}); + // Near only works when the query is a point. + someline = {"type": "LineString", "coordinates": [[40, 5], [41, 6]]}; + somepoly = {"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]]}; + assert.throws(function() { + return t.find({"geo": {"$near": {"$geometry": someline}}}).count(); + }); + assert.throws(function() { + return t.find({"geo": {"$near": {"$geometry": somepoly}}}).count(); + }); + assert.throws(function() { + return t.aggregate({$geoNear: {near: someline, distanceField: "dis", spherical: true}}); + }); + assert.throws(function() { + return t.aggregate({$geoNear: {near: somepoly, distanceField: "dis", spherical: true}}); + }); -// Do some basic near searches. -res = t.find({"geo": {"$near": {"$geometry": origin, $maxDistance: 2000}}}).limit(10); -resNear = db.runCommand( - {geoNear: t.getName(), near: [0, 0], num: 10, maxDistance: Math.PI, spherical: true}); -assert.eq(res.itcount(), resNear.results.length, "10"); + // Do some basic near searches. + res = t.find({"geo": {"$near": {"$geometry": origin, $maxDistance: 2000}}}).limit(10); + resNear = t.aggregate([ + {$geoNear: {near: [0, 0], distanceField: "dis", maxDistance: Math.PI, spherical: true}}, + {$limit: 10}, + ]); + assert.eq(res.itcount(), resNear.itcount(), "10"); -res = t.find({"geo": {"$near": {"$geometry": origin}}}).limit(10); -resNear = db.runCommand({geoNear: t.getName(), near: [0, 0], num: 10, spherical: true}); -assert.eq(res.itcount(), resNear.results.length, "10"); + res = t.find({"geo": {"$near": {"$geometry": origin}}}).limit(10); + resNear = t.aggregate([ + {$geoNear: {near: [0, 0], distanceField: "dis", spherical: true}}, + {$limit: 10}, + ]); + assert.eq(res.itcount(), resNear.itcount(), "10"); -// Find all the points! -res = t.find({"geo": {"$near": {"$geometry": origin}}}).limit(10000); -resNear = db.runCommand({geoNear: t.getName(), near: [0, 0], num: 10000, spherical: true}); -assert.eq(resNear.results.length, res.itcount(), ((2 * points) * (2 * points)).toString()); + // Find all the points! + res = t.find({"geo": {"$near": {"$geometry": origin}}}).limit(10000); + resNear = t.aggregate([ + {$geoNear: {near: [0, 0], distanceField: "dis", spherical: true}}, + {$limit: 10000}, + ]); + assert.eq(res.itcount(), resNear.itcount(), ((2 * points) * (2 * points)).toString()); -// longitude goes -180 to 180 -// latitude goes -90 to 90 -// Let's put in some perverse (polar) data and make sure we get it back. -// Points go long, lat. -t.insert({geo: {"type": "Point", "coordinates": [-180, -90]}}); -t.insert({geo: {"type": "Point", "coordinates": [180, -90]}}); -t.insert({geo: {"type": "Point", "coordinates": [180, 90]}}); -t.insert({geo: {"type": "Point", "coordinates": [-180, 90]}}); -res = t.find({"geo": {"$near": {"$geometry": origin}}}).limit(10000); -resNear = db.runCommand({geoNear: t.getName(), near: [0, 0], num: 10000, spherical: true}); -assert.eq(res.itcount(), resNear.results.length, ((2 * points) * (2 * points) + 4).toString()); + // longitude goes -180 to 180 + // latitude goes -90 to 90 + // Let's put in some perverse (polar) data and make sure we get it back. + // Points go long, lat. + t.insert({geo: {"type": "Point", "coordinates": [-180, -90]}}); + t.insert({geo: {"type": "Point", "coordinates": [180, -90]}}); + t.insert({geo: {"type": "Point", "coordinates": [180, 90]}}); + t.insert({geo: {"type": "Point", "coordinates": [-180, 90]}}); + res = t.find({"geo": {"$near": {"$geometry": origin}}}).limit(10000); + resNear = t.aggregate([ + {$geoNear: {near: [0, 0], distanceField: "dis", spherical: true}}, + {$limit: 10000}, + ]); + assert.eq(res.itcount(), resNear.itcount(), ((2 * points) * (2 * points) + 4).toString()); -function testRadAndDegreesOK(distance) { - // Distance for old style points is radians. - resRadians = t.find({geo: {$nearSphere: [0, 0], $maxDistance: (distance / (6378.1 * 1000))}}); - // Distance for new style points is meters. - resMeters = t.find({"geo": {"$near": {"$geometry": origin, $maxDistance: distance}}}); - // And we should get the same # of results no matter what. - assert.eq(resRadians.itcount(), resMeters.itcount()); + function testRadAndDegreesOK(distance) { + // Distance for old style points is radians. + resRadians = + t.find({geo: {$nearSphere: [0, 0], $maxDistance: (distance / (6378.1 * 1000))}}); + // Distance for new style points is meters. + resMeters = t.find({"geo": {"$near": {"$geometry": origin, $maxDistance: distance}}}); + // And we should get the same # of results no matter what. + assert.eq(resRadians.itcount(), resMeters.itcount()); - // Also, geoNear should behave the same way. - resGNMeters = - db.runCommand({geoNear: t.getName(), near: origin, maxDistance: distance, spherical: true}); - resGNRadians = db.runCommand({ - geoNear: t.getName(), - near: [0, 0], - maxDistance: (distance / (6378.1 * 1000)), - spherical: true - }); - assert.eq(resGNRadians.results.length, resGNMeters.results.length); - for (var i = 0; i < resGNRadians.length; ++i) { - // Radius of earth * radians = distance in meters. - assert.close(resGNRadians.results[i].dis * 6378.1 * 1000, resGNMeters.results[i].dis); + // Also, $geoNear should behave the same way. + resGNMeters = t.aggregate({ + $geoNear: { + near: origin, + distanceField: "dis", + maxDistance: distance, + spherical: true, + } + }).toArray(); + resGNRadians = t.aggregate({ + $geoNear: { + near: [0, 0], + distanceField: "dis", + maxDistance: (distance / (6378.1 * 1000)), + spherical: true, + } + }).toArray(); + const errmsg = `$geoNear using meter distances returned ${tojson(resGNMeters)}, but ` + + `$geoNear using radian distances returned ${tojson(resGNRadians)}`; + assert.eq(resGNRadians.length, resGNMeters.length, errmsg); + for (var i = 0; i < resGNRadians.length; ++i) { + // Radius of earth * radians = distance in meters. + assert.close(resGNRadians[i].dis * 6378.1 * 1000, resGNMeters[i].dis); + } } -} -testRadAndDegreesOK(1); -testRadAndDegreesOK(10); -testRadAndDegreesOK(50); -testRadAndDegreesOK(10000); + testRadAndDegreesOK(1); + testRadAndDegreesOK(10); + testRadAndDegreesOK(50); + testRadAndDegreesOK(10000); -// SERVER-13666 legacy coordinates must be in bounds for spherical near queries. -assert.commandFailed( - db.runCommand({geoNear: t.getName(), near: [1210.466, 31.2051], spherical: true, num: 10})); + // SERVER-13666 legacy coordinates must be in bounds for spherical near queries. + assert.commandFailedWithCode(db.runCommand({ + aggregate: t.getName(), + cursor: {}, + pipeline: [{ + $geoNear: { + near: [1210.466, 31.2051], + distanceField: "dis", + spherical: true, + } + }] + }), + 17444); +}()); diff --git a/jstests/core/geo_s2near_equator_opposite.js b/jstests/core/geo_s2near_equator_opposite.js index 754c27e523d..fb17310030b 100644 --- a/jstests/core/geo_s2near_equator_opposite.js +++ b/jstests/core/geo_s2near_equator_opposite.js @@ -1,37 +1,56 @@ // Tests geo near with 2 points diametrically opposite to each other // on the equator // First reported in SERVER-11830 as a regression in 2.5 - -var t = db.geos2nearequatoropposite; - -t.drop(); - -t.insert({loc: {type: 'Point', coordinates: [0, 0]}}); -t.insert({loc: {type: 'Point', coordinates: [-1, 0]}}); - -t.ensureIndex({loc: '2dsphere'}); - -// upper bound for half of earth's circumference in meters -var dist = 40075000 / 2 + 1; - -var nearSphereCount = - t.find({ - loc: { - $nearSphere: {$geometry: {type: 'Point', coordinates: [180, 0]}, $maxDistance: dist} - } - }).itcount(); -var nearCount = - t.find({ - loc: {$near: {$geometry: {type: 'Point', coordinates: [180, 0]}, $maxDistance: dist}} - }).itcount(); -var geoNearResult = db.runCommand( - {geoNear: t.getName(), near: {type: 'Point', coordinates: [180, 0]}, spherical: true}); - -print('nearSphere count = ' + nearSphereCount); -print('near count = ' + nearCount); -print('geoNearResults = ' + tojson(geoNearResult)); - -assert.eq(2, nearSphereCount, 'unexpected document count for nearSphere'); -assert.eq(2, nearCount, 'unexpected document count for near'); -assert.eq(2, geoNearResult.results.length, 'unexpected document count in geoNear results'); -assert.gt(dist, geoNearResult.stats.maxDistance, 'unexpected maximum distance in geoNear results'); +(function() { + var t = db.geos2nearequatoropposite; + + t.drop(); + + t.insert({loc: {type: 'Point', coordinates: [0, 0]}}); + t.insert({loc: {type: 'Point', coordinates: [-1, 0]}}); + + t.ensureIndex({loc: '2dsphere'}); + + // upper bound for half of earth's circumference in meters + var dist = 40075000 / 2 + 1; + + var nearSphereCount = + t.find({ + loc: { + $nearSphere: + {$geometry: {type: 'Point', coordinates: [180, 0]}, $maxDistance: dist} + } + }).itcount(); + var nearCount = + t.find({ + loc: {$near: {$geometry: {type: 'Point', coordinates: [180, 0]}, $maxDistance: dist}} + }).itcount(); + var geoNearResult = t.aggregate([ + { + $geoNear: { + near: {type: 'Point', coordinates: [180, 0]}, + spherical: true, + distanceField: "dist", + } + }, + { + $group: { + _id: null, + nResults: {$sum: 1}, + maxDistance: {$max: "$distanceField"}, + } + } + ]).toArray(); + + assert.eq(2, nearSphereCount, 'unexpected document count for nearSphere'); + assert.eq(2, nearCount, 'unexpected document count for near'); + assert.eq(1, geoNearResult.length, `unexpected $geoNear result: ${tojson(geoNearResult)}`); + + const geoNearStats = geoNearResult[0]; + assert.eq(2, + geoNearStats.nResults, + `unexpected document count for $geoNear: ${tojson(geoNearStats)}`); + assert.gt(dist, + geoNearStats.maxDistance, + `unexpected maximum distance in $geoNear results: ${tojson(geoNearStats)}`); +}()); diff --git a/jstests/core/geo_s2nearwithin.js b/jstests/core/geo_s2nearwithin.js index 1bcec709643..6df9a1940df 100644 --- a/jstests/core/geo_s2nearwithin.js +++ b/jstests/core/geo_s2nearwithin.js @@ -1,64 +1,57 @@ -// Test geoNear + $within. -t = db.geo_s2nearwithin; -t.drop(); - -points = 10; -for (var x = -points; x < points; x += 1) { - for (var y = -points; y < points; y += 1) { - t.insert({geo: [x, y]}); +// Test $geoNear + $within. +(function() { + t = db.geo_s2nearwithin; + t.drop(); + + points = 10; + for (var x = -points; x < points; x += 1) { + for (var y = -points; y < points; y += 1) { + assert.commandWorked(t.insert({geo: [x, y]})); + } } -} - -origin = { - "type": "Point", - "coordinates": [0, 0] -}; - -t.ensureIndex({geo: "2dsphere"}); -// Near requires an index, and 2dsphere is an index. Spherical isn't -// specified so this doesn't work. -assert.commandFailed(db.runCommand( - {geoNear: t.getName(), near: [0, 0], query: {geo: {$within: {$center: [[0, 0], 1]}}}})); - -// Spherical is specified so this does work. Old style points are weird -// because you can use them with both $center and $centerSphere. Points are -// the only things we will do this conversion for. -resNear = db.runCommand({ - geoNear: t.getName(), - near: [0, 0], - spherical: true, - query: {geo: {$within: {$center: [[0, 0], 1]}}} -}); -assert.eq(resNear.results.length, 5); - -resNear = db.runCommand({ - geoNear: t.getName(), - near: [0, 0], - spherical: true, - query: {geo: {$within: {$centerSphere: [[0, 0], Math.PI / 180.0]}}} -}); -assert.eq(resNear.results.length, 5); - -resNear = db.runCommand({ - geoNear: t.getName(), - near: [0, 0], - spherical: true, - query: {geo: {$within: {$centerSphere: [[0, 0], 0]}}} -}); -assert.eq(resNear.results.length, 1); - -resNear = db.runCommand({ - geoNear: t.getName(), - near: [0, 0], - spherical: true, - query: {geo: {$within: {$centerSphere: [[1, 0], 0.5 * Math.PI / 180.0]}}} -}); -assert.eq(resNear.results.length, 1); -resNear = db.runCommand({ - geoNear: t.getName(), - near: [0, 0], - spherical: true, - query: {geo: {$within: {$center: [[1, 0], 1.5]}}} -}); -assert.eq(resNear.results.length, 9); + origin = {"type": "Point", "coordinates": [0, 0]}; + + assert.commandWorked(t.ensureIndex({geo: "2dsphere"})); + // Near requires an index, and 2dsphere is an index. Spherical isn't + // specified so this doesn't work. + let res = assert.commandFailedWithCode(t.runCommand("aggregate", { + cursor: {}, + pipeline: [{ + $geoNear: { + near: [0, 0], + distanceField: "d", + query: {geo: {$within: {$center: [[0, 0], 1]}}} + } + }], + }), + ErrorCodes.BadValue); + assert(res.errmsg.includes("unable to find index for $geoNear query"), tojson(res)); + + // Spherical is specified so this does work. Old style points are weird + // because you can use them with both $center and $centerSphere. Points are + // the only things we will do this conversion for. + const runGeoNear = (within) => t.aggregate({ + $geoNear: { + near: [0, 0], + distanceField: "d", + spherical: true, + query: {geo: {$within: within}}, + } + }).toArray(); + + resNear = runGeoNear({$center: [[0, 0], 1]}); + assert.eq(resNear.length, 5); + + resNear = runGeoNear({$centerSphere: [[0, 0], Math.PI / 180.0]}); + assert.eq(resNear.length, 5); + + resNear = runGeoNear({$centerSphere: [[0, 0], 0]}); + assert.eq(resNear.length, 1); + + resNear = runGeoNear({$centerSphere: [[1, 0], 0.5 * Math.PI / 180.0]}); + assert.eq(resNear.length, 1); + + resNear = runGeoNear({$center: [[1, 0], 1.5]}); + assert.eq(resNear.length, 9); +}()); diff --git a/jstests/core/geo_s2twofields.js b/jstests/core/geo_s2twofields.js index ada5e96f6d5..517c8c8df12 100644 --- a/jstests/core/geo_s2twofields.js +++ b/jstests/core/geo_s2twofields.js @@ -3,82 +3,86 @@ // // @tags: [requires_fastcount] -var t = db.geo_s2twofields; -t.drop(); +(function() { + var t = db.geo_s2twofields; + t.drop(); -Random.setRandomSeed(); -var random = Random.rand; -var PI = Math.PI; + Random.setRandomSeed(); + var random = Random.rand; + var PI = Math.PI; -function randomCoord(center, minDistDeg, maxDistDeg) { - var dx = random() * (maxDistDeg - minDistDeg) + minDistDeg; - var dy = random() * (maxDistDeg - minDistDeg) + minDistDeg; - return [center[0] + dx, center[1] + dy]; -} + function randomCoord(center, minDistDeg, maxDistDeg) { + var dx = random() * (maxDistDeg - minDistDeg) + minDistDeg; + var dy = random() * (maxDistDeg - minDistDeg) + minDistDeg; + return [center[0] + dx, center[1] + dy]; + } -var nyc = {type: "Point", coordinates: [-74.0064, 40.7142]}; -var miami = {type: "Point", coordinates: [-80.1303, 25.7903]}; -var maxPoints = 10000; -var degrees = 5; + var nyc = {type: "Point", coordinates: [-74.0064, 40.7142]}; + var miami = {type: "Point", coordinates: [-80.1303, 25.7903]}; + var maxPoints = 10000; + var degrees = 5; -var arr = []; -for (var i = 0; i < maxPoints; ++i) { - var fromCoord = randomCoord(nyc.coordinates, 0, degrees); - var toCoord = randomCoord(miami.coordinates, 0, degrees); + var arr = []; + for (var i = 0; i < maxPoints; ++i) { + var fromCoord = randomCoord(nyc.coordinates, 0, degrees); + var toCoord = randomCoord(miami.coordinates, 0, degrees); - arr.push( - {from: {type: "Point", coordinates: fromCoord}, to: {type: "Point", coordinates: toCoord}}); -} -res = t.insert(arr); -assert.writeOK(res); -assert.eq(t.count(), maxPoints); + arr.push({ + from: {type: "Point", coordinates: fromCoord}, + to: {type: "Point", coordinates: toCoord} + }); + } + res = t.insert(arr); + assert.writeOK(res); + assert.eq(t.count(), maxPoints); -function semiRigorousTime(func) { - var lowestTime = func(); - var iter = 2; - for (var i = 0; i < iter; ++i) { - var run = func(); - if (run < lowestTime) { - lowestTime = run; + function semiRigorousTime(func) { + var lowestTime = func(); + var iter = 2; + for (var i = 0; i < iter; ++i) { + var run = func(); + if (run < lowestTime) { + lowestTime = run; + } } + return lowestTime; } - return lowestTime; -} -function timeWithoutAndWithAnIndex(index, query) { - t.dropIndex(index); - var withoutTime = semiRigorousTime(function() { - return t.find(query).explain("executionStats").executionStats.executionTimeMillis; - }); - t.ensureIndex(index); - var withTime = semiRigorousTime(function() { - return t.find(query).explain("executionStats").executionStats.executionTimeMillis; - }); - t.dropIndex(index); - return [withoutTime, withTime]; -} + function timeWithoutAndWithAnIndex(index, query) { + t.dropIndex(index); + var withoutTime = semiRigorousTime(function() { + return t.find(query).explain("executionStats").executionStats.executionTimeMillis; + }); + t.ensureIndex(index); + var withTime = semiRigorousTime(function() { + return t.find(query).explain("executionStats").executionStats.executionTimeMillis; + }); + t.dropIndex(index); + return [withoutTime, withTime]; + } -var maxQueryRad = 0.5 * PI / 180.0; -// When we're not looking at ALL the data, anything indexed should beat not-indexed. -var smallQuery = timeWithoutAndWithAnIndex({to: "2dsphere", from: "2dsphere"}, { - from: {$within: {$centerSphere: [nyc.coordinates, maxQueryRad]}}, - to: {$within: {$centerSphere: [miami.coordinates, maxQueryRad]}} -}); -print("Indexed time " + smallQuery[1] + " unindexed " + smallQuery[0]); -// assert(smallQuery[0] > smallQuery[1]); + var maxQueryRad = 0.5 * PI / 180.0; + // When we're not looking at ALL the data, anything indexed should beat not-indexed. + var smallQuery = timeWithoutAndWithAnIndex({to: "2dsphere", from: "2dsphere"}, { + from: {$within: {$centerSphere: [nyc.coordinates, maxQueryRad]}}, + to: {$within: {$centerSphere: [miami.coordinates, maxQueryRad]}} + }); + print("Indexed time " + smallQuery[1] + " unindexed " + smallQuery[0]); + // assert(smallQuery[0] > smallQuery[1]); -// Let's just index one field. -var smallQuery = timeWithoutAndWithAnIndex({to: "2dsphere"}, { - from: {$within: {$centerSphere: [nyc.coordinates, maxQueryRad]}}, - to: {$within: {$centerSphere: [miami.coordinates, maxQueryRad]}} -}); -print("Indexed time " + smallQuery[1] + " unindexed " + smallQuery[0]); -// assert(smallQuery[0] > smallQuery[1]); + // Let's just index one field. + var smallQuery = timeWithoutAndWithAnIndex({to: "2dsphere"}, { + from: {$within: {$centerSphere: [nyc.coordinates, maxQueryRad]}}, + to: {$within: {$centerSphere: [miami.coordinates, maxQueryRad]}} + }); + print("Indexed time " + smallQuery[1] + " unindexed " + smallQuery[0]); + // assert(smallQuery[0] > smallQuery[1]); -// And the other one. -var smallQuery = timeWithoutAndWithAnIndex({from: "2dsphere"}, { - from: {$within: {$centerSphere: [nyc.coordinates, maxQueryRad]}}, - to: {$within: {$centerSphere: [miami.coordinates, maxQueryRad]}} -}); -print("Indexed time " + smallQuery[1] + " unindexed " + smallQuery[0]); -// assert(smallQuery[0] > smallQuery[1]); + // And the other one. + var smallQuery = timeWithoutAndWithAnIndex({from: "2dsphere"}, { + from: {$within: {$centerSphere: [nyc.coordinates, maxQueryRad]}}, + to: {$within: {$centerSphere: [miami.coordinates, maxQueryRad]}} + }); + print("Indexed time " + smallQuery[1] + " unindexed " + smallQuery[0]); + // assert(smallQuery[0] > smallQuery[1]); +}()); diff --git a/jstests/core/geo_uniqueDocs.js b/jstests/core/geo_uniqueDocs.js index d66d5243c01..6a46337966b 100644 --- a/jstests/core/geo_uniqueDocs.js +++ b/jstests/core/geo_uniqueDocs.js @@ -1,25 +1,34 @@ -// Test uniqueDocs option for $within and geoNear queries SERVER-3139 +// Test uniqueDocs option for $within queries and the $geoNear aggregation stage. SERVER-3139 // SERVER-12120 uniqueDocs is deprecated. Server always returns unique documents. collName = 'geo_uniqueDocs_test'; t = db.geo_uniqueDocs_test; t.drop(); -t.save({locs: [[0, 2], [3, 4]]}); -t.save({locs: [[6, 8], [10, 10]]}); +assert.commandWorked(t.save({locs: [[0, 2], [3, 4]]})); +assert.commandWorked(t.save({locs: [[6, 8], [10, 10]]})); -t.ensureIndex({locs: '2d'}); +assert.commandWorked(t.ensureIndex({locs: '2d'})); -// geoNear tests +// $geoNear tests // uniqueDocs option is ignored. -assert.eq(2, db.runCommand({geoNear: collName, near: [0, 0]}).results.length); -assert.eq(2, db.runCommand({geoNear: collName, near: [0, 0], uniqueDocs: false}).results.length); -assert.eq(2, db.runCommand({geoNear: collName, near: [0, 0], uniqueDocs: true}).results.length); -results = db.runCommand({geoNear: collName, near: [0, 0], num: 2}).results; +assert.eq(2, t.aggregate({$geoNear: {near: [0, 0], distanceField: "dis"}}).toArray().length); +assert.eq(2, + t.aggregate({$geoNear: {near: [0, 0], distanceField: "dis", uniqueDocs: false}}) + .toArray() + .length); +assert.eq(2, + t.aggregate({$geoNear: {near: [0, 0], distanceField: "dis", uniqueDocs: true}}) + .toArray() + .length); +results = t.aggregate([{$geoNear: {near: [0, 0], distanceField: "dis"}}, {$limit: 2}]).toArray(); assert.eq(2, results.length); assert.close(2, results[0].dis); assert.close(10, results[1].dis); -results = db.runCommand({geoNear: collName, near: [0, 0], num: 2, uniqueDocs: true}).results; +results = t.aggregate([ + {$geoNear: {near: [0, 0], distanceField: "dis", uniqueDocs: true}}, + {$limit: 2} + ]).toArray(); assert.eq(2, results.length); assert.close(2, results[0].dis); assert.close(10, results[1].dis); diff --git a/jstests/core/geo_uniqueDocs2.js b/jstests/core/geo_uniqueDocs2.js index 6b0aafb92ae..23aa831f560 100644 --- a/jstests/core/geo_uniqueDocs2.js +++ b/jstests/core/geo_uniqueDocs2.js @@ -34,53 +34,69 @@ assert.eq(1, t.count({loc: {$within: {$center: [[30, 30], 10], $uniqueDocs: true assert.eq(1, t.count({loc: {$within: {$center: [[30, 30], 10], $uniqueDocs: false}}})); // Check number and character of results with geoNear / uniqueDocs / includeLocs. -notUniqueNotInclude = db.runCommand( - {geoNear: collName, near: [50, 50], num: 10, uniqueDocs: false, includeLocs: false}); -uniqueNotInclude = db.runCommand( - {geoNear: collName, near: [50, 50], num: 10, uniqueDocs: true, includeLocs: false}); -notUniqueInclude = db.runCommand( - {geoNear: collName, near: [50, 50], num: 10, uniqueDocs: false, includeLocs: true}); -uniqueInclude = db.runCommand( - {geoNear: collName, near: [50, 50], num: 10, uniqueDocs: true, includeLocs: true}); - -// Check that only unique docs are returned. -assert.eq(1, notUniqueNotInclude.results.length); -assert.eq(1, uniqueNotInclude.results.length); -assert.eq(1, notUniqueInclude.results.length); -assert.eq(1, uniqueInclude.results.length); +notUniqueNotInclude = + t.aggregate({$geoNear: {near: [50, 50], distanceField: "dis", uniqueDocs: false}}).toArray(); +uniqueNotInclude = + t.aggregate({$geoNear: {near: [50, 50], distanceField: "dis", uniqueDocs: true}}).toArray(); +notUniqueInclude = t.aggregate({ + $geoNear: { + near: [50, 50], + distanceField: "dis", + uniqueDocs: false, + includeLocs: "point", + } + }).toArray(); +uniqueInclude = t.aggregate({ + $geoNear: { + near: [50, 50], + distanceField: "dis", + uniqueDocs: true, + includeLocs: "point", + } + }).toArray(); + +// Check that only unique results are returned, regardless of the value of "uniqueDocs" parameter. +assert.eq(1, notUniqueNotInclude.length); +assert.eq(1, uniqueNotInclude.length); +assert.eq(1, notUniqueInclude.length); +assert.eq(1, uniqueInclude.length); // Check that locs are included. -assert(!notUniqueNotInclude.results[0].loc); -assert(!uniqueNotInclude.results[0].loc); -assert(notUniqueInclude.results[0].loc); -assert(uniqueInclude.results[0].loc); - -// For geoNear / uniqueDocs, 'num' limit seems to apply to locs. -assert.eq(1, - db.runCommand( - {geoNear: collName, near: [50, 50], num: 1, uniqueDocs: false, includeLocs: false}) - .results.length); +assert(!notUniqueNotInclude[0].point); +assert(!uniqueNotInclude[0].point); +assert(notUniqueInclude[0].point); +assert(uniqueInclude[0].point); // Check locs returned in includeLocs mode. t.remove({}); objLocs = [{x: 20, y: 30, z: ['loc1', 'loca']}, {x: 40, y: 50, z: ['loc2', 'locb']}]; t.save({loc: objLocs}); -results = db.runCommand( - {geoNear: collName, near: [50, 50], num: 10, uniqueDocs: false, includeLocs: true}) - .results; -assert.contains(results[0].loc, objLocs); +results = t.aggregate({ + $geoNear: { + near: [50, 50], + distanceField: "dis", + uniqueDocs: false, + includeLocs: "point", + } + }).toArray(); +assert.contains(results[0].point, objLocs); // Check locs returned in includeLocs mode, where locs are arrays. t.remove({}); arrLocs = [[20, 30], [40, 50]]; t.save({loc: arrLocs}); -results = db.runCommand( - {geoNear: collName, near: [50, 50], num: 10, uniqueDocs: false, includeLocs: true}) - .results; +results = t.aggregate({ + $geoNear: { + near: [50, 50], + distanceField: "dis", + uniqueDocs: false, + includeLocs: "point", + } + }).toArray(); // The original loc arrays are returned as objects. expectedLocs = arrLocs; -assert.contains(results[0].loc, expectedLocs); +assert.contains(results[0].point, expectedLocs); // Test a large number of locations in the array. t.drop(); diff --git a/jstests/core/geo_validate.js b/jstests/core/geo_validate.js index 6d92e5736ce..190f7886298 100644 --- a/jstests/core/geo_validate.js +++ b/jstests/core/geo_validate.js @@ -77,17 +77,6 @@ assert.throws(function() { }); // -// -// Make sure we can't do a near search with a negative limit -assert.commandFailed( - db.runCommand({geoNear: coll.getName(), near: [0, 0], spherical: true, num: -1})); -assert.commandFailed( - db.runCommand({geoNear: coll.getName(), near: [0, 0], spherical: true, num: -Infinity})); -// NaN is interpreted as limit 0 -assert.commandWorked( - db.runCommand({geoNear: coll.getName(), near: [0, 0], spherical: true, num: NaN})); - -// // SERVER-17241 Polygon has no loop assert.writeError(coll.insert({geo: {type: 'Polygon', coordinates: []}})); diff --git a/jstests/core/geob.js b/jstests/core/geob.js index c711a676b2b..2664d6c5921 100644 --- a/jstests/core/geob.js +++ b/jstests/core/geob.js @@ -1,35 +1,38 @@ -var t = db.geob; -t.drop(); - -var a = {p: [0, 0]}; -var b = {p: [1, 0]}; -var c = {p: [3, 4]}; -var d = {p: [0, 6]}; - -t.save(a); -t.save(b); -t.save(c); -t.save(d); -t.ensureIndex({p: "2d"}); - -var res = t.runCommand("geoNear", {near: [0, 0]}); -assert.close(3, res.stats.avgDistance, "A"); - -assert.close(0, res.results[0].dis, "B1"); -assert.eq(a._id, res.results[0].obj._id, "B2"); - -assert.close(1, res.results[1].dis, "C1"); -assert.eq(b._id, res.results[1].obj._id, "C2"); - -assert.close(5, res.results[2].dis, "D1"); -assert.eq(c._id, res.results[2].obj._id, "D2"); - -assert.close(6, res.results[3].dis, "E1"); -assert.eq(d._id, res.results[3].obj._id, "E2"); - -res = t.runCommand("geoNear", {near: [0, 0], distanceMultiplier: 2}); -assert.close(6, res.stats.avgDistance, "F"); -assert.close(0, res.results[0].dis, "G"); -assert.close(2, res.results[1].dis, "H"); -assert.close(10, res.results[2].dis, "I"); -assert.close(12, res.results[3].dis, "J"); +(function() { + "use strict"; + var t = db.geob; + t.drop(); + + var a = {p: [0, 0]}; + var b = {p: [1, 0]}; + var c = {p: [3, 4]}; + var d = {p: [0, 6]}; + + t.save(a); + t.save(b); + t.save(c); + t.save(d); + t.ensureIndex({p: "2d"}); + + let res = t.aggregate({$geoNear: {near: [0, 0], distanceField: "dis"}}).toArray(); + + assert.close(0, res[0].dis, "B1"); + assert.eq(a._id, res[0]._id, "B2"); + + assert.close(1, res[1].dis, "C1"); + assert.eq(b._id, res[1]._id, "C2"); + + assert.close(5, res[2].dis, "D1"); + assert.eq(c._id, res[2]._id, "D2"); + + assert.close(6, res[3].dis, "E1"); + assert.eq(d._id, res[3]._id, "E2"); + + res = t.aggregate({ + $geoNear: {near: [0, 0], distanceField: "dis", distanceMultiplier: 2.0} + }).toArray(); + assert.close(0, res[0].dis, "G"); + assert.close(2, res[1].dis, "H"); + assert.close(10, res[2].dis, "I"); + assert.close(12, res[3].dis, "J"); +}()); diff --git a/jstests/core/geod.js b/jstests/core/geod.js index 35844d0f914..055453254a5 100644 --- a/jstests/core/geod.js +++ b/jstests/core/geod.js @@ -7,8 +7,11 @@ t.ensureIndex({loc: "2d"}); // should match no points in the dataset. dists = [.49, .51, 1.0]; for (idx in dists) { - b = db.runCommand({geoNear: "geod", near: [1, 0], num: 2, maxDistance: dists[idx]}); - assert.eq(b.errmsg, undefined, "A" + idx); - l = b.results.length; - assert.eq(l, idx, "B" + idx); + b = db.geod + .aggregate([ + {$geoNear: {near: [1, 0], distanceField: "d", maxDistance: dists[idx]}}, + {$limit: 2}, + ]) + .toArray(); + assert.eq(b.length, idx, "B" + idx); } diff --git a/jstests/core/geonear_cmd_input_validation.js b/jstests/core/geonear_cmd_input_validation.js index 661e2113952..9cc82cb6f25 100644 --- a/jstests/core/geonear_cmd_input_validation.js +++ b/jstests/core/geonear_cmd_input_validation.js @@ -23,9 +23,18 @@ indexTypes.forEach(function(indexType) { pointDescription = (isLegacy ? "legacy coordinates" : "GeoJSON point"); function makeCommand(distance) { - var command = {geoNear: t.getName(), near: pointType, spherical: spherical}; - command[optionName] = distance; - return command; + let geoNearSpec = { + near: pointType, + distanceField: "dist", + spherical: spherical + }; + geoNearSpec[optionName] = distance; + + return { + aggregate: t.getName(), + cursor: {}, + pipeline: [{$geoNear: geoNearSpec}], + }; } // Unsupported combinations should return errors. @@ -40,17 +49,22 @@ indexTypes.forEach(function(indexType) { return; } - // This is a supported combination. No error. - assert.commandWorked( - db.runCommand({geoNear: t.getName(), near: pointType, spherical: spherical})); + // Test that there is no error when not specifying a min or max distance. + assert.commandWorked(db.runCommand({ + aggregate: t.getName(), + cursor: {}, + pipeline: [ + {$geoNear: {near: pointType, distanceField: "dist", spherical: spherical}} + ], + })); // No error with min/maxDistance 1. - db.runCommand(makeCommand(1)); + assert.commandWorked(db.runCommand(makeCommand(1))); var outOfRangeDistances = []; if (indexType == '2d') { // maxDistance unlimited; no error. - db.runCommand(makeCommand(1e10)); + assert.commandWorked(db.runCommand(makeCommand(1e10))); } // Try several bad values for min/maxDistance. diff --git a/jstests/core/geonear_key.js b/jstests/core/geonear_key.js index 747b6deacd7..41fcfb0a5da 100644 --- a/jstests/core/geonear_key.js +++ b/jstests/core/geonear_key.js @@ -1,5 +1,5 @@ /** - * Tests for the 'key' field accepted by the geoNear command and the $geoNear aggregation stage. + * Tests for the 'key' field accepted by the $geoNear aggregation stage. */ (function() { "use strict"; @@ -29,36 +29,39 @@ } /** - * Runs the near described by 'nearParams' as both a geoNear command and a $geoNear aggregation. - * Verifies that in both cases, the operation fails with 'code'. + * Runs the near described by 'nearParams' as a $geoNear aggregation and verifies that the + * operation fails with 'code'. */ function assertGeoNearFails(nearParams, code) { - assert.commandFailedWithCode(coll.runCommand("geoNear", nearParams), code); assert.commandFailedWithCode(runNearAgg(nearParams), code); } /** - * Runs the near described by 'nearParams' as both a geoNear command and a $geoNear aggregation. - * Verifies that in both cases, the operation succeeds and returns the _id values in - * 'expectedIds', in order. + * Runs the near described by 'nearParams' as a $geoNear aggregation and verifies that the + * operation returns the _id values in 'expectedIds', in order. */ function assertGeoNearSucceedsAndReturnsIds(nearParams, expectedIds) { - let cmdResult = assert.commandWorked(coll.runCommand("geoNear", nearParams)); let aggResult = assert.commandWorked(runNearAgg(nearParams)); + let res = aggResult.cursor.firstBatch; + let errfn = () => `expected ids ${tojson(expectedIds)}, but these documents were ` + + `returned: ${tojson(res)}`; + + assert.eq(expectedIds.length, res.length, errfn); for (let i = 0; i < expectedIds.length; i++) { - assert.eq(expectedIds[i], cmdResult.results[i].obj._id); - assert.eq(expectedIds[i], aggResult.cursor.firstBatch[i]._id); + assert.eq(expectedIds[i], aggResult.cursor.firstBatch[i]._id, errfn); } } - // Verify that the geoNear fails when the key field is not a string. + // Verify that $geoNear fails when the key field is not a string. assertGeoNearFails({near: [0, 0], key: 1}, ErrorCodes.TypeMismatch); - // Verify that the geoNear fails when the key field the empty string. + // Verify that $geoNear fails when the key field the empty string. assertGeoNearFails({near: [0, 0], key: ""}, ErrorCodes.BadValue); - // Verify that geoNear fails when there are no eligible indexes. + // Verify that $geoNear fails when there are no eligible indexes. assertGeoNearFails({near: [0, 0]}, ErrorCodes.IndexNotFound); + + // Verify that the query system raises an error when an index is specified that doesn't exist. assertGeoNearFails({near: [0, 0], key: "a"}, ErrorCodes.BadValue); // Create a number of 2d and 2dsphere indexes. @@ -67,7 +70,7 @@ assert.commandWorked(coll.createIndex({"b.c": "2d"})); assert.commandWorked(coll.createIndex({"b.d": "2dsphere"})); - // Verify that geoNear fails when the index to use is ambiguous because of the absence of the + // Verify that $geoNear fails when the index to use is ambiguous because of the absence of the // key field. assertGeoNearFails({near: [0, 0]}, ErrorCodes.IndexNotFound); @@ -85,12 +88,12 @@ assertGeoNearSucceedsAndReturnsIds( {near: {type: "Point", coordinates: [0, 0]}, spherical: true, key: "a"}, [0, 1]); - // Verify that geoNear fails when a GeoJSON point is used with a 'key' path that only has a 2d + // Verify that $geoNear fails when a GeoJSON point is used with a 'key' path that only has a 2d // index. GeoJSON points can only be used for spherical geometry. assertGeoNearFails({near: {type: "Point", coordinates: [0, 0]}, key: "b.c"}, ErrorCodes.BadValue); - // Verify that geoNear fails when: + // Verify that $geoNear fails when: // -- The only index available over the 'key' path is 2dsphere. // -- spherical=false. // -- The search point is a legacy coordinate pair. diff --git a/jstests/core/json_schema/misc_validation.js b/jstests/core/json_schema/misc_validation.js index ede7b55f55d..c8f5fcaf54b 100644 --- a/jstests/core/json_schema/misc_validation.js +++ b/jstests/core/json_schema/misc_validation.js @@ -50,18 +50,23 @@ coll.count({$jsonSchema: invalidSchema}); }); - // Test that an invalid $jsonSchema fails to parse in a geoNear command. + // Test that an invalid $jsonSchema fails to parse in a $geoNear query. assert.commandWorked(coll.createIndex({geo: "2dsphere"})); let res = testDB.runCommand({ - geoNear: coll.getName(), - near: [30, 40], - spherical: true, - query: {$jsonSchema: invalidSchema} + aggregate: coll.getName(), + cursor: {}, + pipeline: [{ + $geoNear: { + near: [30, 40], + distanceField: "dis", + query: {$jsonSchema: invalidSchema}, + } + }], }); - assert.commandFailed(res); + assert.commandFailedWithCode(res, ErrorCodes.FailedToParse); assert.neq(-1, - res.errmsg.indexOf("Can't parse filter"), - "geoNear command failed for a reason other than invalid query"); + res.errmsg.indexOf("Unknown $jsonSchema keyword"), + `$geoNear failed for a reason other than invalid query: ${tojson(res)}`); // Test that an invalid $jsonSchema fails to parse in a distinct command. assert.throws(function() { @@ -83,21 +88,23 @@ assert.eq(1, coll.count({$jsonSchema: {properties: {a: {type: "number"}, b: {type: "string"}}}})); - // Test that a valid $jsonSchema is legal in a geoNear command. + // Test that a valid $jsonSchema is legal in a $geoNear stage. const point = {type: "Point", coordinates: [31.0, 41.0]}; assert.writeOK(coll.insert({geo: point, a: 1})); assert.writeOK(coll.insert({geo: point, a: 0})); assert.commandWorked(coll.createIndex({geo: "2dsphere"})); - res = testDB.runCommand({ - geoNear: coll.getName(), - near: [30, 40], - spherical: true, - includeLocs: true, - query: {$jsonSchema: {properties: {a: {minimum: 1}}}} - }); - assert.commandWorked(res); - assert.eq(1, res.results.length); - assert.eq(res.results[0].loc, point); + res = coll.aggregate({ + $geoNear: { + near: [30, 40], + spherical: true, + query: {$jsonSchema: {properties: {a: {minimum: 1}}}}, + distanceField: "dis", + includeLocs: "loc", + } + }) + .toArray(); + assert.eq(1, res.length, tojson(res)); + assert.eq(res[0].loc, point, tojson(res)); // Test that a valid $jsonSchema is legal in a distinct command. coll.drop(); diff --git a/jstests/core/operation_latency_histogram.js b/jstests/core/operation_latency_histogram.js index 12885ade938..da9f14c6f8c 100644 --- a/jstests/core/operation_latency_histogram.js +++ b/jstests/core/operation_latency_histogram.js @@ -126,11 +126,17 @@ // TODO SERVER-24705: createIndex is not currently counted in Top. lastHistogram = assertHistogramDiffEq(testColl, lastHistogram, 0, 0, 0); - // GeoNear + // $geoNear aggregation stage assert.commandWorked(testDB.runCommand({ - geoNear: testColl.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true + aggregate: testColl.getName(), + pipeline: [{ + $geoNear: { + near: {type: "Point", coordinates: [0, 0]}, + spherical: true, + distanceField: "dist", + } + }], + cursor: {}, })); lastHistogram = assertHistogramDiffEq(testColl, lastHistogram, 1, 0, 0); diff --git a/jstests/core/profile_geonear.js b/jstests/core/profile_geonear.js deleted file mode 100644 index c17cfc84bb6..00000000000 --- a/jstests/core/profile_geonear.js +++ /dev/null @@ -1,57 +0,0 @@ -// @tags: [does_not_support_stepdowns, requires_profiling] - -// Confirms that profiled geonear execution contains all expected metrics with proper values. - -(function() { - "use strict"; - - // For getLatestProfilerEntry and getProfilerProtocolStringForCommand - load("jstests/libs/profiler.js"); - - var testDB = db.getSiblingDB("profile_geonear"); - assert.commandWorked(testDB.dropDatabase()); - var conn = testDB.getMongo(); - var coll = testDB.getCollection("test"); - - testDB.setProfilingLevel(2); - - // - // Confirm metrics for distinct with query. - // - var i; - for (i = 0; i < 10; ++i) { - assert.writeOK(coll.insert({a: i, loc: {type: "Point", coordinates: [i, i]}})); - } - assert.commandWorked(coll.createIndex({loc: "2dsphere"})); - - assert.commandWorked(testDB.runCommand({ - geoNear: "test", - near: {type: "Point", coordinates: [1, 1]}, - spherical: true, - collation: {locale: "fr"} - })); - - var profileObj = getLatestProfilerEntry(testDB); - - assert.eq(profileObj.ns, coll.getFullName(), tojson(profileObj)); - assert.eq(profileObj.op, "command", tojson(profileObj)); - assert.eq(profileObj.keysExamined, 82, tojson(profileObj)); - assert.eq(profileObj.docsExamined, 10, tojson(profileObj)); - assert.eq( - profileObj.planSummary, "GEO_NEAR_2DSPHERE { loc: \"2dsphere\" }", tojson(profileObj)); - assert(profileObj.hasOwnProperty("execStats"), tojson(profileObj)); - assert.eq(profileObj.protocol, getProfilerProtocolStringForCommand(conn), tojson(profileObj)); - assert.eq(coll.getName(), profileObj.command.geoNear, tojson(profileObj)); - assert.eq({locale: "fr"}, profileObj.command.collation, tojson(profileObj)); - assert(profileObj.hasOwnProperty("responseLength"), tojson(profileObj)); - assert(profileObj.hasOwnProperty("millis"), tojson(profileObj)); - assert(profileObj.hasOwnProperty("numYield"), tojson(profileObj)); - assert(profileObj.hasOwnProperty("locks"), tojson(profileObj)); - assert.eq(profileObj.appName, "MongoDB Shell", tojson(profileObj)); - - // We cannot confirm "fromMultiPlanner" or "replanned" metrics as there can be at most one - // valid index choice for geoNear. The reason for this is: - // - geoNear requires at least one "2d" or "2dsphere" index - // - geoNear requires there be at most one 2dsphere and at most one 2d index - // - geoNear will always prefer a 2d index over a 2dsphere index if both are defined -})(); diff --git a/jstests/core/txns/commands_not_allowed_in_txn.js b/jstests/core/txns/commands_not_allowed_in_txn.js index a7e9586ba47..4ecd712946b 100644 --- a/jstests/core/txns/commands_not_allowed_in_txn.js +++ b/jstests/core/txns/commands_not_allowed_in_txn.js @@ -97,7 +97,6 @@ {eval: "function() {return 1;}"}, {"$eval": "function() {return 1;}"}, {filemd5: 1, root: "fs"}, - {geoNear: collName, near: [0, 0]}, {mapReduce: collName, map: function() {}, reduce: function(key, vals) {}, out: "out"}, {parallelCollectionScan: collName, numCursors: 1}, ]; diff --git a/jstests/core/txns/statement_ids_accepted.js b/jstests/core/txns/statement_ids_accepted.js index 35760755ac6..68d57ac46dc 100644 --- a/jstests/core/txns/statement_ids_accepted.js +++ b/jstests/core/txns/statement_ids_accepted.js @@ -185,7 +185,8 @@ stmtId: NumberInt(0), autocommit: false })); - jsTestLog("Check that geoNear accepts a statement ID"); + + jsTestLog("Check that geoSearch accepts a statement ID"); assert.writeOK(testColl.insert({geo: {type: "Point", coordinates: [0, 0]}, a: 0}), {writeConcern: {w: "majority"}}); assert.writeOK(testColl.insert({geoh: {lat: 0, long: 0}, b: 0}), @@ -204,16 +205,6 @@ return true; }); assert.commandWorked(sessionDb.runCommand({ - geoNear: collName, - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - readConcern: {level: "snapshot"}, - txnNumber: NumberLong(txnNumber++), - stmtId: NumberInt(0) - })); - - jsTestLog("Check that geoSearch accepts a statement ID"); - assert.commandWorked(sessionDb.runCommand({ geoSearch: collName, search: {b: 0}, near: [0, 0], diff --git a/jstests/core/views/views_all_commands.js b/jstests/core/views/views_all_commands.js index de5c5dd1676..094c74a15cd 100644 --- a/jstests/core/views/views_all_commands.js +++ b/jstests/core/views/views_all_commands.js @@ -250,11 +250,6 @@ fsync: {skip: isUnrelated}, fsyncUnlock: {skip: isUnrelated}, getDatabaseVersion: {skip: isUnrelated}, - geoNear: { - command: - {geoNear: "view", near: {type: "Point", coordinates: [-50, 37]}, spherical: true}, - expectFailure: true - }, geoSearch: { command: { geoSearch: "view", diff --git a/jstests/libs/geo_near_random.js b/jstests/libs/geo_near_random.js index 207ca1302d9..d5de7aa70a7 100644 --- a/jstests/libs/geo_near_random.js +++ b/jstests/libs/geo_near_random.js @@ -43,17 +43,17 @@ GeoNearRandomTest.prototype.insertPts = function(nPts, indexBounds, scale) { this.t.ensureIndex({loc: '2d'}, indexBounds); }; -GeoNearRandomTest.prototype.assertIsPrefix = function(short, long) { +GeoNearRandomTest.prototype.assertIsPrefix = function(short, long, errmsg) { for (var i = 0; i < short.length; i++) { - var xS = short[i].obj ? short[i].obj.loc[0] : short[i].loc[0]; - var yS = short[i].obj ? short[i].obj.loc[1] : short[i].loc[1]; - var dS = short[i].obj ? short[i].dis : 1; + var xS = short[i] ? short[i].loc[0] : short[i].loc[0]; + var yS = short[i] ? short[i].loc[1] : short[i].loc[1]; + var dS = short[i] ? short[i].dis : 1; - var xL = long[i].obj ? long[i].obj.loc[0] : long[i].loc[0]; - var yL = long[i].obj ? long[i].obj.loc[1] : long[i].loc[1]; - var dL = long[i].obj ? long[i].dis : 1; + var xL = long[i] ? long[i].loc[0] : long[i].loc[0]; + var yL = long[i] ? long[i].loc[1] : long[i].loc[1]; + var dL = long[i] ? long[i].dis : 1; - assert.eq([xS, yS, dS], [xL, yL, dL]); + assert.eq([xS, yS, dS], [xL, yL, dL], errmsg); } }; @@ -66,37 +66,29 @@ GeoNearRandomTest.prototype.testPt = function(pt, opts) { print("testing point: " + tojson(pt) + " opts: " + tojson(opts)); - var cmd = {geoNear: this.t.getName(), near: pt, num: 1, spherical: opts.sphere}; + let query = {loc: {}}; + query.loc[opts.sphere ? '$nearSphere' : '$near'] = pt; + const proj = {dis: {$meta: "geoNearDistance"}}; + const runQuery = (limit) => this.t.find(query, proj).limit(opts.nToTest).toArray(); - var last = this.db.runCommand(cmd).results; + let last = runQuery(1); for (var i = 2; i <= opts.nToTest; i++) { - // print(i); // uncomment to watch status - cmd.num = i; - var ret = this.db.runCommand(cmd).results; - - try { - this.assertIsPrefix(last, ret); - } catch (e) { - print("*** failed while compairing " + (i - 1) + " and " + i); - printjson(cmd); - throw e; // rethrow - } - - // Make sure distances are in increasing order - assert.gte(ret[ret.length - 1].dis, last[last.length - 1].dis); + let ret = runQuery(i); + this.assertIsPrefix(last, ret, `Unexpected result when comparing ${i-1} and ${i}`); + // Make sure distances are in increasing order. + assert.gte(ret[ret.length - 1].dis, last[last.length - 1].dis); last = ret; } - last = last.map(function(x) { - return x.obj; - }); - - var query = {loc: {}}; - query.loc[opts.sphere ? '$nearSphere' : '$near'] = pt; - var near = this.t.find(query).limit(opts.nToTest).toArray(); - - // Test that a query using $near/$nearSphere with a limit of 'nToTest' returns the same points - // (in order) as the geoNear command with num=nToTest. - assert.eq(last, near); + // Test that a query using $near or $nearSphere returns the same points in order as the $geoNear + // aggregation stage. + const queryResults = runQuery(opts.nToTest); + const aggResults = this.t + .aggregate([ + {$geoNear: {near: pt, distanceField: "dis", spherical: opts.sphere}}, + {$limit: opts.nToTest} + ]) + .toArray(); + assert.eq(queryResults, aggResults); }; diff --git a/jstests/libs/override_methods/set_read_and_write_concerns.js b/jstests/libs/override_methods/set_read_and_write_concerns.js index f3b95d68ec7..d120ce08734 100644 --- a/jstests/libs/override_methods/set_read_and_write_concerns.js +++ b/jstests/libs/override_methods/set_read_and_write_concerns.js @@ -26,7 +26,6 @@ "count", "distinct", "find", - "geoNear", "geoSearch", "parallelCollectionScan", ]); diff --git a/jstests/libs/override_methods/set_read_preference_secondary.js b/jstests/libs/override_methods/set_read_preference_secondary.js index 5d5efedae97..58b80a5bc0c 100644 --- a/jstests/libs/override_methods/set_read_preference_secondary.js +++ b/jstests/libs/override_methods/set_read_preference_secondary.js @@ -14,7 +14,6 @@ "dbStats", "distinct", "find", - "geoNear", "geoSearch", "mapReduce", "mapreduce", diff --git a/jstests/libs/parallelTester.js b/jstests/libs/parallelTester.js index 2fbde0eb4f8..59f9b43f74d 100644 --- a/jstests/libs/parallelTester.js +++ b/jstests/libs/parallelTester.js @@ -259,7 +259,6 @@ if (typeof _threadInject != "undefined") { parallelFilesDir + "/profile_distinct.js", parallelFilesDir + "/profile_find.js", parallelFilesDir + "/profile_findandmodify.js", - parallelFilesDir + "/profile_geonear.js", parallelFilesDir + "/profile_getmore.js", parallelFilesDir + "/profile_insert.js", parallelFilesDir + "/profile_list_collections.js", diff --git a/jstests/noPassthrough/commands_handle_kill.js b/jstests/noPassthrough/commands_handle_kill.js index f03e40036a1..59470f63620 100644 --- a/jstests/noPassthrough/commands_handle_kill.js +++ b/jstests/noPassthrough/commands_handle_kill.js @@ -179,10 +179,21 @@ if (${ canYield }) { {findAndModify: collName, filter: {fakeField: {$gt: 0}}, update: {$inc: {a: 1}}}); assertCommandPropogatesPlanExecutorKillReason( - {geoNear: collName, near: {type: "Point", coordinates: [0, 0]}, spherical: true}, { - customSetup: function() { - assert.commandWorked(coll.createIndex({geoField: "2dsphere"})); - } + { + aggregate: collName, + cursor: {}, + pipeline: [{ + $geoNear: { + near: {type: "Point", coordinates: [0, 0]}, + spherical: true, + distanceField: "dis" + } + }] + }, + { + customSetup: function() { + assert.commandWorked(coll.createIndex({geoField: "2dsphere"})); + } }); assertCommandPropogatesPlanExecutorKillReason({find: coll.getName(), filter: {}}); diff --git a/jstests/noPassthrough/commands_preserve_exec_error_code.js b/jstests/noPassthrough/commands_preserve_exec_error_code.js index 38e12da2740..373461ad4f0 100644 --- a/jstests/noPassthrough/commands_preserve_exec_error_code.js +++ b/jstests/noPassthrough/commands_preserve_exec_error_code.js @@ -37,8 +37,9 @@ assertFailsWithInternalError(() => coll.deleteOne({_id: 1})); assertFailsWithInternalError(() => coll.count({_id: 1})); assertFailsWithInternalError(() => coll.aggregate([]).itcount()); + assertFailsWithInternalError( + () => coll.aggregate([{$geoNear: {near: [0, 0], distanceField: "d"}}]).itcount()); assertCmdFailsWithInternalError({distinct: coll.getName(), key: "_id"}); - assertCmdFailsWithInternalError({geoNear: coll.getName(), near: [0, 0]}); assertCmdFailsWithInternalError( {findAndModify: coll.getName(), query: {_id: 1}, update: {$set: {x: 2}}}); diff --git a/jstests/noPassthrough/currentop_query.js b/jstests/noPassthrough/currentop_query.js index 1eb1655f035..dfa7ae80fee 100644 --- a/jstests/noPassthrough/currentop_query.js +++ b/jstests/noPassthrough/currentop_query.js @@ -337,30 +337,39 @@ } // - // Confirm currentOp content for geoNear. + // Confirm currentOp content for the $geoNear aggregation stage. // dropAndRecreateTestCollection(); for (let i = 0; i < 10; ++i) { - assert.writeOK(coll.insert({a: i, loc: {type: "Point", coordinates: [i, i]}})); + assert.commandWorked( + coll.insert({a: i, loc: {type: "Point", coordinates: [i, i]}})); } assert.commandWorked(coll.createIndex({loc: "2dsphere"})); - confirmCurrentOpContents({ test: function(db) { assert.commandWorked(db.runCommand({ - geoNear: "currentop_query", - near: {type: "Point", coordinates: [1, 1]}, - spherical: true, - query: {$comment: "currentop_query"}, - collation: {locale: "fr"} + aggregate: "currentop_query", + cursor: {}, + pipeline: [{ + $geoNear: { + near: {type: "Point", coordinates: [1, 1]}, + distanceField: "dist", + spherical: true, + query: {$comment: "currentop_query"}, + } + }], + collation: {locale: "fr"}, + comment: "currentop_query", })); }, - command: "geoNear", planSummary: "GEO_NEAR_2DSPHERE { loc: \"2dsphere\" }", - currentOpFilter: { - "command.query.$comment": "currentop_query", - "command.collation": {locale: "fr"} - } + currentOpFilter: commandOrOriginatingCommand({ + "aggregate": {$exists: true}, + "pipeline.0.$geoNear.query.$comment": "currentop_query", + "collation": {locale: "fr"}, + "comment": "currentop_query", + }, + isRemoteShardCurOp) }); // diff --git a/jstests/noPassthrough/geo_full.js b/jstests/noPassthrough/geo_full.js index 6486679ea4e..b4622f46527 100644 --- a/jstests/noPassthrough/geo_full.js +++ b/jstests/noPassthrough/geo_full.js @@ -500,8 +500,6 @@ "poly.docIn": randYesQuery() }).count()); - var defaultDocLimit = 100; - // $near print("Near query..."); assert.eq( @@ -523,60 +521,38 @@ results.sphere.locsIn); } - // geoNear - // results limited by size of objects - if (data.maxLocs < defaultDocLimit) { - // GeoNear query - print("GeoNear query..."); - // GeoNear command has a default doc limit 100. - assert.eq( - Math.min(defaultDocLimit, results.center.docsIn), - t.getDB() - .runCommand( - {geoNear: "testAllGeo", near: query.center, maxDistance: query.radius}) - .results.length, - "GeoNear query: center: " + query.center + "; radius: " + query.radius + - "; docs: " + results.center.docsIn + "; locs: " + results.center.locsIn); - - var num = Math.min(2 * defaultDocLimit, 2 * results.center.docsIn); - - var output = db.runCommand({ - geoNear: "testAllGeo", - near: query.center, - maxDistance: query.radius, - includeLocs: true, - num: num - }).results; - - assert.eq(Math.min(num, results.center.docsIn), - output.length, - "GeoNear query with limit of " + num + ": center: " + query.center + - "; radius: " + query.radius + "; docs: " + results.center.docsIn + - "; locs: " + results.center.locsIn); - - var distance = 0; + // $geoNear aggregation stage. + const aggregationLimit = 2 * results.center.docsIn; + if (aggregationLimit > 0) { + var output = t.aggregate([ + { + $geoNear: { + near: query.center, + maxDistance: query.radius, + includeLocs: "pt", + distanceField: "dis", + } + }, + {$limit: aggregationLimit} + ]).toArray(); + + const errmsg = { + limit: aggregationLimit, + center: query.center, + radius: query.radius, + docs: results.center.docsIn, + locs: results.center.locsIn, + actualResult: output + }; + assert.eq(results.center.docsIn, output.length, tojson(errmsg)); + + let lastDistance = 0; for (var i = 0; i < output.length; i++) { var retDistance = output[i].dis; - var retLoc = locArray(output[i].loc); - - var arrLocs = locsArray(output[i].obj.locs); - - assert.contains(retLoc, arrLocs); - - var distInObj = false; - for (var j = 0; j < arrLocs.length && distInObj == false; j++) { - var newDistance = Geo.distance(locArray(query.center), arrLocs[j]); - distInObj = (newDistance >= retDistance - 0.0001 && - newDistance <= retDistance + 0.0001); - } - - assert(distInObj); - assert.between(retDistance - 0.0001, - Geo.distance(locArray(query.center), retLoc), - retDistance + 0.0001); + assert.close(retDistance, Geo.distance(locArray(query.center), output[i].pt)); assert.lte(retDistance, query.radius); - assert.gte(retDistance, distance); - distance = retDistance; + assert.gte(retDistance, lastDistance); + lastDistance = retDistance; } } diff --git a/jstests/noPassthrough/global_operation_latency_histogram.js b/jstests/noPassthrough/global_operation_latency_histogram.js index 3e5b4bcd8c8..d6ed2cc3c6e 100644 --- a/jstests/noPassthrough/global_operation_latency_histogram.js +++ b/jstests/noPassthrough/global_operation_latency_histogram.js @@ -102,11 +102,17 @@ assert.commandWorked(testColl.createIndex({pt: "2dsphere"})); lastHistogram = checkHistogramDiff(0, 0, 1); - // GeoNear + // $geoNear aggregation stage assert.commandWorked(testDB.runCommand({ - geoNear: testColl.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true + aggregate: testColl.getName(), + pipeline: [{ + $geoNear: { + near: {type: "Point", coordinates: [0, 0]}, + spherical: true, + distanceField: "dist", + } + }], + cursor: {}, })); lastHistogram = checkHistogramDiff(1, 0, 0); diff --git a/jstests/noPassthrough/log_format_slowms_samplerate_loglevel.js b/jstests/noPassthrough/log_format_slowms_samplerate_loglevel.js index 82893ddd1a2..3d0aa0dfbf3 100644 --- a/jstests/noPassthrough/log_format_slowms_samplerate_loglevel.js +++ b/jstests/noPassthrough/log_format_slowms_samplerate_loglevel.js @@ -320,25 +320,6 @@ keysDeleted: 1 }) }, - { - test: function(db) { - assert.commandWorked(db.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [1, 1]}, - spherical: true, - query: {$comment: logFormatTestComment}, - collation: {locale: "fr"} - })); - }, - logFields: { - command: "geoNear", - geoNear: coll.getName(), - near: {type: "Point", coordinates: [1, 1]}, - planSummary: "GEO_NEAR_2DSPHERE { loc: \"2dsphere\" }", - query: {$comment: logFormatTestComment}, - collation: {locale: "fr"} - } - } ]; // Confirm log contains collation for find command. diff --git a/jstests/noPassthrough/readConcern_snapshot.js b/jstests/noPassthrough/readConcern_snapshot.js index 7927593c911..8878e596987 100644 --- a/jstests/noPassthrough/readConcern_snapshot.js +++ b/jstests/noPassthrough/readConcern_snapshot.js @@ -158,18 +158,5 @@ assert.commandWorked(session.abortTransaction_forTesting()); session.endSession(); - // TODO: SERVER-34113 Remove this test when we completely remove snapshot - // reads since this command is not supported with transaction api. - // readConcern 'snapshot' is supported by geoNear. - session = rst.getPrimary().getDB(dbName).getMongo().startSession({causalConsistency: false}); - sessionDb = session.getDatabase(dbName); - assert.commandWorked(sessionDb.runCommand({ - geoNear: collName, - near: [0, 0], - readConcern: {level: "snapshot"}, - txnNumber: NumberLong(0), - })); - - session.endSession(); rst.stopSet(); }()); diff --git a/jstests/noPassthrough/readConcern_snapshot_mongos.js b/jstests/noPassthrough/readConcern_snapshot_mongos.js index 2d780eec4d5..61e7404a4ca 100644 --- a/jstests/noPassthrough/readConcern_snapshot_mongos.js +++ b/jstests/noPassthrough/readConcern_snapshot_mongos.js @@ -143,14 +143,5 @@ }), ErrorCodes.InvalidOptions); - // TODO SERVER-33712: Add snapshot support for geoNear on mongos. - assert.commandFailedWithCode(sessionDb.runCommand({ - geoNear: collName, - near: [0, 0], - readConcern: {level: "snapshot"}, - txnNumber: NumberLong(txnNumber++) - }), - ErrorCodes.InvalidOptions); - st.stop(); }()); diff --git a/jstests/noPassthrough/read_concern_snapshot_yielding.js b/jstests/noPassthrough/read_concern_snapshot_yielding.js index a88ddb5fe05..2a44a57d750 100644 --- a/jstests/noPassthrough/read_concern_snapshot_yielding.js +++ b/jstests/noPassthrough/read_concern_snapshot_yielding.js @@ -242,22 +242,6 @@ assert.eq(res.cursor.firstBatch.length, TestData.numDocs, tojson(res)); }, {"command.pipeline": [{$match: {x: 1}}]}); - // TODO: SERVER-34113 Remove this test when we completely remove snapshot - // reads since this command is not supported with transaction api. - // Test geoNear. - testCommand(function() { - const sessionId = db.getMongo().startSession({causalConsistency: false}).getSessionId(); - const res = assert.commandWorked(db.runCommand({ - geoNear: "coll", - near: [0, 0], - readConcern: {level: "snapshot"}, - lsid: sessionId, - txnNumber: NumberLong(0) - })); - assert(res.hasOwnProperty("results")); - assert.eq(res.results.length, TestData.numDocs, tojson(res)); - }, {"command.geoNear": "coll"}); - // Test getMore with an initial find batchSize of 0. Interrupt behavior of a getMore is not // expected to change with a change of batchSize in the originating command. testCommand(function() { diff --git a/jstests/noPassthrough/read_majority_reads.js b/jstests/noPassthrough/read_majority_reads.js index eec0ddc80cb..c10a86eac41 100644 --- a/jstests/noPassthrough/read_majority_reads.js +++ b/jstests/noPassthrough/read_majority_reads.js @@ -7,7 +7,6 @@ * - distinct * - count * - parallelCollectionScan - * - geoNear * - geoSearch * * Each operation is tested on a single node, and (if supported) through mongos on both sharded and @@ -55,6 +54,13 @@ 'aggregate', {readConcern: {level: 'majority'}, cursor: {batchSize: 0}, pipeline: []}))); }, + aggregateGeoNear: function(coll) { + return makeCursor(coll.getDB(), assert.commandWorked(coll.runCommand('aggregate', { + readConcern: {level: 'majority'}, + cursor: {batchSize: 0}, + pipeline: [{$geoNear: {near: [0, 0], distanceField: "d", spherical: true}}] + }))); + }, parallelCollectionScan: function(coll) { var res = coll.runCommand('parallelCollectionScan', {readConcern: {level: 'majority'}, numCursors: 1}); @@ -101,20 +107,6 @@ expectedBefore: 'before', expectedAfter: 'after', }, - geoNear: { - run: function(coll) { - var res = coll.runCommand('geoNear', { - readConcern: {level: 'majority'}, - near: [0, 0], - spherical: true, - }); - assert.commandWorked(res); - assert.eq(res.results.length, 1, tojson(res)); - return res.results[0].obj.state; - }, - expectedBefore: 'before', - expectedAfter: 'after', - }, geoSearch: { run: function(coll) { var res = coll.runCommand('geoSearch', { @@ -140,20 +132,21 @@ assert.commandWorked(mongodConnection.adminCommand({"setCommittedSnapshot": snapshot})); } + assert.commandWorked(coll.createIndex({point: '2dsphere'})); for (var testName in cursorTestCases) { jsTestLog('Running ' + testName + ' against ' + coll.toString()); var getCursor = cursorTestCases[testName]; // Setup initial state. assert.writeOK(coll.remove({})); - assert.writeOK(coll.save({_id: 1, state: 'before'})); + assert.writeOK(coll.save({_id: 1, state: 'before', point: [0, 0]})); setCommittedSnapshot(makeSnapshot()); // Check initial conditions. assert.eq(getCursor(coll).next().state, 'before'); // Change state without making it committed. - assert.writeOK(coll.save({_id: 1, state: 'after'})); + assert.writeOK(coll.save({_id: 1, state: 'after', point: [0, 0]})); // Cursor still sees old state. assert.eq(getCursor(coll).next().state, 'before'); @@ -171,7 +164,6 @@ assert.eq(oldCursor.next().state, 'after'); } - assert.commandWorked(coll.ensureIndex({point: '2dsphere'})); assert.commandWorked(coll.ensureIndex({point: 'geoHaystack', _id: 1}, {bucketSize: 1})); for (var testName in nonCursorTestCases) { jsTestLog('Running ' + testName + ' against ' + coll.toString()); diff --git a/jstests/noPassthrough/shell_can_use_read_concern.js b/jstests/noPassthrough/shell_can_use_read_concern.js index 5a3134af9cb..dd9f7ed6ec1 100644 --- a/jstests/noPassthrough/shell_can_use_read_concern.js +++ b/jstests/noPassthrough/shell_can_use_read_concern.js @@ -226,22 +226,6 @@ }); // - // Tests for the "geoNear" command. - // - - testCommandCanBeCausallyConsistent(function() { - assert.commandWorked(coll.createIndex({loc: "2dsphere"})); - }, {expectedSession: withSession, expectedAfterClusterTime: false}); - - testCommandCanBeCausallyConsistent(function() { - assert.commandWorked(db.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true - })); - }); - - // // Tests for the "geoSearch" command. // diff --git a/jstests/noPassthroughWithMongod/geo_axis_aligned.js b/jstests/noPassthroughWithMongod/geo_axis_aligned.js index 5e08a6c1739..0849b6b7acc 100644 --- a/jstests/noPassthroughWithMongod/geo_axis_aligned.js +++ b/jstests/noPassthroughWithMongod/geo_axis_aligned.js @@ -89,9 +89,14 @@ for (var b = 0; b < bits.length; b++) { print(" DOING DIST QUERY "); - a = db.runCommand({geoNear: "axisaligned", near: center[j], maxDistance: radius[i]}) - .results; - assert.eq(5, a.length); + a = t.aggregate({ + $geoNear: { + near: center[j], + distanceField: "dis", + maxDistance: radius[i], + } + }).toArray(); + assert.eq(5, a.length, tojson(a)); var distance = 0; for (var k = 0; k < a.length; k++) { diff --git a/jstests/readonly/geo.js b/jstests/readonly/geo.js index 13705af0408..2ba43f597b4 100644 --- a/jstests/readonly/geo.js +++ b/jstests/readonly/geo.js @@ -30,14 +30,19 @@ runReadOnlyTest(function() { writableCollection.insertMany(locDocs); }, exec: function(readableCollection) { - var res = readableCollection.runCommand({ - geoNear: readableCollection.getName(), - near: {type: "Point", coordinates: [40.7211404, -73.9591494]}, - spherical: true, - limit: 1 - }); - assert.commandWorked(res); - assert.eq(res.results[0].obj.name, "The Counting Room", printjson(res)); + const res = readableCollection + .aggregate([ + { + $geoNear: { + near: {type: "Point", coordinates: [40.7211404, -73.9591494]}, + distanceField: "dist", + spherical: true, + } + }, + {$limit: 1} + ]) + .toArray(); + assert.eq(res[0].name, "The Counting Room", printjson(res)); } }; }()); diff --git a/jstests/sharding/causal_consistency_shell_support.js b/jstests/sharding/causal_consistency_shell_support.js index 2ac39c6b66b..5dd66ffc771 100644 --- a/jstests/sharding/causal_consistency_shell_support.js +++ b/jstests/sharding/causal_consistency_shell_support.js @@ -156,15 +156,24 @@ runCommandAndCheckLogicalTimes(findCmd, testDB, false); commandReturnsExpectedResult(findCmd, testDB, findCallback); - // GeoNear command. + // Aggregate command with $geoNear. let geoNearColl = "geoNearColl"; let geoNearCmd = { - geoNear: geoNearColl, - near: {type: "Point", coordinates: [-10, 10]}, - spherical: true + aggregate: geoNearColl, + cursor: {}, + pipeline: [ + { + $geoNear: { + near: {type: "Point", coordinates: [-10, 10]}, + distanceField: "dist", + spherical: true + } + }, + ], }; let geoNearCallback = function(res) { - assert.eq(res.results[0].obj, {_id: 1, loc: {type: "Point", coordinates: [-10, 10]}}); + assert.eq(res.cursor.firstBatch, + [{_id: 1, loc: {type: "Point", coordinates: [-10, 10]}, dist: 0}]); }; assert.commandWorked(testDB[geoNearColl].createIndex({loc: "2dsphere"})); diff --git a/jstests/sharding/collation_targeting.js b/jstests/sharding/collation_targeting.js index 21740339d0f..fc2b9c193eb 100644 --- a/jstests/sharding/collation_targeting.js +++ b/jstests/sharding/collation_targeting.js @@ -13,7 +13,7 @@ assert.commandWorked(testDB.adminCommand({enableSharding: testDB.getName()})); st.ensurePrimaryShard(testDB.getName(), st.shard1.shardName); - // Create a collection sharded on {a: 1}. Add 2dsphere index to test geoNear. + // Create a collection sharded on {a: 1}. Add 2dsphere index to test $geoNear. var coll = testDB.getCollection("simple_collation"); coll.drop(); assert.commandWorked(coll.createIndex({a: 1})); @@ -38,7 +38,7 @@ // st.shard0.shardName: {a: 1} // st.shard1.shardName: {a: 100}, {a: "FOO"} // shard0002: {a: "foo"} - // Include geo field to test geoNear. + // Include geo field to test $geoNear. var a_1 = {_id: 0, a: 1, geo: {type: "Point", coordinates: [0, 0]}}; var a_100 = {_id: 1, a: 100, geo: {type: "Point", coordinates: [0, 0]}}; var a_FOO = {_id: 2, a: "FOO", geo: {type: "Point", coordinates: [0, 0]}}; @@ -70,6 +70,46 @@ assert.commandWorked(explain); assert.eq(1, Object.keys(explain.shards).length); + // Aggregate with $geoNear. + const geoJSONPoint = {type: "Point", coordinates: [0, 0]}; + + // Test $geoNear with a query on strings with a non-simple collation. This should + // scatter-gather. + const geoNearStageStringQuery = [{ + $geoNear: { + near: geoJSONPoint, + distanceField: "dist", + spherical: true, + query: {a: "foo"}, + } + }]; + assert.eq(2, coll.aggregate(geoNearStageStringQuery, {collation: caseInsensitive}).itcount()); + explain = coll.explain().aggregate(geoNearStageStringQuery, {collation: caseInsensitive}); + assert.commandWorked(explain); + assert.eq(3, Object.keys(explain.shards).length); + + // Test $geoNear with a query on strings with a simple collation. This should be single-shard. + assert.eq(1, coll.aggregate(geoNearStageStringQuery).itcount()); + explain = coll.explain().aggregate(geoNearStageStringQuery); + assert.commandWorked(explain); + assert.eq(1, Object.keys(explain.shards).length); + + // Test a $geoNear with a query on numbers with a non-simple collation. This should be + // single-shard. + const geoNearStageNumericalQuery = [{ + $geoNear: { + near: geoJSONPoint, + distanceField: "dist", + spherical: true, + query: {a: 100}, + } + }]; + assert.eq(1, + coll.aggregate(geoNearStageNumericalQuery, {collation: caseInsensitive}).itcount()); + explain = coll.explain().aggregate(geoNearStageNumericalQuery, {collation: caseInsensitive}); + assert.commandWorked(explain); + assert.eq(1, Object.keys(explain.shards).length); + // Count. // Test a count command on strings with a non-simple collation. This should be scatter-gather. @@ -168,31 +208,6 @@ assert.commandWorked(explain); assert.eq(1, explain.queryPlanner.winningPlan.shards.length); - // GeoNear. - - // Test geoNear on strings with a non-simple collation. - assert.eq(2, - assert - .commandWorked(testDB.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {a: "foo"}, - collation: caseInsensitive - })) - .results.length); - - // Test geoNear on strings with a simple collation. - assert.eq(1, - assert - .commandWorked(testDB.runCommand({ - geoNear: coll.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {a: "foo"} - })) - .results.length); - // MapReduce. // Test mapReduce on strings with a non-simple collation. @@ -444,4 +459,4 @@ } st.stop(); -})();
\ No newline at end of file +})(); diff --git a/jstests/sharding/collation_targeting_inherited.js b/jstests/sharding/collation_targeting_inherited.js index b9e88059dc1..4c68e23fbc7 100644 --- a/jstests/sharding/collation_targeting_inherited.js +++ b/jstests/sharding/collation_targeting_inherited.js @@ -80,6 +80,49 @@ assert.commandWorked(explain); assert.eq(1, Object.keys(explain.shards).length); + // Aggregate with $geoNear. + const geoJSONPoint = {type: "Point", coordinates: [0, 0]}; + + // Test $geoNear with a query on strings with a non-simple collation inherited from the + // collection default. This should scatter-gather. + const geoNearStageStringQuery = [{ + $geoNear: { + near: geoJSONPoint, + distanceField: "dist", + spherical: true, + query: {a: "foo"}, + } + }]; + assert.eq(2, collCaseInsensitive.aggregate(geoNearStageStringQuery).itcount()); + explain = collCaseInsensitive.explain().aggregate(geoNearStageStringQuery); + assert.commandWorked(explain); + assert.eq(3, Object.keys(explain.shards).length); + + // Test $geoNear with a query on strings with a simple collation. This should be single-shard. + assert.eq( + 1, + collCaseInsensitive.aggregate(geoNearStageStringQuery, {collation: {locale: "simple"}}) + .itcount()); + explain = collCaseInsensitive.explain().aggregate(geoNearStageStringQuery, + {collation: {locale: "simple"}}); + assert.commandWorked(explain); + assert.eq(1, Object.keys(explain.shards).length); + + // Test a $geoNear with a query on numbers with a non-simple collation inherited from the + // collection default. This should be single-shard. + const geoNearStageNumericalQuery = [{ + $geoNear: { + near: geoJSONPoint, + distanceField: "dist", + spherical: true, + query: {a: 100}, + } + }]; + assert.eq(1, collCaseInsensitive.aggregate(geoNearStageNumericalQuery).itcount()); + explain = collCaseInsensitive.explain().aggregate(geoNearStageNumericalQuery); + assert.commandWorked(explain); + assert.eq(1, Object.keys(explain.shards).length); + // Count. // Test a count command on strings with a non-simple collation inherited from the collection @@ -184,31 +227,6 @@ assert.commandWorked(explain); assert.eq(1, explain.queryPlanner.winningPlan.shards.length); - // GeoNear. - - // Test geoNear on strings with a non-simple collation inherited from collection default. - assert.eq(2, - assert - .commandWorked(testDB.runCommand({ - geoNear: collCaseInsensitive.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {a: "foo"} - })) - .results.length); - - // Test geoNear on strings with a simple collation. - assert.eq(1, - assert - .commandWorked(testDB.runCommand({ - geoNear: collCaseInsensitive.getName(), - near: {type: "Point", coordinates: [0, 0]}, - spherical: true, - query: {a: "foo"}, - collation: {locale: "simple"} - })) - .results.length); - // MapReduce. // Test mapReduce on strings with a non-simple collation inherited from collection default. @@ -461,4 +479,4 @@ assert.eq(1, explain.queryPlanner.winningPlan.shards.length); st.stop(); -})();
\ No newline at end of file +})(); diff --git a/jstests/sharding/database_and_shard_versioning_all_commands.js b/jstests/sharding/database_and_shard_versioning_all_commands.js index 5ffc2abb8d3..37f3ef5fbd0 100644 --- a/jstests/sharding/database_and_shard_versioning_all_commands.js +++ b/jstests/sharding/database_and_shard_versioning_all_commands.js @@ -227,21 +227,6 @@ }, flushRouterConfig: {skip: "executes locally on mongos (not sent to any remote node)"}, fsync: {skip: "broadcast to all shards"}, - geoNear: { - sendsDbVersion: true, - sendsShardVersion: true, - setUp: function(mongosConn) { - // Expects the collection to exist with a geo index, and does not implicitly create - // the collection or index. - assert.commandWorked(mongosConn.getCollection(ns).runCommand( - {createIndexes: collName, indexes: [{key: {loc: "2d"}, name: "loc_2d"}]})); - assert.writeOK(mongosConn.getCollection(ns).insert({x: 1, loc: [1, 1]})); - }, - command: {geoNear: collName, near: [1, 1]}, - cleanUp: function(mongosConn) { - assert(mongosConn.getDB(dbName).getCollection(collName).drop()); - } - }, getCmdLineOpts: {skip: "executes locally on mongos (not sent to any remote node)"}, getDiagnosticData: {skip: "executes locally on mongos (not sent to any remote node)"}, getLastError: {skip: "does not forward command to primary shard"}, diff --git a/jstests/sharding/geo_near_sharded.js b/jstests/sharding/geo_near_sharded.js index f297b789747..361468cec18 100644 --- a/jstests/sharding/geo_near_sharded.js +++ b/jstests/sharding/geo_near_sharded.js @@ -41,9 +41,20 @@ assert.commandWorked(db[coll].ensureIndex({loc: indexType})); - assert.commandWorked( - db.runCommand({geoNear: coll, near: [0, 0], spherical: true, includeLocs: true}), - tojson({sharded: sharded, indexType: indexType})); + let res = assert.commandWorked(db.runCommand({ + aggregate: coll, + cursor: {}, + pipeline: [{ + $geoNear: { + near: [0, 0], + spherical: true, + includeLocs: "match", + distanceField: "dist", + } + }] + }), + tojson({sharded: sharded, indexType: indexType})); + assert.gt(res.cursor.firstBatch.length, 0, tojson(res)); } // TODO: SERVER-33954 Remove shardAsReplicaSet: false diff --git a/jstests/sharding/read_pref_cmd.js b/jstests/sharding/read_pref_cmd.js index 2c3e3e2f95e..0576d71231a 100644 --- a/jstests/sharding/read_pref_cmd.js +++ b/jstests/sharding/read_pref_cmd.js @@ -164,7 +164,6 @@ var testReadPreference = function(conn, hostList, isMongos, mode, tagSets, secEx testDB.user.ensureIndex({loc: '2d'}); testDB.user.ensureIndex({position: 'geoHaystack', type: 1}, {bucketSize: 10}); testDB.runCommand({getLastError: 1, w: NODE_COUNT}); - cmdTest({geoNear: 'user', near: [1, 1]}, true, formatProfileQuery({geoNear: 'user'})); // Mongos doesn't implement geoSearch; test it only with ReplicaSetConnection. if (!isMongos) { diff --git a/jstests/sharding/safe_secondary_reads_drop_recreate.js b/jstests/sharding/safe_secondary_reads_drop_recreate.js index d8f4f7fe8d2..d8a336073c3 100644 --- a/jstests/sharding/safe_secondary_reads_drop_recreate.js +++ b/jstests/sharding/safe_secondary_reads_drop_recreate.js @@ -178,7 +178,11 @@ {createIndexes: coll, indexes: [{key: {loc: "2d"}, name: "loc_2d"}]})); assert.writeOK(mongosConn.getCollection(nss).insert({x: 1, loc: [1, 1]})); }, - command: {geoNear: coll, near: [1, 1]}, + command: { + aggregate: coll, + cursor: {}, + pipeline: [{$geoNear: {near: [1, 1], distanceField: "d"}}] + }, checkResults: function(res) { // The command should fail on the new collection, because the geo index was dropped. assert.commandFailed(res); diff --git a/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js b/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js index 13605561e74..a205a0ca8a1 100644 --- a/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js +++ b/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js @@ -202,29 +202,6 @@ forceerror: {skip: "does not return user data"}, fsync: {skip: "does not return user data"}, fsyncUnlock: {skip: "does not return user data"}, - geoNear: { - setUp: function(mongosConn) { - assert.commandWorked(mongosConn.getCollection(nss).runCommand( - {createIndexes: coll, indexes: [{key: {loc: "2d"}, name: "loc_2d"}]})); - assert.writeOK(mongosConn.getCollection(nss).insert({x: 1, loc: [1, 1]})); - }, - command: {geoNear: coll, near: [1, 1]}, - checkResults: function(res) { - // The command should work and return orphaned results, because it doesn't do - // filtering and also because the collection is sharded, it will get broadcast to - // both shards. - assert.commandWorked(res); - assert.eq(2, res.results.length, tojson(res)); - }, - checkAvailableReadConcernResults: function(res) { - // The command should work and return orphaned results, because it doesn't do - // filtering. The expected result is 1, because the stale mongos assumes the - // collection is still on only 1 shard and will not broadcast it to both. - assert.commandWorked(res); - assert.eq(1, res.results.length, tojson(res)); - }, - behavior: "versioned" - }, geoSearch: {skip: "not supported in mongos"}, getCmdLineOpts: {skip: "does not return user data"}, getDiagnosticData: {skip: "does not return user data"}, diff --git a/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js b/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js index 85f236dbae1..c748456fb1b 100644 --- a/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js +++ b/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js @@ -175,20 +175,6 @@ forceerror: {skip: "does not return user data"}, fsync: {skip: "does not return user data"}, fsyncUnlock: {skip: "does not return user data"}, - geoNear: { - setUp: function(mongosConn) { - assert.commandWorked(mongosConn.getCollection(nss).runCommand( - {createIndexes: coll, indexes: [{key: {loc: "2d"}, name: "loc_2d"}]})); - assert.writeOK(mongosConn.getCollection(nss).insert({x: 1, loc: [1, 1]})); - }, - command: {geoNear: coll, near: [1, 1]}, - checkResults: function(res) { - // The command should work and return correct results due to rerouting - assert.commandWorked(res); - assert.eq(1, res.results.length, tojson(res)); - }, - behavior: "versioned" - }, geoSearch: {skip: "not supported in mongos"}, getCmdLineOpts: {skip: "does not return user data"}, getDiagnosticData: {skip: "does not return user data"}, diff --git a/jstests/sharding/shard7.js b/jstests/sharding/shard7.js index 5ec1801b7c4..20122b60e24 100644 --- a/jstests/sharding/shard7.js +++ b/jstests/sharding/shard7.js @@ -49,8 +49,8 @@ assert.eq(0, c.count({c: 1})); c.ensureIndex({loc: '2d'}); c.save({a: 2, b: 2, loc: [0, 0]}); -near = db.runCommand({geoNear: 'foo', near: [0, 0], query: unsatisfiable}); -assert.commandWorked(near); -assert.eq(0, near.results.length); +near = + c.aggregate({$geoNear: {near: [0, 0], query: unsatisfiable, distanceField: "dist"}}).toArray(); +assert.eq(0, near.length, tojson(near)); s.stop(); diff --git a/src/mongo/db/SConscript b/src/mongo/db/SConscript index d329ef5b1ea..736619ab543 100644 --- a/src/mongo/db/SConscript +++ b/src/mongo/db/SConscript @@ -976,6 +976,7 @@ env.Library( 'query/explain.cpp', 'query/find.cpp', 'pipeline/document_source_cursor.cpp', + 'pipeline/document_source_geo_near_cursor.cpp', 'pipeline/pipeline_d.cpp', 'query/get_executor.cpp', 'query/internal_plans.cpp', diff --git a/src/mongo/db/commands/SConscript b/src/mongo/db/commands/SConscript index 6efa6d3e148..8e718100a33 100644 --- a/src/mongo/db/commands/SConscript +++ b/src/mongo/db/commands/SConscript @@ -212,7 +212,6 @@ env.Library( "explain_cmd.cpp", "find_and_modify.cpp", "find_cmd.cpp", - "geo_near_cmd.cpp", "get_last_error.cpp", "getmore_cmd.cpp", "index_filter_commands.cpp", diff --git a/src/mongo/db/commands/geo_near_cmd.cpp b/src/mongo/db/commands/geo_near_cmd.cpp deleted file mode 100644 index 7385f6645f5..00000000000 --- a/src/mongo/db/commands/geo_near_cmd.cpp +++ /dev/null @@ -1,404 +0,0 @@ -/** -* Copyright (C) 2012-2014 MongoDB Inc. -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License, version 3, -* as published by the Free Software Foundation. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see <http://www.gnu.org/licenses/>. -* -* As a special exception, the copyright holders give permission to link the -* code of portions of this program with the OpenSSL library under certain -* conditions as described in each individual source file and distribute -* linked combinations including the program with the OpenSSL library. You -* must comply with the GNU Affero General Public License in all respects for -* all of the code used other than as permitted herein. If you modify file(s) -* with this exception, you may extend this exception to your version of the -* file(s), but you are not obligated to do so. If you do not wish to do so, -* delete this exception statement from your version. If you delete this -* exception statement from all source files in the program, then also delete -* it in the license file. -*/ - -#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kCommand - -#include <vector> - -#include "mongo/bson/util/bson_extract.h" -#include "mongo/db/auth/action_set.h" -#include "mongo/db/auth/action_type.h" -#include "mongo/db/auth/privilege.h" -#include "mongo/db/catalog/collection.h" -#include "mongo/db/catalog/database.h" -#include "mongo/db/catalog/index_catalog.h" -#include "mongo/db/client.h" -#include "mongo/db/commands.h" -#include "mongo/db/curop.h" -#include "mongo/db/db_raii.h" -#include "mongo/db/exec/working_set_common.h" -#include "mongo/db/geo/geoconstants.h" -#include "mongo/db/geo/geoparser.h" -#include "mongo/db/index/index_descriptor.h" -#include "mongo/db/index_names.h" -#include "mongo/db/jsobj.h" -#include "mongo/db/matcher/expression_geo.h" -#include "mongo/db/matcher/extensions_callback_real.h" -#include "mongo/db/pipeline/document_source_geo_near.h" -#include "mongo/db/query/explain.h" -#include "mongo/db/query/find_common.h" -#include "mongo/db/query/get_executor.h" -#include "mongo/db/query/plan_summary_stats.h" -#include "mongo/util/log.h" - -namespace mongo { - -using std::unique_ptr; -using std::stringstream; - -/** - * The geoNear command is deprecated. Users should prefer the $near query operator, the $nearSphere - * query operator, or the $geoNear aggregation stage. See - * http://dochub.mongodb.org/core/geoNear-deprecation for more detail. - */ -class Geo2dFindNearCmd : public ErrmsgCommandDeprecated { -public: - Geo2dFindNearCmd() : ErrmsgCommandDeprecated("geoNear") {} - - virtual bool supportsWriteConcern(const BSONObj& cmd) const override { - return false; - } - AllowedOnSecondary secondaryAllowed(ServiceContext*) const override { - return AllowedOnSecondary::kAlways; - } - bool supportsReadConcern(const std::string& dbName, - const BSONObj& cmdObj, - repl::ReadConcernLevel level) const final { - return true; - } - - ReadWriteType getReadWriteType() const { - return ReadWriteType::kRead; - } - - std::size_t reserveBytesForReply() const override { - return FindCommon::kInitReplyBufferSize; - } - - std::string help() const override { - return "http://dochub.mongodb.org/core/geo#GeospatialIndexing-geoNearCommand"; - } - - virtual void addRequiredPrivileges(const std::string& dbname, - const BSONObj& cmdObj, - std::vector<Privilege>* out) const { - ActionSet actions; - actions.addAction(ActionType::find); - out->push_back(Privilege(parseResourcePattern(dbname, cmdObj), actions)); - } - - bool errmsgRun(OperationContext* opCtx, - const string& dbname, - const BSONObj& cmdObj, - string& errmsg, - BSONObjBuilder& result) { - // Do not log the deprecation warning when in a direct client, since the $geoNear - // aggregation stage runs the geoNear command in a direct client. - RARELY if (!opCtx->getClient()->isInDirectClient()) { - warning() << "Support for the geoNear command has been deprecated. Please plan to " - "rewrite geoNear commands using the $near query operator, the $nearSphere " - "query operator, or the $geoNear aggregation stage. See " - "http://dochub.mongodb.org/core/geoNear-deprecation."; - } - - if (!cmdObj["start"].eoo()) { - errmsg = "using deprecated 'start' argument to geoNear"; - return false; - } - - const NamespaceString nss(CommandHelpers::parseNsCollectionRequired(dbname, cmdObj)); - AutoGetCollectionForReadCommand ctx(opCtx, nss); - - Collection* collection = ctx.getCollection(); - if (!collection) { - errmsg = "can't find ns"; - return false; - } - - auto nearFieldName = getFieldName(opCtx, collection, cmdObj); - - PointWithCRS point; - uassert(17304, - "'near' field must be point", - GeoParser::parseQueryPoint(cmdObj["near"], &point).isOK()); - - bool isSpherical = cmdObj["spherical"].trueValue(); - - // Build the $near expression for the query. - BSONObjBuilder nearBob; - if (isSpherical) { - nearBob.append("$nearSphere", cmdObj["near"].Obj()); - } else { - nearBob.append("$near", cmdObj["near"].Obj()); - } - - if (!cmdObj["maxDistance"].eoo()) { - uassert(17299, "maxDistance must be a number", cmdObj["maxDistance"].isNumber()); - nearBob.append("$maxDistance", cmdObj["maxDistance"].number()); - } - - if (!cmdObj["minDistance"].eoo()) { - uassert(17300, "minDistance must be a number", cmdObj["minDistance"].isNumber()); - nearBob.append("$minDistance", cmdObj["minDistance"].number()); - } - - if (!cmdObj["uniqueDocs"].eoo()) { - warning() << nss << ": ignoring deprecated uniqueDocs option in geoNear command"; - } - - // And, build the full query expression. - BSONObjBuilder queryBob; - queryBob.append(nearFieldName, nearBob.obj()); - if (!cmdObj["query"].eoo() && cmdObj["query"].isABSONObj()) { - queryBob.appendElements(cmdObj["query"].Obj()); - } - BSONObj rewritten = queryBob.obj(); - - // Extract the collation, if it exists. - BSONObj collation; - { - BSONElement collationElt; - Status collationEltStatus = - bsonExtractTypedField(cmdObj, "collation", BSONType::Object, &collationElt); - if (!collationEltStatus.isOK() && (collationEltStatus != ErrorCodes::NoSuchKey)) { - uassertStatusOK(collationEltStatus); - } - if (collationEltStatus.isOK()) { - collation = collationElt.Obj(); - } - } - - long long numWanted = 100; - const char* limitName = !cmdObj["num"].eoo() ? "num" : "limit"; - BSONElement eNumWanted = cmdObj[limitName]; - if (!eNumWanted.eoo()) { - uassert(17303, "limit must be number", eNumWanted.isNumber()); - numWanted = eNumWanted.safeNumberLong(); - uassert(17302, "limit must be >=0", numWanted >= 0); - } - - bool includeLocs = false; - if (!cmdObj["includeLocs"].eoo()) { - includeLocs = cmdObj["includeLocs"].trueValue(); - } - - double distanceMultiplier = 1.0; - BSONElement eDistanceMultiplier = cmdObj["distanceMultiplier"]; - if (!eDistanceMultiplier.eoo()) { - uassert(17296, "distanceMultiplier must be a number", eDistanceMultiplier.isNumber()); - distanceMultiplier = eDistanceMultiplier.number(); - uassert(17297, "distanceMultiplier must be non-negative", distanceMultiplier >= 0); - } - - BSONObj projObj = BSON("$pt" << BSON("$meta" << QueryRequest::metaGeoNearPoint) << "$dis" - << BSON("$meta" << QueryRequest::metaGeoNearDistance)); - - auto qr = stdx::make_unique<QueryRequest>(nss); - qr->setFilter(rewritten); - qr->setProj(projObj); - qr->setLimit(numWanted); - qr->setCollation(collation); - const ExtensionsCallbackReal extensionsCallback(opCtx, &nss); - const boost::intrusive_ptr<ExpressionContext> expCtx; - auto statusWithCQ = - CanonicalQuery::canonicalize(opCtx, - std::move(qr), - expCtx, - extensionsCallback, - MatchExpressionParser::kAllowAllSpecialFeatures); - if (!statusWithCQ.isOK()) { - errmsg = "Can't parse filter / create query"; - return false; - } - unique_ptr<CanonicalQuery> cq = std::move(statusWithCQ.getValue()); - - // Prevent chunks from being cleaned up during yields - this allows us to only check the - // version on initial entry into geoNear. - auto rangePreserver = CollectionShardingState::get(opCtx, nss)->getMetadata(opCtx); - - const auto& readConcernArgs = repl::ReadConcernArgs::get(opCtx); - const PlanExecutor::YieldPolicy yieldPolicy = - readConcernArgs.getLevel() == repl::ReadConcernLevel::kSnapshotReadConcern - ? PlanExecutor::INTERRUPT_ONLY - : PlanExecutor::YIELD_AUTO; - auto exec = uassertStatusOK(getExecutor(opCtx, collection, std::move(cq), yieldPolicy, 0)); - - auto curOp = CurOp::get(opCtx); - { - stdx::lock_guard<Client> lk(*opCtx->getClient()); - curOp->setPlanSummary_inlock(Explain::getPlanSummary(exec.get())); - } - - double totalDistance = 0; - BSONObjBuilder resultBuilder(result.subarrayStart("results")); - double farthestDist = 0; - - BSONObj currObj; - long long results = 0; - PlanExecutor::ExecState state; - while (PlanExecutor::ADVANCED == (state = exec->getNext(&currObj, NULL))) { - // Come up with the correct distance. - double dist = currObj["$dis"].number() * distanceMultiplier; - totalDistance += dist; - if (dist > farthestDist) { - farthestDist = dist; - } - - // Strip out '$dis' and '$pt' from the result obj. The rest gets added as 'obj' - // in the command result. - BSONObjIterator resIt(currObj); - BSONObjBuilder resBob; - while (resIt.more()) { - BSONElement elt = resIt.next(); - if (!mongoutils::str::equals("$pt", elt.fieldName()) && - !mongoutils::str::equals("$dis", elt.fieldName())) { - resBob.append(elt); - } - } - BSONObj resObj = resBob.obj(); - - // Don't make a too-big result object. - if (resultBuilder.len() + resObj.objsize() > BSONObjMaxUserSize) { - warning() << "Too many geoNear results for query " << redact(rewritten) - << ", truncating output."; - break; - } - - // Add the next result to the result builder. - BSONObjBuilder oneResultBuilder( - resultBuilder.subobjStart(BSONObjBuilder::numStr(results))); - oneResultBuilder.append("dis", dist); - if (includeLocs) { - oneResultBuilder.appendAs(currObj["$pt"], "loc"); - } - oneResultBuilder.append("obj", resObj); - oneResultBuilder.done(); - - ++results; - - // Break if we have the number of requested result documents. - if (results >= numWanted) { - break; - } - } - - resultBuilder.done(); - - // Return an error if execution fails for any reason. - if (PlanExecutor::FAILURE == state || PlanExecutor::DEAD == state) { - log() << "Plan executor error during geoNear command: " << PlanExecutor::statestr(state) - << ", stats: " << redact(Explain::getWinningPlanStats(exec.get())); - - uassertStatusOK(WorkingSetCommon::getMemberObjectStatus(currObj).withContext( - "Executor error during geoNear command")); - } - - PlanSummaryStats summary; - Explain::getSummaryStats(*exec, &summary); - - // Fill out the stats subobj. - BSONObjBuilder stats(result.subobjStart("stats")); - - stats.appendNumber("nscanned", summary.totalKeysExamined); - stats.appendNumber("objectsLoaded", summary.totalDocsExamined); - - if (results > 0) { - stats.append("avgDistance", totalDistance / results); - } - stats.append("maxDistance", farthestDist); - stats.appendIntOrLL("time", - durationCount<Microseconds>(curOp->elapsedTimeExcludingPauses())); - stats.done(); - - collection->infoCache()->notifyOfQuery(opCtx, summary.indexesUsed); - - curOp->debug().setPlanSummaryMetrics(summary); - - if (curOp->shouldDBProfile()) { - BSONObjBuilder execStatsBob; - Explain::getWinningPlanStats(exec.get(), &execStatsBob); - curOp->debug().execStats = execStatsBob.obj(); - } - - return true; - } - -private: - /** - * Given a collection and the geoNear command parameters, returns the field path over which - * the geoNear should operate. - * - * Throws an assertion with ErrorCodes::IndexNotFound if there is no single geo index - * which this geoNear command should use. - */ - StringData getFieldName(OperationContext* opCtx, Collection* collection, BSONObj cmdObj) { - if (auto keyElt = cmdObj[DocumentSourceGeoNear::kKeyFieldName]) { - uassert(ErrorCodes::TypeMismatch, - str::stream() << "geoNear parameter '" << DocumentSourceGeoNear::kKeyFieldName - << "' must be of type string but found type: " - << typeName(keyElt.type()), - keyElt.type() == BSONType::String); - auto fieldName = keyElt.valueStringData(); - uassert(ErrorCodes::BadValue, - str::stream() << "$geoNear parameter '" << DocumentSourceGeoNear::kKeyFieldName - << "' cannot be the empty string", - !fieldName.empty()); - return fieldName; - } - - vector<IndexDescriptor*> idxs; - - // First, try 2d. - collection->getIndexCatalog()->findIndexByType(opCtx, IndexNames::GEO_2D, idxs); - uassert(ErrorCodes::IndexNotFound, - "more than one 2d index, not sure which to run geoNear on", - idxs.size() <= 1u); - - if (1 == idxs.size()) { - BSONObj indexKp = idxs[0]->keyPattern(); - BSONObjIterator kpIt(indexKp); - while (kpIt.more()) { - BSONElement elt = kpIt.next(); - if (BSONType::String == elt.type() && IndexNames::GEO_2D == elt.valuestr()) { - return elt.fieldNameStringData(); - } - } - } - - // Next, 2dsphere. - idxs.clear(); - collection->getIndexCatalog()->findIndexByType(opCtx, IndexNames::GEO_2DSPHERE, idxs); - uassert(ErrorCodes::IndexNotFound, "no geo indices for geoNear", !idxs.empty()); - uassert(ErrorCodes::IndexNotFound, - "more than one 2dsphere index, not sure which to run geoNear on", - idxs.size() == 1u); - - // 1 == idx.size(). - BSONObj indexKp = idxs[0]->keyPattern(); - BSONObjIterator kpIt(indexKp); - while (kpIt.more()) { - BSONElement elt = kpIt.next(); - if (BSONType::String == elt.type() && IndexNames::GEO_2DSPHERE == elt.valuestr()) { - return elt.fieldNameStringData(); - } - } - - MONGO_UNREACHABLE; - } -} geo2dFindNearCmd; -} // namespace mongo diff --git a/src/mongo/db/pipeline/cluster_aggregation_planner.cpp b/src/mongo/db/pipeline/cluster_aggregation_planner.cpp index 53ea48dd68f..5148d56f888 100644 --- a/src/mongo/db/pipeline/cluster_aggregation_planner.cpp +++ b/src/mongo/db/pipeline/cluster_aggregation_planner.cpp @@ -100,10 +100,7 @@ void moveFinalUnwindFromShardsToMerger(Pipeline* shardPipe, Pipeline* mergePipe) * Documents. */ void limitFieldsSentFromShardsToMerger(Pipeline* shardPipe, Pipeline* mergePipe) { - auto depsMetadata = DocumentSourceMatch::isTextQuery(shardPipe->getInitialQuery()) - ? DepsTracker::MetadataAvailable::kTextScore - : DepsTracker::MetadataAvailable::kNoMetadata; - DepsTracker mergeDeps(mergePipe->getDependencies(depsMetadata)); + DepsTracker mergeDeps(mergePipe->getDependencies(DepsTracker::kAllMetadataAvailable)); if (mergeDeps.needWholeDocument) return; // the merge needs all fields, so nothing we can do. @@ -113,7 +110,7 @@ void limitFieldsSentFromShardsToMerger(Pipeline* shardPipe, Pipeline* mergePipe) // Remove metadata from dependencies since it automatically flows through projection and we // don't want to project it in to the document. - mergeDeps.setNeedTextScore(false); + mergeDeps.setNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE, false); // HEURISTIC: only apply optimization if none of the shard stages have an exhaustive list of // field dependencies. While this may not be 100% ideal in all cases, it is simple and @@ -125,7 +122,7 @@ void limitFieldsSentFromShardsToMerger(Pipeline* shardPipe, Pipeline* mergePipe) // 2) Optimization IS NOT applied immediately following a $project or $group since it would // add an unnecessary project (and therefore a deep-copy). for (auto&& source : shardPipe->getSources()) { - DepsTracker dt(depsMetadata); + DepsTracker dt(DepsTracker::kAllMetadataAvailable); if (source->getDependencies(&dt) & DocumentSource::EXHAUSTIVE_FIELDS) return; } diff --git a/src/mongo/db/pipeline/dependencies.cpp b/src/mongo/db/pipeline/dependencies.cpp index a1fc3b88a19..df24a8bb9ed 100644 --- a/src/mongo/db/pipeline/dependencies.cpp +++ b/src/mongo/db/pipeline/dependencies.cpp @@ -41,6 +41,8 @@ using std::vector; namespace str = mongoutils::str; +constexpr DepsTracker::MetadataAvailable DepsTracker::kAllGeoNearDataAvailable; + bool DepsTracker::_appendMetaProjections(BSONObjBuilder* projectionBuilder) const { if (_needTextScore) { projectionBuilder->append(Document::metaFieldTextScore, @@ -52,7 +54,17 @@ bool DepsTracker::_appendMetaProjections(BSONObjBuilder* projectionBuilder) cons BSON("$meta" << "sortKey")); } - return (_needTextScore || _needSortKey); + if (_needGeoNearDistance) { + projectionBuilder->append(Document::metaFieldGeoNearDistance, + BSON("$meta" + << "geoNearDistance")); + } + if (_needGeoNearPoint) { + projectionBuilder->append(Document::metaFieldGeoNearPoint, + BSON("$meta" + << "geoNearPoint")); + } + return (_needTextScore || _needSortKey || _needGeoNearDistance || _needGeoNearPoint); } BSONObj DepsTracker::toProjection() const { @@ -134,6 +146,81 @@ boost::optional<ParsedDeps> DepsTracker::toParsedDeps() const { return ParsedDeps(md.freeze()); } +bool DepsTracker::getNeedsMetadata(MetadataType type) const { + switch (type) { + case MetadataType::TEXT_SCORE: + return _needTextScore; + case MetadataType::SORT_KEY: + return _needSortKey; + case MetadataType::GEO_NEAR_DISTANCE: + return _needGeoNearDistance; + case MetadataType::GEO_NEAR_POINT: + return _needGeoNearPoint; + } + MONGO_UNREACHABLE; +} + +bool DepsTracker::isMetadataAvailable(MetadataType type) const { + switch (type) { + case MetadataType::TEXT_SCORE: + return _metadataAvailable & MetadataAvailable::kTextScore; + case MetadataType::SORT_KEY: + MONGO_UNREACHABLE; + case MetadataType::GEO_NEAR_DISTANCE: + return _metadataAvailable & MetadataAvailable::kGeoNearDistance; + case MetadataType::GEO_NEAR_POINT: + return _metadataAvailable & MetadataAvailable::kGeoNearPoint; + } + MONGO_UNREACHABLE; +} + +void DepsTracker::setNeedsMetadata(MetadataType type, bool required) { + switch (type) { + case MetadataType::TEXT_SCORE: + uassert(40218, + "pipeline requires text score metadata, but there is no text score available", + !required || isMetadataAvailable(type)); + _needTextScore = required; + return; + case MetadataType::SORT_KEY: + invariant(required || !_needSortKey); + _needSortKey = required; + return; + case MetadataType::GEO_NEAR_DISTANCE: + uassert(50860, + "pipeline requires $geoNear distance metadata, but it is not available", + !required || isMetadataAvailable(type)); + invariant(required || !_needGeoNearDistance); + _needGeoNearDistance = required; + return; + case MetadataType::GEO_NEAR_POINT: + uassert(50859, + "pipeline requires $geoNear point metadata, but it is not available", + !required || isMetadataAvailable(type)); + invariant(required || !_needGeoNearPoint); + _needGeoNearPoint = required; + return; + } + MONGO_UNREACHABLE; +} + +std::vector<DepsTracker::MetadataType> DepsTracker::getAllRequiredMetadataTypes() const { + std::vector<MetadataType> reqs; + if (_needTextScore) { + reqs.push_back(MetadataType::TEXT_SCORE); + } + if (_needSortKey) { + reqs.push_back(MetadataType::SORT_KEY); + } + if (_needGeoNearDistance) { + reqs.push_back(MetadataType::GEO_NEAR_DISTANCE); + } + if (_needGeoNearPoint) { + reqs.push_back(MetadataType::GEO_NEAR_POINT); + } + return reqs; +} + namespace { // Mutually recursive with arrayHelper Document documentHelper(const BSONObj& bson, const Document& neededFields, int nFieldsNeeded = -1); diff --git a/src/mongo/db/pipeline/dependencies.h b/src/mongo/db/pipeline/dependencies.h index 133fc1d9893..dec5296060f 100644 --- a/src/mongo/db/pipeline/dependencies.h +++ b/src/mongo/db/pipeline/dependencies.h @@ -43,9 +43,43 @@ class ParsedDeps; */ struct DepsTracker { /** + * Represents the type of metadata a pipeline might request. + */ + enum class MetadataType { + // The score associated with a text match. + TEXT_SCORE, + + // The key to use for sorting. + SORT_KEY, + + // The computed distance for a near query. + GEO_NEAR_DISTANCE, + + // The point used in the computation of the GEO_NEAR_DISTANCE. + GEO_NEAR_POINT, + }; + + /** * Represents what metadata is available on documents that are input to the pipeline. */ - enum MetadataAvailable { kNoMetadata = 0, kTextScore = 1 }; + enum MetadataAvailable { + kNoMetadata = 0, + kTextScore = 1 << 1, + kGeoNearDistance = 1 << 2, + kGeoNearPoint = 1 << 3, + }; + + /** + * Represents a state where all geo metadata is available. + */ + static constexpr auto kAllGeoNearDataAvailable = + MetadataAvailable(MetadataAvailable::kGeoNearDistance | MetadataAvailable::kGeoNearPoint); + + /** + * Represents a state where all metadata is available. + */ + static constexpr auto kAllMetadataAvailable = + MetadataAvailable(kTextScore | kGeoNearDistance | kGeoNearPoint); DepsTracker(MetadataAvailable metadataAvailable = kNoMetadata) : _metadataAvailable(metadataAvailable) {} @@ -71,36 +105,44 @@ struct DepsTracker { return !match.empty(); } + /** + * Returns a value with bits set indicating the types of metadata available. + */ MetadataAvailable getMetadataAvailable() const { return _metadataAvailable; } - bool isTextScoreAvailable() const { - return _metadataAvailable & MetadataAvailable::kTextScore; - } + /** + * Returns true if the DepsTracker the metadata 'type' is available to the pipeline. It is + * illegal to call this with MetadataType::SORT_KEY, since the sort key will always be available + * if needed. + */ + bool isMetadataAvailable(MetadataType type) const; - bool getNeedTextScore() const { - return _needTextScore; - } + /** + * Sets whether or not metadata 'type' is required. Throws if 'required' is true but that + * metadata is not available to the pipeline. + * + * Except for MetadataType::SORT_KEY, once 'type' is required, it cannot be unset. + */ + void setNeedsMetadata(MetadataType type, bool required); - void setNeedTextScore(bool needTextScore) { - if (needTextScore && !isTextScoreAvailable()) { - uasserted( - 40218, - "pipeline requires text score metadata, but there is no text score available"); - } - _needTextScore = needTextScore; - } + /** + * Returns true if the DepsTracker requires that metadata of type 'type' is present. + */ + bool getNeedsMetadata(MetadataType type) const; - bool getNeedSortKey() const { - return _needSortKey; + /** + * Returns true if there exists a type of metadata required by the DepsTracker. + */ + bool getNeedsAnyMetadata() const { + return _needTextScore || _needSortKey || _needGeoNearDistance || _needGeoNearPoint; } - void setNeedSortKey(bool needSortKey) { - // We don't expect to ever unset '_needSortKey'. - invariant(!_needSortKey || needSortKey); - _needSortKey = needSortKey; - } + /** + * Returns a vector containing all the types of metadata required by this DepsTracker. + */ + std::vector<MetadataType> getAllRequiredMetadataTypes() const; std::set<std::string> fields; // Names of needed fields in dotted notation. std::set<Variables::Id> vars; // IDs of referenced variables. @@ -114,8 +156,12 @@ private: bool _appendMetaProjections(BSONObjBuilder* bb) const; MetadataAvailable _metadataAvailable; - bool _needTextScore = false; // if true, add a {$meta: "textScore"} to the projection. - bool _needSortKey = false; // if true, add a {$meta: "sortKey"} to the projection. + + // Each member variable influences a different $meta projection. + bool _needTextScore = false; // {$meta: "textScore"} + bool _needSortKey = false; // {$meta: "sortKey"} + bool _needGeoNearDistance = false; // {$meta: "geoNearDistance"} + bool _needGeoNearPoint = false; // {$meta: "geoNearPoint"} }; /** diff --git a/src/mongo/db/pipeline/dependencies_test.cpp b/src/mongo/db/pipeline/dependencies_test.cpp index cb33e1fcc9f..733cc94dd60 100644 --- a/src/mongo/db/pipeline/dependencies_test.cpp +++ b/src/mongo/db/pipeline/dependencies_test.cpp @@ -116,7 +116,7 @@ TEST(DependenciesToProjectionTest, ShouldOnlyRequestTextScoreIfEntireDocumentAnd DepsTracker deps(DepsTracker::MetadataAvailable::kTextScore); deps.fields = arrayToSet(array); deps.needWholeDocument = true; - deps.setNeedTextScore(true); + deps.setNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE, true); ASSERT_BSONOBJ_EQ(deps.toProjection(), BSON(Document::metaFieldTextScore << metaTextScore)); } @@ -125,7 +125,7 @@ TEST(DependenciesToProjectionTest, const char* array[] = {"a"}; // needTextScore without needWholeDocument DepsTracker deps(DepsTracker::MetadataAvailable::kTextScore); deps.fields = arrayToSet(array); - deps.setNeedTextScore(true); + deps.setNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE, true); ASSERT_BSONOBJ_EQ( deps.toProjection(), BSON(Document::metaFieldTextScore << metaTextScore << "a" << 1 << "_id" << 0)); @@ -135,7 +135,7 @@ TEST(DependenciesToProjectionTest, ShouldProduceEmptyObjectIfThereAreNoDependenc DepsTracker deps(DepsTracker::MetadataAvailable::kTextScore); deps.fields = {}; deps.needWholeDocument = false; - deps.setNeedTextScore(false); + deps.setNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE, false); ASSERT_BSONOBJ_EQ(deps.toProjection(), BSONObj()); } @@ -143,7 +143,7 @@ TEST(DependenciesToProjectionTest, ShouldAttemptToExcludeOtherFieldsIfOnlyTextSc DepsTracker deps(DepsTracker::MetadataAvailable::kTextScore); deps.fields = {}; deps.needWholeDocument = false; - deps.setNeedTextScore(true); + deps.setNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE, true); ASSERT_BSONOBJ_EQ(deps.toProjection(), BSON(Document::metaFieldTextScore << metaTextScore << "_id" << 0 << "$noFieldsNeeded" @@ -155,7 +155,7 @@ TEST(DependenciesToProjectionTest, DepsTracker deps(DepsTracker::MetadataAvailable::kTextScore); deps.fields = {}; deps.needWholeDocument = true; - deps.setNeedTextScore(true); + deps.setNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE, true); ASSERT_BSONOBJ_EQ(deps.toProjection(), BSON(Document::metaFieldTextScore << metaTextScore)); } diff --git a/src/mongo/db/pipeline/document.cpp b/src/mongo/db/pipeline/document.cpp index 4f531fe72f3..f6f88b504fc 100644 --- a/src/mongo/db/pipeline/document.cpp +++ b/src/mongo/db/pipeline/document.cpp @@ -45,8 +45,11 @@ using std::vector; const DocumentStorage DocumentStorage::kEmptyDoc; -const std::vector<StringData> Document::allMetadataFieldNames = { - Document::metaFieldTextScore, Document::metaFieldRandVal, Document::metaFieldSortKey}; +const std::vector<StringData> Document::allMetadataFieldNames = {Document::metaFieldTextScore, + Document::metaFieldRandVal, + Document::metaFieldSortKey, + Document::metaFieldGeoNearDistance, + Document::metaFieldGeoNearPoint}; Position DocumentStorage::findField(StringData requested) const { int reqSize = requested.size(); // get size calculation out of the way if needed @@ -205,6 +208,8 @@ intrusive_ptr<DocumentStorage> DocumentStorage::clone() const { out->_textScore = _textScore; out->_randVal = _randVal; out->_sortKey = _sortKey.getOwned(); + out->_geoNearDistance = _geoNearDistance; + out->_geoNearPoint = _geoNearPoint.getOwned(); // Tell values that they have been memcpyed (updates ref counts) for (DocumentStorageIterator it = out->iteratorAll(); !it.atEnd(); it.advance()) { @@ -272,6 +277,8 @@ BSONObj Document::toBson() const { constexpr StringData Document::metaFieldTextScore; constexpr StringData Document::metaFieldRandVal; constexpr StringData Document::metaFieldSortKey; +constexpr StringData Document::metaFieldGeoNearDistance; +constexpr StringData Document::metaFieldGeoNearPoint; BSONObj Document::toBsonWithMetaData() const { BSONObjBuilder bb; @@ -282,6 +289,10 @@ BSONObj Document::toBsonWithMetaData() const { bb.append(metaFieldRandVal, getRandMetaField()); if (hasSortKeyMetaField()) bb.append(metaFieldSortKey, getSortKeyMetaField()); + if (hasGeoNearDistance()) + bb.append(metaFieldGeoNearDistance, getGeoNearDistance()); + if (hasGeoNearPoint()) + getGeoNearPoint().addToBsonObj(&bb, metaFieldGeoNearPoint); return bb.obj(); } @@ -302,6 +313,20 @@ Document Document::fromBsonWithMetaData(const BSONObj& bson) { } else if (fieldName == metaFieldSortKey) { md.setSortKeyMetaField(elem.Obj()); continue; + } else if (fieldName == metaFieldGeoNearDistance) { + md.setGeoNearDistance(elem.Double()); + continue; + } else if (fieldName == metaFieldGeoNearPoint) { + Value val; + if (elem.type() == BSONType::Array) { + val = Value(BSONArray(elem.embeddedObject())); + } else { + invariant(elem.type() == BSONType::Object); + val = Value(elem.embeddedObject()); + } + + md.setGeoNearPoint(val); + continue; } } diff --git a/src/mongo/db/pipeline/document.h b/src/mongo/db/pipeline/document.h index eb7438b776e..76d207276ce 100644 --- a/src/mongo/db/pipeline/document.h +++ b/src/mongo/db/pipeline/document.h @@ -93,6 +93,8 @@ public: static constexpr StringData metaFieldTextScore = "$textScore"_sd; static constexpr StringData metaFieldRandVal = "$randVal"_sd; static constexpr StringData metaFieldSortKey = "$sortKey"_sd; + static constexpr StringData metaFieldGeoNearDistance = "$dis"_sd; + static constexpr StringData metaFieldGeoNearPoint = "$pt"_sd; static const std::vector<StringData> allMetadataFieldNames; @@ -266,6 +268,20 @@ public: return storage().getSortKeyMetaField(); } + bool hasGeoNearDistance() const { + return storage().hasGeoNearDistance(); + } + double getGeoNearDistance() const { + return storage().getGeoNearDistance(); + } + + bool hasGeoNearPoint() const { + return storage().hasGeoNearPoint(); + } + Value getGeoNearPoint() const { + return storage().getGeoNearPoint(); + } + /// members for Sorter struct SorterDeserializeSettings {}; // unused void serializeForSorter(BufBuilder& buf) const; @@ -518,6 +534,14 @@ public: storage().setSortKeyMetaField(sortKey); } + void setGeoNearDistance(double dist) { + storage().setGeoNearDistance(dist); + } + + void setGeoNearPoint(Value point) { + storage().setGeoNearPoint(std::move(point)); + } + /** Convert to a read-only document and release reference. * * Call this to indicate that you are done with this Document and will diff --git a/src/mongo/db/pipeline/document_internal.h b/src/mongo/db/pipeline/document_internal.h index baa68e60658..e1f7c299fc6 100644 --- a/src/mongo/db/pipeline/document_internal.h +++ b/src/mongo/db/pipeline/document_internal.h @@ -189,7 +189,8 @@ public: _hashTabMask(0), _metaFields(), _textScore(0), - _randVal(0) {} + _randVal(0), + _geoNearDistance(0) {} ~DocumentStorage(); @@ -197,7 +198,10 @@ public: TEXT_SCORE, RAND_VAL, SORT_KEY, + GEONEAR_DIST, + GEONEAR_POINT, + // New fields must be added before the NUM_FIELDS sentinel. NUM_FIELDS }; @@ -284,6 +288,12 @@ public: if (source.hasSortKeyMetaField()) { setSortKeyMetaField(source.getSortKeyMetaField()); } + if (source.hasGeoNearDistance()) { + setGeoNearDistance(source.getGeoNearDistance()); + } + if (source.hasGeoNearPoint()) { + setGeoNearPoint(source.getGeoNearPoint()); + } } bool hasTextScore() const { @@ -319,6 +329,28 @@ public: _sortKey = sortKey.getOwned(); } + bool hasGeoNearDistance() const { + return _metaFields.test(MetaType::GEONEAR_DIST); + } + double getGeoNearDistance() const { + return _geoNearDistance; + } + void setGeoNearDistance(double dist) { + _metaFields.set(MetaType::GEONEAR_DIST); + _geoNearDistance = dist; + } + + bool hasGeoNearPoint() const { + return _metaFields.test(MetaType::GEONEAR_POINT); + } + Value getGeoNearPoint() const { + return _geoNearPoint; + } + void setGeoNearPoint(Value point) { + _metaFields.set(MetaType::GEONEAR_POINT); + _geoNearPoint = std::move(point); + } + private: /// Same as lastElement->next() or firstElement() if empty. const ValueElement* end() const { @@ -401,6 +433,8 @@ private: double _textScore; double _randVal; BSONObj _sortKey; + double _geoNearDistance; + Value _geoNearPoint; // When adding a field, make sure to update clone() method // Defined in document.cpp diff --git a/src/mongo/db/pipeline/document_source.h b/src/mongo/db/pipeline/document_source.h index de383b154e2..6e706876aff 100644 --- a/src/mongo/db/pipeline/document_source.h +++ b/src/mongo/db/pipeline/document_source.h @@ -648,7 +648,7 @@ public: /** * Get the dependencies this operation needs to do its job. If overridden, subclasses must add * all paths needed to apply their transformation to 'deps->fields', and call - * 'deps->setNeedTextScore()' if the text score is required. + * 'deps->setNeedsMetadata()' to indicate what metadata (e.g. text score), if any, is required. * * See GetDepsReturn above for the possible return values and what they mean. */ diff --git a/src/mongo/db/pipeline/document_source_add_fields_test.cpp b/src/mongo/db/pipeline/document_source_add_fields_test.cpp index fa9b27f5d61..8fa400e731a 100644 --- a/src/mongo/db/pipeline/document_source_add_fields_test.cpp +++ b/src/mongo/db/pipeline/document_source_add_fields_test.cpp @@ -133,7 +133,7 @@ TEST_F(AddFieldsTest, ShouldAddReferencedFieldsToDependencies) { ASSERT_EQUALS(1U, dependencies.fields.count("c")); ASSERT_EQUALS(1U, dependencies.fields.count("d")); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(true, dependencies.getNeedTextScore()); + ASSERT_EQUALS(true, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(AddFieldsTest, ShouldPropagatePauses) { diff --git a/src/mongo/db/pipeline/document_source_bucket_auto_test.cpp b/src/mongo/db/pipeline/document_source_bucket_auto_test.cpp index df19b4eb497..157c2aad4f1 100644 --- a/src/mongo/db/pipeline/document_source_bucket_auto_test.cpp +++ b/src/mongo/db/pipeline/document_source_bucket_auto_test.cpp @@ -447,7 +447,7 @@ TEST_F(BucketAutoTests, ShouldAddDependenciesOfGroupByFieldAndComputedFields) { ASSERT_EQUALS(1U, dependencies.fields.count("b")); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(BucketAutoTests, ShouldNeedTextScoreInDependenciesFromGroupByField) { @@ -459,7 +459,7 @@ TEST_F(BucketAutoTests, ShouldNeedTextScoreInDependenciesFromGroupByField) { ASSERT_EQUALS(0U, dependencies.fields.size()); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(true, dependencies.getNeedTextScore()); + ASSERT_EQUALS(true, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(BucketAutoTests, ShouldNeedTextScoreInDependenciesFromOutputField) { @@ -475,7 +475,7 @@ TEST_F(BucketAutoTests, ShouldNeedTextScoreInDependenciesFromOutputField) { ASSERT_EQUALS(1U, dependencies.fields.count("x")); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(true, dependencies.getNeedTextScore()); + ASSERT_EQUALS(true, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(BucketAutoTests, SerializesDefaultAccumulatorIfOutputFieldIsNotSpecified) { diff --git a/src/mongo/db/pipeline/document_source_cursor.cpp b/src/mongo/db/pipeline/document_source_cursor.cpp index 10fe39a1484..aeb16094abb 100644 --- a/src/mongo/db/pipeline/document_source_cursor.cpp +++ b/src/mongo/db/pipeline/document_source_cursor.cpp @@ -63,6 +63,10 @@ DocumentSource::GetNextResult DocumentSourceCursor::getNext() { return std::move(out); } +Document DocumentSourceCursor::transformBSONObjToDocument(const BSONObj& obj) const { + return _dependencies ? _dependencies->extractFields(obj) : Document::fromBsonWithMetaData(obj); +} + void DocumentSourceCursor::loadBatch() { if (!_exec || _exec->isDisposed()) { // No more documents. @@ -85,10 +89,8 @@ void DocumentSourceCursor::loadBatch() { while ((state = _exec->getNext(&resultObj, nullptr)) == PlanExecutor::ADVANCED) { if (_shouldProduceEmptyDocs) { _currentBatch.push_back(Document()); - } else if (_dependencies) { - _currentBatch.push_back(_dependencies->extractFields(resultObj)); } else { - _currentBatch.push_back(Document::fromBsonWithMetaData(resultObj)); + _currentBatch.push_back(transformBSONObjToDocument(resultObj)); } if (_limit) { @@ -302,6 +304,8 @@ DocumentSourceCursor::DocumentSourceCursor( _docsAddedToBatches(0), _exec(std::move(exec)), _outputSorts(_exec->getOutputSorts()) { + // Later code in the DocumentSourceCursor lifecycle expects that '_exec' is in a saved state. + _exec->saveState(); _planSummary = Explain::getPlanSummary(_exec.get()); recordPlanSummaryStats(); diff --git a/src/mongo/db/pipeline/document_source_cursor.h b/src/mongo/db/pipeline/document_source_cursor.h index fc9b37f9403..fa10f450d4e 100644 --- a/src/mongo/db/pipeline/document_source_cursor.h +++ b/src/mongo/db/pipeline/document_source_cursor.h @@ -43,14 +43,17 @@ namespace mongo { /** * Constructs and returns Documents from the BSONObj objects produced by a supplied PlanExecutor. */ -class DocumentSourceCursor final : public DocumentSource { +class DocumentSourceCursor : public DocumentSource { public: // virtuals from DocumentSource GetNextResult getNext() final; - const char* getSourceName() const final; - BSONObjSet getOutputSorts() final { + + const char* getSourceName() const override; + + BSONObjSet getOutputSorts() override { return _outputSorts; } + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; StageConstraints constraints(Pipeline::SplitState pipeState) const final { @@ -150,6 +153,12 @@ public: } protected: + DocumentSourceCursor(Collection* collection, + std::unique_ptr<PlanExecutor, PlanExecutor::Deleter> exec, + const boost::intrusive_ptr<ExpressionContext>& pExpCtx); + + ~DocumentSourceCursor(); + /** * Disposes of '_exec' if it hasn't been disposed already. This involves taking a collection * lock. @@ -162,12 +171,15 @@ protected: Pipeline::SourceContainer::iterator doOptimizeAt(Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) final; -private: - DocumentSourceCursor(Collection* collection, - std::unique_ptr<PlanExecutor, PlanExecutor::Deleter> exec, - const boost::intrusive_ptr<ExpressionContext>& pExpCtx); - ~DocumentSourceCursor(); + /** + * If '_shouldProduceEmptyDocs' is false, this function hook is called on each 'obj' returned by + * '_exec' when loading a batch and returns a Document to be added to '_currentBatch'. + * + * The default implementation is a dependency-aware BSONObj-to-Document transformation. + */ + virtual Document transformBSONObjToDocument(const BSONObj& obj) const; +private: /** * Acquires the appropriate locks, then destroys and de-registers '_exec'. '_exec' must be * non-null. @@ -180,12 +192,14 @@ private: void cleanupExecutor(const AutoGetCollectionForRead& readLock); /** - * Reads a batch of data from '_exec'. + * Reads a batch of data from '_exec'. Subclasses can specify custom behavior to be performed on + * each document by overloading transformBSONObjToDocument(). */ void loadBatch(); void recordPlanSummaryStats(); + // Batches results returned from the underlying PlanExecutor. std::deque<Document> _currentBatch; // BSONObj members must outlive _projection and cursor. diff --git a/src/mongo/db/pipeline/document_source_facet.cpp b/src/mongo/db/pipeline/document_source_facet.cpp index bc49ab6371f..8b1ce60f6e1 100644 --- a/src/mongo/db/pipeline/document_source_facet.cpp +++ b/src/mongo/db/pipeline/document_source_facet.cpp @@ -276,11 +276,17 @@ DocumentSource::GetDepsReturn DocumentSourceFacet::getDependencies(DepsTracker* deps->vars.insert(subDepsTracker.vars.begin(), subDepsTracker.vars.end()); deps->needWholeDocument = deps->needWholeDocument || subDepsTracker.needWholeDocument; - deps->setNeedTextScore(deps->getNeedTextScore() || subDepsTracker.getNeedTextScore()); + + // The text score is the only type of metadata that could be needed by $facet. + deps->setNeedsMetadata( + DepsTracker::MetadataType::TEXT_SCORE, + deps->getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE) || + subDepsTracker.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); // If there are variables defined at this stage's scope, there may be dependencies upon // them in subsequent pipelines. Keep enumerating. - if (deps->needWholeDocument && deps->getNeedTextScore() && !scopeHasVariables) { + if (deps->needWholeDocument && + deps->getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE) && !scopeHasVariables) { break; } } diff --git a/src/mongo/db/pipeline/document_source_facet_test.cpp b/src/mongo/db/pipeline/document_source_facet_test.cpp index 25f8cdc7a07..bc88b491229 100644 --- a/src/mongo/db/pipeline/document_source_facet_test.cpp +++ b/src/mongo/db/pipeline/document_source_facet_test.cpp @@ -564,7 +564,7 @@ TEST_F(DocumentSourceFacetTest, ShouldUnionDependenciesOfInnerPipelines) { DepsTracker deps(DepsTracker::MetadataAvailable::kNoMetadata); ASSERT_EQ(facetStage->getDependencies(&deps), DocumentSource::GetDepsReturn::EXHAUSTIVE_ALL); ASSERT_FALSE(deps.needWholeDocument); - ASSERT_FALSE(deps.getNeedTextScore()); + ASSERT_FALSE(deps.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); ASSERT_EQ(deps.fields.size(), 2UL); ASSERT_EQ(deps.fields.count("a"), 1UL); ASSERT_EQ(deps.fields.count("b"), 1UL); @@ -602,7 +602,7 @@ TEST_F(DocumentSourceFacetTest, ShouldRequireWholeDocumentIfAnyPipelineRequiresW DepsTracker deps(DepsTracker::MetadataAvailable::kNoMetadata); ASSERT_EQ(facetStage->getDependencies(&deps), DocumentSource::GetDepsReturn::EXHAUSTIVE_ALL); ASSERT_TRUE(deps.needWholeDocument); - ASSERT_FALSE(deps.getNeedTextScore()); + ASSERT_FALSE(deps.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } /** @@ -611,7 +611,7 @@ TEST_F(DocumentSourceFacetTest, ShouldRequireWholeDocumentIfAnyPipelineRequiresW class DocumentSourceNeedsOnlyTextScore : public DocumentSourcePassthrough { public: GetDepsReturn getDependencies(DepsTracker* deps) const override { - deps->setNeedTextScore(true); + deps->setNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE, true); return GetDepsReturn::EXHAUSTIVE_ALL; } static boost::intrusive_ptr<DocumentSourceNeedsOnlyTextScore> create() { @@ -641,7 +641,7 @@ TEST_F(DocumentSourceFacetTest, ShouldRequireTextScoreIfAnyPipelineRequiresTextS DepsTracker deps(DepsTracker::MetadataAvailable::kTextScore); ASSERT_EQ(facetStage->getDependencies(&deps), DocumentSource::GetDepsReturn::EXHAUSTIVE_ALL); ASSERT_TRUE(deps.needWholeDocument); - ASSERT_TRUE(deps.getNeedTextScore()); + ASSERT_TRUE(deps.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceFacetTest, ShouldThrowIfAnyPipelineRequiresTextScoreButItIsNotAvailable) { diff --git a/src/mongo/db/pipeline/document_source_geo_near.cpp b/src/mongo/db/pipeline/document_source_geo_near.cpp index 39a31253573..7ba5e2a4f8e 100644 --- a/src/mongo/db/pipeline/document_source_geo_near.cpp +++ b/src/mongo/db/pipeline/document_source_geo_near.cpp @@ -33,84 +33,26 @@ #include "mongo/db/pipeline/document_source_geo_near.h" #include "mongo/db/pipeline/document.h" -#include "mongo/db/pipeline/document_source_limit.h" #include "mongo/db/pipeline/document_source_sort.h" #include "mongo/db/pipeline/lite_parsed_document_source.h" -#include "mongo/rpc/get_status_from_command_result.h" #include "mongo/util/log.h" namespace mongo { using boost::intrusive_ptr; +constexpr StringData DocumentSourceGeoNear::kKeyFieldName; +constexpr const char* DocumentSourceGeoNear::kStageName; + REGISTER_DOCUMENT_SOURCE(geoNear, LiteParsedDocumentSourceDefault::parse, DocumentSourceGeoNear::createFromBson); -const long long DocumentSourceGeoNear::kDefaultLimit = 100; - -constexpr StringData DocumentSourceGeoNear::kKeyFieldName; - -const char* DocumentSourceGeoNear::getSourceName() const { - return "$geoNear"; -} - -DocumentSource::GetNextResult DocumentSourceGeoNear::getNext() { - pExpCtx->checkForInterrupt(); - - if (!resultsIterator) - runCommand(); - - if (!resultsIterator->more()) - return GetNextResult::makeEOF(); - - // Each result from the geoNear command is wrapped in a wrapper object with "obj", - // "dis" and maybe "loc" fields. We want to take the object from "obj" and inject the - // other fields into it. - Document result(resultsIterator->next().embeddedObject()); - MutableDocument output(result["obj"].getDocument()); - output.setNestedField(*distanceField, result["dis"]); - if (includeLocs) - output.setNestedField(*includeLocs, result["loc"]); - - // In a cluster, $geoNear output will be merged via $sort, so add the sort key. - if (pExpCtx->needsMerge) { - output.setSortKeyMetaField(BSON("" << result["dis"])); - } - - return output.freeze(); -} - -Pipeline::SourceContainer::iterator DocumentSourceGeoNear::doOptimizeAt( - Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { - invariant(*itr == this); - - auto nextLimit = dynamic_cast<DocumentSourceLimit*>((*std::next(itr)).get()); - - if (nextLimit) { - // If the next stage is a $limit, we can combine it with ourselves. - limit = std::min(limit, nextLimit->getLimit()); - container->erase(std::next(itr)); - return itr; - } - return std::next(itr); -} - -// This command is sent as-is to the shards. -intrusive_ptr<DocumentSource> DocumentSourceGeoNear::getShardSource() { - return this; -} -// On mongoS this becomes a merge sort by distance (nearest-first) with limit. -std::list<intrusive_ptr<DocumentSource>> DocumentSourceGeoNear::getMergeSources() { - return {DocumentSourceSort::create( - pExpCtx, BSON(distanceField->fullPath() << 1 << "$mergePresorted" << true), limit)}; -} - Value DocumentSourceGeoNear::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { MutableDocument result; - if (!keyFieldPath.empty()) { - result.setField(kKeyFieldName, Value(keyFieldPath)); + if (keyFieldPath) { + result.setField(kKeyFieldName, Value(keyFieldPath->fullPath())); } if (coordsIsArray) { @@ -119,20 +61,21 @@ Value DocumentSourceGeoNear::serialize(boost::optional<ExplainOptions::Verbosity result.setField("near", Value(coords)); } - // not in buildGeoNearCmd result.setField("distanceField", Value(distanceField->fullPath())); - result.setField("limit", Value(limit)); - - if (maxDistance > 0) - result.setField("maxDistance", Value(maxDistance)); + if (maxDistance) { + result.setField("maxDistance", Value(*maxDistance)); + } - if (minDistance > 0) - result.setField("minDistance", Value(minDistance)); + if (minDistance) { + result.setField("minDistance", Value(*minDistance)); + } result.setField("query", Value(query)); result.setField("spherical", Value(spherical)); - result.setField("distanceMultiplier", Value(distanceMultiplier)); + if (distanceMultiplier) { + result.setField("distanceMultiplier", Value(*distanceMultiplier)); + } if (includeLocs) result.setField("includeLocs", Value(includeLocs->fullPath())); @@ -140,60 +83,6 @@ Value DocumentSourceGeoNear::serialize(boost::optional<ExplainOptions::Verbosity return Value(DOC(getSourceName() << result.freeze())); } -BSONObj DocumentSourceGeoNear::buildGeoNearCmd() const { - // this is very similar to sourceToBson, but slightly different. - // differences will be noted. - - BSONObjBuilder geoNear; // not building a subField - - geoNear.append("geoNear", pExpCtx->ns.coll()); // not in toBson - - if (coordsIsArray) { - geoNear.appendArray("near", coords); - } else { - geoNear.append("near", coords); - } - - geoNear.append("num", limit); // called limit in toBson - - if (maxDistance > 0) - geoNear.append("maxDistance", maxDistance); - - if (minDistance > 0) - geoNear.append("minDistance", minDistance); - - geoNear.append("query", query); - if (pExpCtx->getCollator()) { - geoNear.append("collation", pExpCtx->getCollator()->getSpec().toBSON()); - } else { - geoNear.append("collation", CollationSpec::kSimpleSpec); - } - - geoNear.append("spherical", spherical); - geoNear.append("distanceMultiplier", distanceMultiplier); - - if (includeLocs) - geoNear.append("includeLocs", true); // String in toBson - - if (!keyFieldPath.empty()) { - geoNear.append(kKeyFieldName, keyFieldPath); - } - - return geoNear.obj(); -} - -void DocumentSourceGeoNear::runCommand() { - massert(16603, "Already ran geoNearCommand", !resultsIterator); - - bool ok = pExpCtx->mongoProcessInterface->directClient()->runCommand( - pExpCtx->ns.db().toString(), buildGeoNearCmd(), cmdOutput); - if (!ok) { - uassertStatusOK(getStatusFromCommandResult(cmdOutput)); - } - - resultsIterator.reset(new BSONObjIterator(cmdOutput["results"].embeddedObject())); -} - intrusive_ptr<DocumentSourceGeoNear> DocumentSourceGeoNear::create( const intrusive_ptr<ExpressionContext>& pCtx) { intrusive_ptr<DocumentSourceGeoNear> source(new DocumentSourceGeoNear(pCtx)); @@ -208,8 +97,25 @@ intrusive_ptr<DocumentSource> DocumentSourceGeoNear::createFromBson( } void DocumentSourceGeoNear::parseOptions(BSONObj options) { - // near and distanceField are required + // First, check for explicitly-disallowed fields. + // The old geoNear command used to accept a collation. We explicitly ban it here, since the + // $geoNear stage should respect the collation associated with the entire pipeline. + uassert(40227, + "$geoNear does not accept the 'collation' parameter. Instead, specify a collation " + "for the entire aggregation command.", + !options["collation"]); + + // The following fields were present in older versions but are no longer supported. + uassert(50858, + "$geoNear no longer supports the 'limit' parameter. Use a $limit stage instead.", + !options["limit"]); + uassert(50857, + "$geoNear no longer supports the 'num' parameter. Use a $limit stage instead.", + !options["num"]); + uassert(50856, "$geoNear no longer supports the 'start' argument.", !options["start"]); + + // The "near" and "distanceField" parameters are required. uassert(16605, "$geoNear requires a 'near' option as an Array", options["near"].isABSONObj()); // Array or Object (Object is deprecated) @@ -221,33 +127,47 @@ void DocumentSourceGeoNear::parseOptions(BSONObj options) { options["distanceField"].type() == String); distanceField.reset(new FieldPath(options["distanceField"].str())); - // remaining fields are optional - - // num and limit are synonyms - if (options["limit"].isNumber()) - limit = options["limit"].numberLong(); - if (options["num"].isNumber()) - limit = options["num"].numberLong(); - - if (options["maxDistance"].isNumber()) + // The remaining fields are optional. + if (auto maxDistElem = options["maxDistance"]) { + uassert(ErrorCodes::TypeMismatch, + "maxDistance must be a number", + isNumericBSONType(maxDistElem.type())); maxDistance = options["maxDistance"].numberDouble(); + uassert(ErrorCodes::BadValue, "maxDistance must be nonnegative", *maxDistance >= 0); + } - if (options["minDistance"].isNumber()) + if (auto minDistElem = options["minDistance"]) { + uassert(ErrorCodes::TypeMismatch, + "minDistance must be a number", + isNumericBSONType(minDistElem.type())); minDistance = options["minDistance"].numberDouble(); + uassert(ErrorCodes::BadValue, "minDistance must be nonnegative", *minDistance >= 0); + } - if (options["query"].type() == Object) - query = options["query"].embeddedObject().getOwned(); + if (auto distMultElem = options["distanceMultiplier"]) { + uassert(ErrorCodes::TypeMismatch, + "distanceMultiplier must be a number", + isNumericBSONType(distMultElem.type())); + distanceMultiplier = options["distanceMultiplier"].numberDouble(); + uassert(ErrorCodes::BadValue, + "distanceMultiplier must be nonnegative", + *distanceMultiplier >= 0); + } - spherical = options["spherical"].trueValue(); + if (auto queryElem = options["query"]) { + uassert(ErrorCodes::TypeMismatch, + "query must be an object", + queryElem.type() == BSONType::Object); + query = queryElem.embeddedObject().getOwned(); + } - if (options["distanceMultiplier"].isNumber()) - distanceMultiplier = options["distanceMultiplier"].numberDouble(); + spherical = options["spherical"].trueValue(); if (options.hasField("includeLocs")) { uassert(16607, "$geoNear requires that 'includeLocs' option is a String", options["includeLocs"].type() == String); - includeLocs.reset(new FieldPath(options["includeLocs"].str())); + includeLocs = FieldPath(options["includeLocs"].str()); } if (options.hasField("uniqueDocs")) @@ -259,27 +179,64 @@ void DocumentSourceGeoNear::parseOptions(BSONObj options) { << "' must be of type string but found type: " << typeName(keyElt.type()), keyElt.type() == BSONType::String); - keyFieldPath = keyElt.str(); + const auto keyFieldStr = keyElt.valueStringData(); uassert(ErrorCodes::BadValue, str::stream() << "$geoNear parameter '" << DocumentSourceGeoNear::kKeyFieldName << "' cannot be the empty string", - !keyFieldPath.empty()); + !keyFieldStr.empty()); + keyFieldPath = FieldPath(keyFieldStr); } +} - // The collation field is disallowed, even though it is accepted by the geoNear command, since - // the $geoNear operation should respect the collation associated with the entire pipeline. - uassert(40227, - "$geoNear does not accept the 'collation' parameter. Instead, specify a collation " - "for the entire aggregation command.", - !options["collation"]); +BSONObj DocumentSourceGeoNear::asNearQuery(StringData nearFieldName) const { + BSONObjBuilder queryBuilder; + queryBuilder.appendElements(query); + + BSONObjBuilder nearBuilder(queryBuilder.subobjStart(nearFieldName)); + if (spherical) { + if (coordsIsArray) { + nearBuilder.appendArray("$nearSphere", coords); + } else { + nearBuilder.append("$nearSphere", coords); + } + } else { + if (coordsIsArray) { + nearBuilder.appendArray("$near", coords); + } else { + nearBuilder.append("$near", coords); + } + } + if (minDistance) { + nearBuilder.append("$minDistance", *minDistance); + } + if (maxDistance) { + nearBuilder.append("$maxDistance", *maxDistance); + } + nearBuilder.doneFast(); + return queryBuilder.obj(); +} + +bool DocumentSourceGeoNear::needsGeoNearPoint() const { + return static_cast<bool>(includeLocs); +} + +DocumentSource::GetDepsReturn DocumentSourceGeoNear::getDependencies(DepsTracker* deps) const { + // TODO (SERVER-35424): Implement better dependency tracking. For example, 'distanceField' is + // produced by this stage, and we could inform the query system that it need not include it in + // its response. For now, assume that we require the entire document as well as the appropriate + // geoNear metadata. + deps->setNeedsMetadata(DepsTracker::MetadataType::GEO_NEAR_DISTANCE, true); + deps->setNeedsMetadata(DepsTracker::MetadataType::GEO_NEAR_POINT, needsGeoNearPoint()); + + deps->needWholeDocument = true; + return GetDepsReturn::EXHAUSTIVE_FIELDS; } DocumentSourceGeoNear::DocumentSourceGeoNear(const intrusive_ptr<ExpressionContext>& pExpCtx) - : DocumentSource(pExpCtx), - coordsIsArray(false), - limit(DocumentSourceGeoNear::kDefaultLimit), - maxDistance(-1.0), - minDistance(-1.0), - spherical(false), - distanceMultiplier(1.0) {} + : DocumentSource(pExpCtx), coordsIsArray(false), spherical(false) {} + +std::list<boost::intrusive_ptr<DocumentSource>> DocumentSourceGeoNear::getMergeSources() { + return {DocumentSourceSort::create( + pExpCtx, BSON(distanceField->fullPath() << 1 << "$mergePresorted" << true))}; } +} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_geo_near.h b/src/mongo/db/pipeline/document_source_geo_near.h index fa67ab1c71a..7d77fa5a422 100644 --- a/src/mongo/db/pipeline/document_source_geo_near.h +++ b/src/mongo/db/pipeline/document_source_geo_near.h @@ -35,86 +35,120 @@ namespace mongo { class DocumentSourceGeoNear : public DocumentSource, public NeedsMergerDocumentSource { public: - static const long long kDefaultLimit; - static constexpr StringData kKeyFieldName = "key"_sd; + static constexpr auto kStageName = "$geoNear"; - // virtuals from DocumentSource - GetNextResult getNext() final; - const char* getSourceName() const final; /** - * Attempts to combine with a subsequent limit stage, setting the internal limit field - * as a result. + * Only exposed for testing. */ - Pipeline::SourceContainer::iterator doOptimizeAt(Pipeline::SourceContainer::iterator itr, - Pipeline::SourceContainer* container) final; + static boost::intrusive_ptr<DocumentSourceGeoNear> create( + const boost::intrusive_ptr<ExpressionContext>&); + + const char* getSourceName() const final { + return kStageName; + } StageConstraints constraints(Pipeline::SplitState pipeState) const final { - StageConstraints constraints(StreamType::kStreaming, - PositionRequirement::kFirst, - HostTypeRequirement::kAnyShard, - DiskUseRequirement::kNoDiskUse, - FacetRequirement::kNotAllowed, - TransactionRequirement::kAllowed); - - constraints.requiresInputDocSource = false; - return constraints; + return {StreamType::kStreaming, + PositionRequirement::kFirst, + HostTypeRequirement::kAnyShard, + DiskUseRequirement::kNoDiskUse, + FacetRequirement::kNotAllowed, + TransactionRequirement::kAllowed}; } - Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; - BSONObjSet getOutputSorts() final { - return SimpleBSONObjComparator::kInstance.makeBSONObjSet( - {BSON(distanceField->fullPath() << -1)}); + /** + * DocumentSourceGeoNear should always be replaced by a DocumentSourceGeoNearCursor before + * executing a pipeline, so this method should never be called. + */ + GetNextResult getNext() final { + MONGO_UNREACHABLE; } - // Virtuals for NeedsMergerDocumentSource - boost::intrusive_ptr<DocumentSource> getShardSource() final; - std::list<boost::intrusive_ptr<DocumentSource>> getMergeSources() final; + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; static boost::intrusive_ptr<DocumentSource> createFromBson( BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& pCtx); - static char geoNearName[]; - - long long getLimit() { - return limit; - } - + /** + * A query predicate to apply to the documents in addition to the "near" predicate. + */ BSONObj getQuery() const { return query; }; - // this should only be used for testing - static boost::intrusive_ptr<DocumentSourceGeoNear> create( - const boost::intrusive_ptr<ExpressionContext>& pCtx); + /** + * The field in which the computed distance will be stored. + */ + FieldPath getDistanceField() const { + return *distanceField; + } + + /** + * The field in which the matching point will be stored, if requested. + */ + boost::optional<FieldPath> getLocationField() const { + return includeLocs; + } + + /** + * The field over which to apply the "near" predicate, if specified. + */ + boost::optional<FieldPath> getKeyField() const { + return keyFieldPath; + } + + /** + * A scaling factor to apply to the distance, if specified by the user. + */ + boost::optional<double> getDistanceMultiplier() const { + return distanceMultiplier; + } + + GetDepsReturn getDependencies(DepsTracker* deps) const final; + + /** + * Returns true if the $geoNear specification requires the geoNear point metadata. + */ + bool needsGeoNearPoint() const; + + /** + * Converts this $geoNear aggregation stage into an equivalent $near or $nearSphere query on + * 'nearFieldName'. + */ + BSONObj asNearQuery(StringData nearFieldName) const; + + /** + * This document source is sent as-is to the shards. + */ + boost::intrusive_ptr<DocumentSource> getShardSource() final { + return this; + } + + /** + * In a sharded cluster, this becomes a merge sort by distance, from nearest to furthest. + */ + std::list<boost::intrusive_ptr<DocumentSource>> getMergeSources() final; private: explicit DocumentSourceGeoNear(const boost::intrusive_ptr<ExpressionContext>& pExpCtx); + /** + * Parses the fields in the object 'options', throwing if an error occurs. + */ void parseOptions(BSONObj options); - BSONObj buildGeoNearCmd() const; - void runCommand(); // These fields describe the command to run. - // coords and distanceField are required, rest are optional + // 'coords' and 'distanceField' are required; the rest are optional. BSONObj coords; // "near" option, but near is a reserved keyword on windows bool coordsIsArray; std::unique_ptr<FieldPath> distanceField; // Using unique_ptr because FieldPath can't be empty - long long limit; - double maxDistance; - double minDistance; BSONObj query; bool spherical; - double distanceMultiplier; - std::unique_ptr<FieldPath> includeLocs; - - // The field path over which the command should run, extracted from the 'key' parameter passed - // by the user. Or the empty string the user did not provide a 'key'. - std::string keyFieldPath; - - // these fields are used while processing the results - BSONObj cmdOutput; - std::unique_ptr<BSONObjIterator> resultsIterator; // iterator over cmdOutput["results"] + boost::optional<double> maxDistance; + boost::optional<double> minDistance; + boost::optional<double> distanceMultiplier; + boost::optional<FieldPath> includeLocs; + boost::optional<FieldPath> keyFieldPath; }; - } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_geo_near_cursor.cpp b/src/mongo/db/pipeline/document_source_geo_near_cursor.cpp new file mode 100644 index 00000000000..4b1a9596fd9 --- /dev/null +++ b/src/mongo/db/pipeline/document_source_geo_near_cursor.cpp @@ -0,0 +1,118 @@ +/** + * Copyright (C) 2018 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/db/pipeline/document_source_geo_near_cursor.h" + +#include <boost/intrusive_ptr.hpp> +#include <boost/optional.hpp> +#include <list> +#include <memory> + +#include "mongo/base/string_data.h" +#include "mongo/bson/bsonelement.h" +#include "mongo/bson/bsonobj.h" +#include "mongo/bson/simple_bsonobj_comparator.h" +#include "mongo/db/catalog/collection.h" +#include "mongo/db/pipeline/document.h" +#include "mongo/db/pipeline/document_source_cursor.h" +#include "mongo/db/pipeline/document_source_sort.h" +#include "mongo/db/pipeline/expression_context.h" +#include "mongo/db/pipeline/field_path.h" +#include "mongo/db/query/plan_executor.h" + +namespace mongo { +constexpr const char* DocumentSourceGeoNearCursor::kStageName; + +boost::intrusive_ptr<DocumentSourceGeoNearCursor> DocumentSourceGeoNearCursor::create( + Collection* collection, + std::unique_ptr<PlanExecutor, PlanExecutor::Deleter> exec, + const boost::intrusive_ptr<ExpressionContext>& expCtx, + FieldPath distanceField, + boost::optional<FieldPath> locationField, + double distanceMultiplier) { + return {new DocumentSourceGeoNearCursor(collection, + std::move(exec), + expCtx, + std::move(distanceField), + std::move(locationField), + distanceMultiplier)}; +} + +DocumentSourceGeoNearCursor::DocumentSourceGeoNearCursor( + Collection* collection, + std::unique_ptr<PlanExecutor, PlanExecutor::Deleter> exec, + const boost::intrusive_ptr<ExpressionContext>& expCtx, + FieldPath distanceField, + boost::optional<FieldPath> locationField, + double distanceMultiplier) + : DocumentSourceCursor(collection, std::move(exec), expCtx), + _distanceField(std::move(distanceField)), + _locationField(std::move(locationField)), + _distanceMultiplier(distanceMultiplier) { + invariant(_distanceMultiplier >= 0); +} + +const char* DocumentSourceGeoNearCursor::getSourceName() const { + return kStageName; +} + +BSONObjSet DocumentSourceGeoNearCursor::getOutputSorts() { + return SimpleBSONObjComparator::kInstance.makeBSONObjSet( + {BSON(_distanceField.fullPath() << 1)}); +} + +Document DocumentSourceGeoNearCursor::transformBSONObjToDocument(const BSONObj& obj) const { + MutableDocument output(Document::fromBsonWithMetaData(obj)); + + // Scale the distance by the requested factor. + invariant(output.peek().hasGeoNearDistance(), + str::stream() + << "Query returned a document that is unexpectedly missing the geoNear distance: " + << obj.jsonString()); + const auto distance = output.peek().getGeoNearDistance() * _distanceMultiplier; + + output.setNestedField(_distanceField, Value(distance)); + if (_locationField) { + invariant( + output.peek().hasGeoNearPoint(), + str::stream() + << "Query returned a document that is unexpectedly missing the geoNear point: " + << obj.jsonString()); + output.setNestedField(*_locationField, output.peek().getGeoNearPoint()); + } + + // In a cluster, $geoNear will be merged via $sort, so add the sort key. + if (pExpCtx->needsMerge) { + output.setSortKeyMetaField(BSON("" << distance)); + } + + return output.freeze(); +} +} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_geo_near_cursor.h b/src/mongo/db/pipeline/document_source_geo_near_cursor.h new file mode 100644 index 00000000000..1e95ba5daa6 --- /dev/null +++ b/src/mongo/db/pipeline/document_source_geo_near_cursor.h @@ -0,0 +1,102 @@ +/** + * Copyright (C) 2018 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#pragma once + +#include <boost/intrusive_ptr.hpp> +#include <boost/optional.hpp> +#include <list> +#include <memory> + +#include "mongo/base/string_data.h" +#include "mongo/bson/bsonobj.h" +#include "mongo/db/catalog/collection.h" +#include "mongo/db/pipeline/document.h" +#include "mongo/db/pipeline/document_source_cursor.h" +#include "mongo/db/pipeline/expression_context.h" +#include "mongo/db/pipeline/field_path.h" +#include "mongo/db/query/plan_executor.h" + +namespace mongo { +/** + * Like DocumentSourceCursor, this stage returns Documents from BSONObjs produced by a PlanExecutor, + * but does extra work to compute distances to satisfy a $near or $nearSphere query. + */ +class DocumentSourceGeoNearCursor final : public DocumentSourceCursor { +public: + /** + * The name of this stage. + */ + static constexpr auto kStageName = "$geoNearCursor"; + + /** + * Create a new DocumentSourceGeoNearCursor. If specified, 'distanceMultiplier' must be + * nonnegative. + */ + static boost::intrusive_ptr<DocumentSourceGeoNearCursor> create( + Collection*, + std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>, + const boost::intrusive_ptr<ExpressionContext>&, + FieldPath distanceField, + boost::optional<FieldPath> locationField = boost::none, + double distanceMultiplier = 1.0); + + const char* getSourceName() const final; + + /** + * $geoNear returns documents ordered from nearest to furthest, which is an ascending sort on + * '_distanceField'. + */ + BSONObjSet getOutputSorts() final; + +private: + DocumentSourceGeoNearCursor(Collection*, + std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>, + const boost::intrusive_ptr<ExpressionContext>&, + FieldPath distanceField, + boost::optional<FieldPath> locationField, + double distanceMultiplier); + + ~DocumentSourceGeoNearCursor() = default; + + /** + * Transforms 'obj' into a Document, calculating the distance. + */ + Document transformBSONObjToDocument(const BSONObj& obj) const final; + + // The output field in which to store the computed distance. + FieldPath _distanceField; + + // The output field to store the point that matched, if specified. + boost::optional<FieldPath> _locationField; + + // A multiplicative factor applied to each distance. For example, you can use this to convert + // radian distances into meters by multiplying by the radius of the Earth. + double _distanceMultiplier = 1.0; +}; +} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_geo_near_test.cpp b/src/mongo/db/pipeline/document_source_geo_near_test.cpp index e1fdbc6d5e6..f6fe24096f5 100644 --- a/src/mongo/db/pipeline/document_source_geo_near_test.cpp +++ b/src/mongo/db/pipeline/document_source_geo_near_test.cpp @@ -44,45 +44,6 @@ namespace { // This provides access to getExpCtx(), but we'll use a different name for this test suite. using DocumentSourceGeoNearTest = AggregationContextFixture; -TEST_F(DocumentSourceGeoNearTest, ShouldAbsorbSubsequentLimitStage) { - auto geoNear = DocumentSourceGeoNear::create(getExpCtx()); - - Pipeline::SourceContainer container; - container.push_back(geoNear); - - ASSERT_EQUALS(geoNear->getLimit(), DocumentSourceGeoNear::kDefaultLimit); - - container.push_back(DocumentSourceLimit::create(getExpCtx(), 200)); - geoNear->optimizeAt(container.begin(), &container); - - ASSERT_EQUALS(container.size(), 1U); - ASSERT_EQUALS(geoNear->getLimit(), DocumentSourceGeoNear::kDefaultLimit); - - container.push_back(DocumentSourceLimit::create(getExpCtx(), 50)); - geoNear->optimizeAt(container.begin(), &container); - - ASSERT_EQUALS(container.size(), 1U); - ASSERT_EQUALS(geoNear->getLimit(), 50); - - container.push_back(DocumentSourceLimit::create(getExpCtx(), 30)); - geoNear->optimizeAt(container.begin(), &container); - - ASSERT_EQUALS(container.size(), 1U); - ASSERT_EQUALS(geoNear->getLimit(), 30); -} - -TEST_F(DocumentSourceGeoNearTest, ShouldReportOutputsAreSortedByDistanceField) { - BSONObj queryObj = fromjson( - "{geoNear: { near: {type: 'Point', coordinates: [0, 0]}, distanceField: 'dist', " - "maxDistance: 2}}"); - auto geoNear = DocumentSourceGeoNear::createFromBson(queryObj.firstElement(), getExpCtx()); - - BSONObjSet outputSort = geoNear->getOutputSorts(); - - ASSERT_EQUALS(outputSort.count(BSON("dist" << -1)), 1U); - ASSERT_EQUALS(outputSort.size(), 1U); -} - TEST_F(DocumentSourceGeoNearTest, FailToParseIfKeyFieldNotAString) { auto stageObj = fromjson("{$geoNear: {distanceField: 'dist', near: [0, 0], key: 1}}"); ASSERT_THROWS_CODE(DocumentSourceGeoNear::createFromBson(stageObj.firstElement(), getExpCtx()), @@ -97,6 +58,35 @@ TEST_F(DocumentSourceGeoNearTest, FailToParseIfKeyIsTheEmptyString) { ErrorCodes::BadValue); } +TEST_F(DocumentSourceGeoNearTest, FailToParseIfDistanceMultiplierIsNegative) { + auto stageObj = + fromjson("{$geoNear: {distanceField: 'dist', near: [0, 0], distanceMultiplier: -1.0}}"); + ASSERT_THROWS_CODE(DocumentSourceGeoNear::createFromBson(stageObj.firstElement(), getExpCtx()), + AssertionException, + ErrorCodes::BadValue); +} + +TEST_F(DocumentSourceGeoNearTest, FailToParseIfLimitFieldSpecified) { + auto stageObj = fromjson("{$geoNear: {distanceField: 'dist', near: [0, 0], limit: 1}}"); + ASSERT_THROWS_CODE(DocumentSourceGeoNear::createFromBson(stageObj.firstElement(), getExpCtx()), + AssertionException, + 50858); +} + +TEST_F(DocumentSourceGeoNearTest, FailToParseIfNumFieldSpecified) { + auto stageObj = fromjson("{$geoNear: {distanceField: 'dist', near: [0, 0], num: 1}}"); + ASSERT_THROWS_CODE(DocumentSourceGeoNear::createFromBson(stageObj.firstElement(), getExpCtx()), + AssertionException, + 50857); +} + +TEST_F(DocumentSourceGeoNearTest, FailToParseIfStartOptionIsSpecified) { + auto stageObj = fromjson("{$geoNear: {distanceField: 'dist', near: [0, 0], start: {}}}"); + ASSERT_THROWS_CODE(DocumentSourceGeoNear::createFromBson(stageObj.firstElement(), getExpCtx()), + AssertionException, + 50856); +} + TEST_F(DocumentSourceGeoNearTest, CanParseAndSerializeKeyField) { auto stageObj = fromjson("{$geoNear: {distanceField: 'dist', near: [0, 0], key: 'a.b'}}"); auto geoNear = DocumentSourceGeoNear::createFromBson(stageObj.firstElement(), getExpCtx()); @@ -108,12 +98,9 @@ TEST_F(DocumentSourceGeoNearTest, CanParseAndSerializeKeyField) { Value{Document{{"key", "a.b"_sd}, {"near", std::vector<Value>{Value{0}, Value{0}}}, {"distanceField", "dist"_sd}, - {"limit", 100}, {"query", BSONObj()}, - {"spherical", false}, - {"distanceMultiplier", 1}}}}}}; + {"spherical", false}}}}}}; ASSERT_VALUE_EQ(expectedSerialization, serialized[0]); } - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_group_test.cpp b/src/mongo/db/pipeline/document_source_group_test.cpp index de99452f3d7..8d0ce4a6679 100644 --- a/src/mongo/db/pipeline/document_source_group_test.cpp +++ b/src/mongo/db/pipeline/document_source_group_test.cpp @@ -767,7 +767,7 @@ public: ASSERT_EQUALS(1U, dependencies.fields.count("u")); ASSERT_EQUALS(1U, dependencies.fields.count("v")); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } }; diff --git a/src/mongo/db/pipeline/document_source_limit_test.cpp b/src/mongo/db/pipeline/document_source_limit_test.cpp index 6294a897ff2..b776de9789b 100644 --- a/src/mongo/db/pipeline/document_source_limit_test.cpp +++ b/src/mongo/db/pipeline/document_source_limit_test.cpp @@ -98,7 +98,7 @@ TEST_F(DocumentSourceLimitTest, ShouldNotIntroduceAnyDependencies) { ASSERT_EQUALS(DocumentSource::SEE_NEXT, limit->getDependencies(&dependencies)); ASSERT_EQUALS(0U, dependencies.fields.size()); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceLimitTest, ShouldPropagatePauses) { diff --git a/src/mongo/db/pipeline/document_source_match.cpp b/src/mongo/db/pipeline/document_source_match.cpp index 47e85a4c6e7..45bc8c8629e 100644 --- a/src/mongo/db/pipeline/document_source_match.cpp +++ b/src/mongo/db/pipeline/document_source_match.cpp @@ -485,7 +485,7 @@ DocumentSource::GetDepsReturn DocumentSourceMatch::getDependencies(DepsTracker* // A $text aggregation field should return EXHAUSTIVE_FIELDS, since we don't necessarily // know what field it will be searching without examining indices. deps->needWholeDocument = true; - deps->setNeedTextScore(true); + deps->setNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE, true); return EXHAUSTIVE_FIELDS; } diff --git a/src/mongo/db/pipeline/document_source_match_test.cpp b/src/mongo/db/pipeline/document_source_match_test.cpp index 4e14a990c7a..5f81dc71986 100644 --- a/src/mongo/db/pipeline/document_source_match_test.cpp +++ b/src/mongo/db/pipeline/document_source_match_test.cpp @@ -219,7 +219,7 @@ TEST_F(DocumentSourceMatchTest, ShouldAddDependenciesOfAllBranchesOfOrClause) { ASSERT_EQUALS(1U, dependencies.fields.count("x.y")); ASSERT_EQUALS(2U, dependencies.fields.size()); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, TextSearchShouldRequireWholeDocumentAndTextScore) { @@ -227,7 +227,7 @@ TEST_F(DocumentSourceMatchTest, TextSearchShouldRequireWholeDocumentAndTextScore DepsTracker dependencies(DepsTracker::MetadataAvailable::kTextScore); ASSERT_EQUALS(DocumentSource::EXHAUSTIVE_FIELDS, match->getDependencies(&dependencies)); ASSERT_EQUALS(true, dependencies.needWholeDocument); - ASSERT_EQUALS(true, dependencies.getNeedTextScore()); + ASSERT_EQUALS(true, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, ShouldOnlyAddOuterFieldAsDependencyOfImplicitEqualityPredicate) { @@ -238,7 +238,7 @@ TEST_F(DocumentSourceMatchTest, ShouldOnlyAddOuterFieldAsDependencyOfImplicitEqu ASSERT_EQUALS(1U, dependencies.fields.count("a")); ASSERT_EQUALS(1U, dependencies.fields.size()); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, ShouldOnlyAddOuterFieldAsDependencyOfClausesWithinElemMatch) { @@ -249,7 +249,7 @@ TEST_F(DocumentSourceMatchTest, ShouldOnlyAddOuterFieldAsDependencyOfClausesWith ASSERT_EQUALS(1U, dependencies.fields.count("a")); ASSERT_EQUALS(1U, dependencies.fields.size()); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, @@ -266,7 +266,7 @@ TEST_F(DocumentSourceMatchTest, ASSERT_EQUALS(1U, dependencies.fields.count("a")); ASSERT_EQUALS(1U, dependencies.fields.size()); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, @@ -277,7 +277,7 @@ TEST_F(DocumentSourceMatchTest, ASSERT_EQUALS(DocumentSource::SEE_NEXT, match->getDependencies(&dependencies)); ASSERT_EQUALS(0U, dependencies.fields.size()); ASSERT_EQUALS(true, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, @@ -288,7 +288,7 @@ TEST_F(DocumentSourceMatchTest, ASSERT_EQUALS(DocumentSource::SEE_NEXT, match->getDependencies(&dependencies1)); ASSERT_EQUALS(0U, dependencies1.fields.size()); ASSERT_EQUALS(true, dependencies1.needWholeDocument); - ASSERT_EQUALS(false, dependencies1.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies1.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); query = fromjson("{a: {$_internalSchemaObjectMatch: {$_internalSchemaMaxProperties: 1}}}"); match = DocumentSourceMatch::create(query, getExpCtx()); @@ -297,7 +297,7 @@ TEST_F(DocumentSourceMatchTest, ASSERT_EQUALS(1U, dependencies2.fields.size()); ASSERT_EQUALS(1U, dependencies2.fields.count("a")); ASSERT_EQUALS(false, dependencies2.needWholeDocument); - ASSERT_EQUALS(false, dependencies2.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies2.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, @@ -310,7 +310,7 @@ TEST_F(DocumentSourceMatchTest, ASSERT_EQUALS(DocumentSource::SEE_NEXT, match->getDependencies(&dependencies)); ASSERT_EQUALS(1U, dependencies.fields.size()); ASSERT_EQUALS(true, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, @@ -321,7 +321,7 @@ TEST_F(DocumentSourceMatchTest, ASSERT_EQUALS(DocumentSource::SEE_NEXT, match->getDependencies(&dependencies)); ASSERT_EQUALS(0U, dependencies.fields.size()); ASSERT_EQUALS(true, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForClausesWithInternalSchemaType) { @@ -332,7 +332,7 @@ TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForClausesWithIntern ASSERT_EQUALS(1U, dependencies.fields.size()); ASSERT_EQUALS(1U, dependencies.fields.count("a")); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForClausesWithInternalSchemaCond) { @@ -345,7 +345,7 @@ TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForClausesWithIntern ASSERT_EQUALS(1U, dependencies.fields.count("b")); ASSERT_EQUALS(1U, dependencies.fields.count("c")); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForClausesWithInternalSchemaXor) { @@ -358,7 +358,7 @@ TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForClausesWithIntern ASSERT_EQUALS(1U, dependencies.fields.count("b")); ASSERT_EQUALS(1U, dependencies.fields.count("c")); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForClausesWithEmptyJSONSchema) { @@ -368,7 +368,7 @@ TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForClausesWithEmptyJ ASSERT_EQUALS(DocumentSource::SEE_NEXT, match->getDependencies(&dependencies)); ASSERT_EQUALS(0U, dependencies.fields.size()); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForClausesWithJSONSchemaProperties) { @@ -379,7 +379,7 @@ TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForClausesWithJSONSc ASSERT_EQUALS(1U, dependencies.fields.count("a")); ASSERT_EQUALS(1U, dependencies.fields.size()); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForMultiplePredicatesWithJSONSchema) { @@ -391,7 +391,7 @@ TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForMultiplePredicate ASSERT_EQUALS(1U, dependencies.fields.count("a")); ASSERT_EQUALS(1U, dependencies.fields.count("b")); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, ShouldAddOuterFieldToDependenciesIfElemMatchContainsNoFieldNames) { @@ -402,7 +402,7 @@ TEST_F(DocumentSourceMatchTest, ShouldAddOuterFieldToDependenciesIfElemMatchCont ASSERT_EQUALS(1U, dependencies.fields.count("a")); ASSERT_EQUALS(1U, dependencies.fields.size()); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, ShouldAddNotClausesFieldAsDependency) { @@ -412,7 +412,7 @@ TEST_F(DocumentSourceMatchTest, ShouldAddNotClausesFieldAsDependency) { ASSERT_EQUALS(1U, dependencies.fields.count("b")); ASSERT_EQUALS(1U, dependencies.fields.size()); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, ShouldAddDependenciesOfEachNorClause) { @@ -424,7 +424,7 @@ TEST_F(DocumentSourceMatchTest, ShouldAddDependenciesOfEachNorClause) { ASSERT_EQUALS(1U, dependencies.fields.count("b.c")); ASSERT_EQUALS(2U, dependencies.fields.size()); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, CommentShouldNotAddAnyDependencies) { @@ -433,7 +433,7 @@ TEST_F(DocumentSourceMatchTest, CommentShouldNotAddAnyDependencies) { ASSERT_EQUALS(DocumentSource::SEE_NEXT, match->getDependencies(&dependencies)); ASSERT_EQUALS(0U, dependencies.fields.size()); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, ClauseAndedWithCommentShouldAddDependencies) { @@ -444,7 +444,7 @@ TEST_F(DocumentSourceMatchTest, ClauseAndedWithCommentShouldAddDependencies) { ASSERT_EQUALS(1U, dependencies.fields.count("a")); ASSERT_EQUALS(1U, dependencies.fields.size()); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceMatchTest, MultipleMatchStagesShouldCombineIntoOne) { diff --git a/src/mongo/db/pipeline/document_source_project_test.cpp b/src/mongo/db/pipeline/document_source_project_test.cpp index 5688d343675..c991eebbbc4 100644 --- a/src/mongo/db/pipeline/document_source_project_test.cpp +++ b/src/mongo/db/pipeline/document_source_project_test.cpp @@ -181,7 +181,7 @@ TEST_F(ProjectStageTest, InclusionShouldAddDependenciesOfIncludedAndComputedFiel ASSERT_EQUALS(1U, dependencies.fields.count("c")); ASSERT_EQUALS(1U, dependencies.fields.count("d")); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(true, dependencies.getNeedTextScore()); + ASSERT_EQUALS(true, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(ProjectStageTest, ExclusionShouldNotAddDependencies) { @@ -192,7 +192,7 @@ TEST_F(ProjectStageTest, ExclusionShouldNotAddDependencies) { ASSERT_EQUALS(0U, dependencies.fields.size()); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(ProjectStageTest, InclusionProjectionReportsIncludedPathsFromGetModifiedPaths) { diff --git a/src/mongo/db/pipeline/document_source_replace_root_test.cpp b/src/mongo/db/pipeline/document_source_replace_root_test.cpp index 321afbc8220..0ac8a9f9db0 100644 --- a/src/mongo/db/pipeline/document_source_replace_root_test.cpp +++ b/src/mongo/db/pipeline/document_source_replace_root_test.cpp @@ -268,7 +268,7 @@ TEST_F(ReplaceRootBasics, OnlyDependentFieldIsNewRoot) { // Should not need any other fields. ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(ReplaceRootBasics, ReplaceRootModifiesAllFields) { diff --git a/src/mongo/db/pipeline/document_source_sort.cpp b/src/mongo/db/pipeline/document_source_sort.cpp index 30ee3a5b2e2..a3374521faf 100644 --- a/src/mongo/db/pipeline/document_source_sort.cpp +++ b/src/mongo/db/pipeline/document_source_sort.cpp @@ -226,7 +226,7 @@ DocumentSource::GetDepsReturn DocumentSourceSort::getDependencies(DepsTracker* d } if (pExpCtx->needsMerge) { // Include the sort key if we will merge several sorted streams later. - deps->setNeedSortKey(true); + deps->setNeedsMetadata(DepsTracker::MetadataType::SORT_KEY, true); } return SEE_NEXT; diff --git a/src/mongo/db/pipeline/document_source_sort_test.cpp b/src/mongo/db/pipeline/document_source_sort_test.cpp index fca4caaaf28..9f1f9606220 100644 --- a/src/mongo/db/pipeline/document_source_sort_test.cpp +++ b/src/mongo/db/pipeline/document_source_sort_test.cpp @@ -169,7 +169,7 @@ TEST_F(DocumentSourceSortTest, Dependencies) { ASSERT_EQUALS(1U, dependencies.fields.count("a")); ASSERT_EQUALS(1U, dependencies.fields.count("b.c")); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(DocumentSourceSortTest, OutputSort) { diff --git a/src/mongo/db/pipeline/document_source_unwind_test.cpp b/src/mongo/db/pipeline/document_source_unwind_test.cpp index 8c01c50bde9..af04d436083 100644 --- a/src/mongo/db/pipeline/document_source_unwind_test.cpp +++ b/src/mongo/db/pipeline/document_source_unwind_test.cpp @@ -680,7 +680,7 @@ TEST_F(UnwindStageTest, AddsUnwoundPathToDependencies) { ASSERT_EQUALS(1U, dependencies.fields.size()); ASSERT_EQUALS(1U, dependencies.fields.count("x.y.z")); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(UnwindStageTest, TruncatesOutputSortAtUnwoundPath) { diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp index 9094071c9d0..5cb0122f565 100644 --- a/src/mongo/db/pipeline/expression.cpp +++ b/src/mongo/db/pipeline/expression.cpp @@ -2534,7 +2534,7 @@ Value ExpressionMeta::evaluate(const Document& root) const { void ExpressionMeta::_doAddDependencies(DepsTracker* deps) const { if (_metaType == MetaType::TEXT_SCORE) { - deps->setNeedTextScore(true); + deps->setNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE, true); } } diff --git a/src/mongo/db/pipeline/expression_test.cpp b/src/mongo/db/pipeline/expression_test.cpp index 374056289c3..2d7c4da922a 100644 --- a/src/mongo/db/pipeline/expression_test.cpp +++ b/src/mongo/db/pipeline/expression_test.cpp @@ -244,7 +244,7 @@ protected: } ASSERT_BSONOBJ_EQ(expectedDependencies, dependenciesBson.arr()); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } void assertContents(const intrusive_ptr<Testable>& expr, const BSONArray& expectedContents) { @@ -1565,7 +1565,7 @@ public: ASSERT_EQUALS(1U, dependencies.fields.size()); ASSERT_EQUALS(1U, dependencies.fields.count("a.b")); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } }; @@ -2071,7 +2071,7 @@ public: expression->addDependencies(&dependencies); ASSERT_EQUALS(0U, dependencies.fields.size()); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } }; @@ -2502,7 +2502,7 @@ public: ASSERT_EQUALS(1U, dependencies.fields.size()); ASSERT_EQUALS(1U, dependencies.fields.count("a.b")); ASSERT_EQUALS(false, dependencies.needWholeDocument); - ASSERT_EQUALS(false, dependencies.getNeedTextScore()); + ASSERT_EQUALS(false, dependencies.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } }; diff --git a/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp b/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp index 348044018e9..33d915a47a9 100644 --- a/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp +++ b/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp @@ -120,7 +120,7 @@ TEST(ExclusionProjection, ShouldNotAddAnyDependencies) { ASSERT_EQ(deps.fields.size(), 0UL); ASSERT_FALSE(deps.needWholeDocument); - ASSERT_FALSE(deps.getNeedTextScore()); + ASSERT_FALSE(deps.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST(ExclusionProjection, ShouldReportExcludedFieldsAsModified) { diff --git a/src/mongo/db/pipeline/pipeline.cpp b/src/mongo/db/pipeline/pipeline.cpp index 79dd962d871..6654b836a3e 100644 --- a/src/mongo/db/pipeline/pipeline.cpp +++ b/src/mongo/db/pipeline/pipeline.cpp @@ -69,6 +69,9 @@ using DiskUseRequirement = DocumentSource::StageConstraints::DiskUseRequirement; using FacetRequirement = DocumentSource::StageConstraints::FacetRequirement; using StreamType = DocumentSource::StageConstraints::StreamType; +constexpr MatchExpressionParser::AllowedFeatureSet Pipeline::kAllowedMatcherFeatures; +constexpr MatchExpressionParser::AllowedFeatureSet Pipeline::kGeoNearMatcherFeatures; + Pipeline::Pipeline(const intrusive_ptr<ExpressionContext>& pTheCtx) : pCtx(pTheCtx) {} Pipeline::Pipeline(SourceContainer stages, const intrusive_ptr<ExpressionContext>& expCtx) @@ -509,12 +512,9 @@ DepsTracker Pipeline::getDependencies(DepsTracker::MetadataAvailable metadataAva } if (!knowAllMeta) { - if (localDeps.getNeedTextScore()) - deps.setNeedTextScore(true); - - if (localDeps.getNeedSortKey()) - deps.setNeedSortKey(true); - + for (auto&& req : localDeps.getAllRequiredMetadataTypes()) { + deps.setNeedsMetadata(req, true); + } knowAllMeta = status & DocumentSource::EXHAUSTIVE_META; } @@ -531,11 +531,12 @@ DepsTracker Pipeline::getDependencies(DepsTracker::MetadataAvailable metadataAva if (metadataAvailable & DepsTracker::MetadataAvailable::kTextScore) { // If there is a text score, assume we need to keep it if we can't prove we don't. If we are // the first half of a pipeline which has been split, future stages might need it. - if (!knowAllMeta) - deps.setNeedTextScore(true); + if (!knowAllMeta) { + deps.setNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE, true); + } } else { // If there is no text score available, then we don't need to ask for it. - deps.setNeedTextScore(false); + deps.setNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE, false); } return deps; diff --git a/src/mongo/db/pipeline/pipeline.h b/src/mongo/db/pipeline/pipeline.h index 22a8f8a8f88..afcedc14ee6 100644 --- a/src/mongo/db/pipeline/pipeline.h +++ b/src/mongo/db/pipeline/pipeline.h @@ -69,7 +69,7 @@ public: enum class SplitState { kUnsplit, kSplitForShards, kSplitForMerge }; /** - * List of supported match expression features in a pipeline. + * The list of default supported match expression features. */ static constexpr MatchExpressionParser::AllowedFeatureSet kAllowedMatcherFeatures = MatchExpressionParser::AllowedFeatures::kText | @@ -77,6 +77,15 @@ public: MatchExpressionParser::AllowedFeatures::kJSONSchema; /** + * The match expression features allowed when running a pipeline with $geoNear. + */ + static constexpr MatchExpressionParser::AllowedFeatureSet kGeoNearMatcherFeatures = + MatchExpressionParser::AllowedFeatures::kText | + MatchExpressionParser::AllowedFeatures::kExpr | + MatchExpressionParser::AllowedFeatures::kJSONSchema | + MatchExpressionParser::AllowedFeatures::kGeoNear; + + /** * Parses a Pipeline from a vector of BSONObjs. Returns a non-OK status if it failed to parse. * The returned pipeline is not optimized, but the caller may convert it to an optimized * pipeline by calling optimizePipeline(). diff --git a/src/mongo/db/pipeline/pipeline_d.cpp b/src/mongo/db/pipeline/pipeline_d.cpp index 7b5cd468130..9fec47e815d 100644 --- a/src/mongo/db/pipeline/pipeline_d.cpp +++ b/src/mongo/db/pipeline/pipeline_d.cpp @@ -58,6 +58,8 @@ #include "mongo/db/pipeline/document_source.h" #include "mongo/db/pipeline/document_source_change_stream.h" #include "mongo/db/pipeline/document_source_cursor.h" +#include "mongo/db/pipeline/document_source_geo_near.h" +#include "mongo/db/pipeline/document_source_geo_near_cursor.h" #include "mongo/db/pipeline/document_source_match.h" #include "mongo/db/pipeline/document_source_merge_cursors.h" #include "mongo/db/pipeline/document_source_sample.h" @@ -181,7 +183,8 @@ StatusWith<std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>> attemptToGetExe BSONObj projectionObj, BSONObj sortObj, const AggregationRequest* aggRequest, - const size_t plannerOpts) { + const size_t plannerOpts, + const MatchExpressionParser::AllowedFeatureSet& matcherFeatures) { auto qr = stdx::make_unique<QueryRequest>(nss); qr->setTailableMode(pExpCtx->tailableMode); qr->setOplogReplay(oplogReplay); @@ -206,7 +209,7 @@ StatusWith<std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>> attemptToGetExe const ExtensionsCallbackReal extensionsCallback(pExpCtx->opCtx, &nss); auto cq = CanonicalQuery::canonicalize( - opCtx, std::move(qr), pExpCtx, extensionsCallback, Pipeline::kAllowedMatcherFeatures); + opCtx, std::move(qr), pExpCtx, extensionsCallback, matcherFeatures); if (!cq.isOK()) { // Return an error instead of uasserting, since there are cases where the combination of @@ -226,6 +229,50 @@ BSONObj removeSortKeyMetaProjection(BSONObj projectionObj) { } return projectionObj.removeField(Document::metaFieldSortKey); } + +/** + * Examines the indexes in 'collection' and returns the field name of a geo-indexed field suitable + * for use in $geoNear. 2d indexes are given priority over 2dsphere indexes. + * + * The 'collection' is required to exist. Throws if no usable 2d or 2dsphere index could be found. + */ +StringData extractGeoNearFieldFromIndexes(OperationContext* opCtx, Collection* collection) { + invariant(collection); + + std::vector<IndexDescriptor*> idxs; + collection->getIndexCatalog()->findIndexByType(opCtx, IndexNames::GEO_2D, idxs); + uassert(ErrorCodes::IndexNotFound, + str::stream() << "There is more than one 2d index on " << collection->ns().ns() + << "; unsure which to use for $geoNear", + idxs.size() <= 1U); + if (idxs.size() == 1U) { + for (auto&& elem : idxs.front()->keyPattern()) { + if (elem.type() == BSONType::String && elem.valueStringData() == IndexNames::GEO_2D) { + return elem.fieldNameStringData(); + } + } + MONGO_UNREACHABLE; + } + + // If there are no 2d indexes, look for a 2dsphere index. + idxs.clear(); + collection->getIndexCatalog()->findIndexByType(opCtx, IndexNames::GEO_2DSPHERE, idxs); + uassert(ErrorCodes::IndexNotFound, + "$geoNear requires a 2d or 2dsphere index, but none were found", + !idxs.empty()); + uassert(ErrorCodes::IndexNotFound, + str::stream() << "There is more than one 2dsphere index on " << collection->ns().ns() + << "; unsure which to use for $geoNear", + idxs.size() <= 1U); + + invariant(idxs.size() == 1U); + for (auto&& elem : idxs.front()->keyPattern()) { + if (elem.type() == BSONType::String && elem.valueStringData() == IndexNames::GEO_2DSPHERE) { + return elem.fieldNameStringData(); + } + } + MONGO_UNREACHABLE; +} } // namespace void PipelineD::prepareCursorSource(Collection* collection, @@ -260,16 +307,32 @@ void PipelineD::prepareCursorSource(Collection* collection, expCtx, sampleSize, idString, numRecords)); addCursorSource( - collection, pipeline, - expCtx, - std::move(exec), + DocumentSourceCursor::create(collection, std::move(exec), expCtx), pipeline->getDependencies(DepsTracker::MetadataAvailable::kNoMetadata)); return; } } } + // If the first stage is $geoNear, prepare a special DocumentSourceGeoNearCursor stage; + // otherwise, create a generic DocumentSourceCursor. + const auto geoNearStage = + sources.empty() ? nullptr : dynamic_cast<DocumentSourceGeoNear*>(sources.front().get()); + if (geoNearStage) { + prepareGeoNearCursorSource(collection, nss, aggRequest, pipeline); + } else { + prepareGenericCursorSource(collection, nss, aggRequest, pipeline); + } +} + +void PipelineD::prepareGenericCursorSource(Collection* collection, + const NamespaceString& nss, + const AggregationRequest* aggRequest, + Pipeline* pipeline) { + Pipeline::SourceContainer& sources = pipeline->_sources; + auto expCtx = pipeline->getContext(); + // Look for an initial match. This works whether we got an initial query or not. If not, it // results in a "{}" query, which will be what we want in that case. bool oplogReplay = false; @@ -283,7 +346,7 @@ void PipelineD::prepareCursorSource(Collection* collection, sources.pop_front(); } else { // A $geoNear stage, the only other stage that can produce an initial query, is also - // a valid initial stage and will be handled above. + // a valid initial stage. However, we should be in prepareGeoNearCursorSource() instead. MONGO_UNREACHABLE; } } @@ -321,6 +384,7 @@ void PipelineD::prepareCursorSource(Collection* collection, deps, queryObj, aggRequest, + Pipeline::kAllowedMatcherFeatures, &sortObj, &projForQuery)); @@ -335,8 +399,71 @@ void PipelineD::prepareCursorSource(Collection* collection, } } - addCursorSource( - collection, pipeline, expCtx, std::move(exec), deps, queryObj, sortObj, projForQuery); + addCursorSource(pipeline, + DocumentSourceCursor::create(collection, std::move(exec), expCtx), + deps, + queryObj, + sortObj, + projForQuery); +} + +void PipelineD::prepareGeoNearCursorSource(Collection* collection, + const NamespaceString& nss, + const AggregationRequest* aggRequest, + Pipeline* pipeline) { + uassert(ErrorCodes::NamespaceNotFound, + str::stream() << "$geoNear requires a geo index to run, but " << nss.ns() + << " does not exist", + collection); + + Pipeline::SourceContainer& sources = pipeline->_sources; + auto expCtx = pipeline->getContext(); + const auto geoNearStage = dynamic_cast<DocumentSourceGeoNear*>(sources.front().get()); + invariant(geoNearStage); + + auto deps = pipeline->getDependencies(DepsTracker::kAllGeoNearDataAvailable); + + // If the user specified a "key" field, use that field to satisfy the "near" query. Otherwise, + // look for a geo-indexed field in 'collection' that can. + auto nearFieldName = + (geoNearStage->getKeyField() ? geoNearStage->getKeyField()->fullPath() + : extractGeoNearFieldFromIndexes(expCtx->opCtx, collection)) + .toString(); + + // Create a PlanExecutor whose query is the "near" predicate on 'nearFieldName' combined with + // the optional "query" argument in the $geoNear stage. + BSONObj fullQuery = geoNearStage->asNearQuery(nearFieldName); + BSONObj proj = deps.toProjection(); + BSONObj sortFromQuerySystem; + auto exec = uassertStatusOK(prepareExecutor(expCtx->opCtx, + collection, + nss, + pipeline, + expCtx, + false, /* oplogReplay */ + nullptr, /* sortStage */ + deps, + std::move(fullQuery), + aggRequest, + Pipeline::kGeoNearMatcherFeatures, + &sortFromQuerySystem, + &proj)); + + invariant(sortFromQuerySystem.isEmpty(), + str::stream() << "Unexpectedly got the following sort from the query system: " + << sortFromQuerySystem.jsonString()); + + auto geoNearCursor = + DocumentSourceGeoNearCursor::create(collection, + std::move(exec), + expCtx, + geoNearStage->getDistanceField(), + geoNearStage->getLocationField(), + geoNearStage->getDistanceMultiplier().value_or(1.0)); + + // Remove the initial $geoNear; it will be replaced by $geoNearCursor. + sources.pop_front(); + addCursorSource(pipeline, std::move(geoNearCursor), std::move(deps)); } StatusWith<std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>> PipelineD::prepareExecutor( @@ -350,6 +477,7 @@ StatusWith<std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>> PipelineD::prep const DepsTracker& deps, const BSONObj& queryObj, const AggregationRequest* aggRequest, + const MatchExpressionParser::AllowedFeatureSet& matcherFeatures, BSONObj* sortObj, BSONObj* projectionObj) { // The query system has the potential to use an index to provide a non-blocking sort and/or to @@ -378,11 +506,11 @@ StatusWith<std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>> PipelineD::prep plannerOpts |= QueryPlannerParams::IS_COUNT; } - // The only way to get a text score or the sort key is to let the query system handle the - // projection. In all other cases, unless the query system can do an index-covered projection - // and avoid going to the raw record at all, it is faster to have ParsedDeps filter the fields - // we need. - if (!deps.getNeedTextScore() && !deps.getNeedSortKey()) { + // The only way to get meta information (e.g. the text score) is to let the query system handle + // the projection. In all other cases, unless the query system can do an index-covered + // projection and avoid going to the raw record at all, it is faster to have ParsedDeps filter + // the fields we need. + if (!deps.getNeedsAnyMetadata()) { plannerOpts |= QueryPlannerParams::NO_UNCOVERED_PROJECTIONS; } @@ -405,7 +533,8 @@ StatusWith<std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>> PipelineD::prep expCtx->needsMerge ? metaSortProjection : emptyProjection, *sortObj, aggRequest, - plannerOpts); + plannerOpts, + matcherFeatures); if (swExecutorSort.isOK()) { // Success! Now see if the query system can also cover the projection. @@ -418,7 +547,8 @@ StatusWith<std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>> PipelineD::prep *projectionObj, *sortObj, aggRequest, - plannerOpts); + plannerOpts, + matcherFeatures); std::unique_ptr<PlanExecutor, PlanExecutor::Deleter> exec; if (swExecutorSortAndProj.isOK()) { @@ -458,7 +588,9 @@ StatusWith<std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>> PipelineD::prep // sort. dassert(sortObj->isEmpty()); *projectionObj = removeSortKeyMetaProjection(*projectionObj); - if (deps.getNeedSortKey() && !deps.getNeedTextScore()) { + const auto metadataRequired = deps.getAllRequiredMetadataTypes(); + if (metadataRequired.size() == 1 && + metadataRequired.front() == DepsTracker::MetadataType::SORT_KEY) { // A sort key requirement would have prevented us from being able to add this parameter // before, but now we know the query system won't cover the sort, so we will be able to // compute the sort key ourselves during the $sort stage, and thus don't need a query @@ -476,7 +608,8 @@ StatusWith<std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>> PipelineD::prep *projectionObj, *sortObj, aggRequest, - plannerOpts); + plannerOpts, + matcherFeatures); if (swExecutorProj.isOK()) { // Success! We have a covered projection. return std::move(swExecutorProj.getValue()); @@ -499,34 +632,24 @@ StatusWith<std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>> PipelineD::prep *projectionObj, *sortObj, aggRequest, - plannerOpts); + plannerOpts, + matcherFeatures); } -void PipelineD::addCursorSource(Collection* collection, - Pipeline* pipeline, - const intrusive_ptr<ExpressionContext>& expCtx, - unique_ptr<PlanExecutor, PlanExecutor::Deleter> exec, +void PipelineD::addCursorSource(Pipeline* pipeline, + boost::intrusive_ptr<DocumentSourceCursor> cursor, DepsTracker deps, const BSONObj& queryObj, const BSONObj& sortObj, const BSONObj& projectionObj) { - // DocumentSourceCursor expects a yielding PlanExecutor that has had its state saved. - exec->saveState(); - - // Put the PlanExecutor into a DocumentSourceCursor and add it to the front of the pipeline. - intrusive_ptr<DocumentSourceCursor> pSource = - DocumentSourceCursor::create(collection, std::move(exec), expCtx); - - // Note the query, sort, and projection for explain. - pSource->setQuery(queryObj); - pSource->setSort(sortObj); - + cursor->setQuery(queryObj); + cursor->setSort(sortObj); if (deps.hasNoRequirements()) { - pSource->shouldProduceEmptyDocs(); + cursor->shouldProduceEmptyDocs(); } if (!projectionObj.isEmpty()) { - pSource->setProjection(projectionObj, boost::none); + cursor->setProjection(projectionObj, boost::none); } else { // There may be fewer dependencies now if the sort was covered. if (!sortObj.isEmpty()) { @@ -535,9 +658,9 @@ void PipelineD::addCursorSource(Collection* collection, : DepsTracker::MetadataAvailable::kNoMetadata); } - pSource->setProjection(deps.toProjection(), deps.toParsedDeps()); + cursor->setProjection(deps.toProjection(), deps.toParsedDeps()); } - pipeline->addInitialSource(pSource); + pipeline->addInitialSource(std::move(cursor)); } Timestamp PipelineD::getLatestOplogTimestamp(const Pipeline* pipeline) { diff --git a/src/mongo/db/pipeline/pipeline_d.h b/src/mongo/db/pipeline/pipeline_d.h index 4064b38e7c8..f85b13d1001 100644 --- a/src/mongo/db/pipeline/pipeline_d.h +++ b/src/mongo/db/pipeline/pipeline_d.h @@ -35,6 +35,7 @@ #include "mongo/db/dbdirectclient.h" #include "mongo/db/namespace_string.h" #include "mongo/db/pipeline/aggregation_request.h" +#include "mongo/db/pipeline/document_source_cursor.h" #include "mongo/db/pipeline/mongo_process_common.h" #include "mongo/db/query/plan_executor.h" @@ -136,7 +137,7 @@ public: /** * If the first stage in the pipeline does not generate its own output documents, attaches a - * DocumentSourceCursor to the front of the pipeline which will output documents from the + * cursor document source to the front of the pipeline which will output documents from the * collection to feed into the pipeline. * * This method looks for early pipeline stages that can be folded into the underlying @@ -154,6 +155,24 @@ public: Pipeline* pipeline); /** + * Prepare a generic DocumentSourceCursor for 'pipeline'. + */ + static void prepareGenericCursorSource(Collection* collection, + const NamespaceString& nss, + const AggregationRequest* aggRequest, + Pipeline* pipeline); + + /** + * Prepare a special DocumentSourceGeoNearCursor for 'pipeline'. Unlike + * 'prepareGenericCursorSource()', throws if 'collection' does not exist, as the $geoNearCursor + * requires a 2d or 2dsphere index. + */ + static void prepareGeoNearCursorSource(Collection* collection, + const NamespaceString& nss, + const AggregationRequest* aggRequest, + Pipeline* pipeline); + + /** * Injects a MongodInterface into stages which require access to mongod-specific functionality. */ static void injectMongodInterface(Pipeline* pipeline); @@ -187,17 +206,17 @@ private: const DepsTracker& deps, const BSONObj& queryObj, const AggregationRequest* aggRequest, + const MatchExpressionParser::AllowedFeatureSet& matcherFeatures, BSONObj* sortObj, BSONObj* projectionObj); /** - * Creates a DocumentSourceCursor from the given PlanExecutor and adds it to the front of the - * Pipeline. + * Adds 'cursor' to the front of 'pipeline', using 'deps' to inform the cursor of its + * dependencies. If specified, 'queryObj', 'sortObj' and 'projectionObj' are passed to the + * cursor for explain reporting. */ - static void addCursorSource(Collection* collection, - Pipeline* pipeline, - const boost::intrusive_ptr<ExpressionContext>& expCtx, - std::unique_ptr<PlanExecutor, PlanExecutor::Deleter> exec, + static void addCursorSource(Pipeline* pipeline, + boost::intrusive_ptr<DocumentSourceCursor> cursor, DepsTracker deps, const BSONObj& queryObj = BSONObj(), const BSONObj& sortObj = BSONObj(), diff --git a/src/mongo/db/pipeline/pipeline_test.cpp b/src/mongo/db/pipeline/pipeline_test.cpp index c53262cfc47..a0573014a1b 100644 --- a/src/mongo/db/pipeline/pipeline_test.cpp +++ b/src/mongo/db/pipeline/pipeline_test.cpp @@ -2492,11 +2492,11 @@ TEST_F(PipelineDependenciesTest, EmptyPipelineShouldRequireWholeDocument) { auto depsTracker = pipeline->getDependencies(DepsTracker::MetadataAvailable::kNoMetadata); ASSERT_TRUE(depsTracker.needWholeDocument); - ASSERT_FALSE(depsTracker.getNeedTextScore()); + ASSERT_FALSE(depsTracker.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); depsTracker = pipeline->getDependencies(DepsTracker::MetadataAvailable::kTextScore); ASSERT_TRUE(depsTracker.needWholeDocument); - ASSERT_TRUE(depsTracker.getNeedTextScore()); + ASSERT_TRUE(depsTracker.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } // @@ -2557,7 +2557,7 @@ public: class DocumentSourceNeedsOnlyTextScore : public DocumentSourceDependencyDummy { public: GetDepsReturn getDependencies(DepsTracker* deps) const final { - deps->setNeedTextScore(true); + deps->setNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE, true); return GetDepsReturn::EXHAUSTIVE_META; } @@ -2586,7 +2586,7 @@ TEST_F(PipelineDependenciesTest, ShouldRequireWholeDocumentIfAnyStageDoesNotSupp auto depsTracker = pipeline->getDependencies(DepsTracker::MetadataAvailable::kNoMetadata); ASSERT_TRUE(depsTracker.needWholeDocument); // The inputs did not have a text score available, so we should not require a text score. - ASSERT_FALSE(depsTracker.getNeedTextScore()); + ASSERT_FALSE(depsTracker.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); // Now in the other order. pipeline = unittest::assertGet(Pipeline::create({notSupported, needsASeeNext}, ctx)); @@ -2625,7 +2625,7 @@ TEST_F(PipelineDependenciesTest, ShouldNotAddAnyRequiredFieldsAfterFirstStageWit auto depsTracker = pipeline->getDependencies(DepsTracker::MetadataAvailable::kNoMetadata); ASSERT_FALSE(depsTracker.needWholeDocument); - ASSERT_FALSE(depsTracker.getNeedTextScore()); + ASSERT_FALSE(depsTracker.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); // 'needsOnlyB' claims to know all its field dependencies, so we shouldn't add any from // 'needsASeeNext'. @@ -2638,7 +2638,7 @@ TEST_F(PipelineDependenciesTest, ShouldNotRequireTextScoreIfThereIsNoScoreAvaila auto pipeline = unittest::assertGet(Pipeline::create({}, ctx)); auto depsTracker = pipeline->getDependencies(DepsTracker::MetadataAvailable::kNoMetadata); - ASSERT_FALSE(depsTracker.getNeedTextScore()); + ASSERT_FALSE(depsTracker.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(PipelineDependenciesTest, ShouldThrowIfTextScoreIsNeededButNotPresent) { @@ -2655,12 +2655,12 @@ TEST_F(PipelineDependenciesTest, ShouldRequireTextScoreIfAvailableAndNoStageRetu auto pipeline = unittest::assertGet(Pipeline::create({}, ctx)); auto depsTracker = pipeline->getDependencies(DepsTracker::MetadataAvailable::kTextScore); - ASSERT_TRUE(depsTracker.getNeedTextScore()); + ASSERT_TRUE(depsTracker.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); auto needsASeeNext = DocumentSourceNeedsASeeNext::create(); pipeline = unittest::assertGet(Pipeline::create({needsASeeNext}, ctx)); depsTracker = pipeline->getDependencies(DepsTracker::MetadataAvailable::kTextScore); - ASSERT_TRUE(depsTracker.getNeedTextScore()); + ASSERT_TRUE(depsTracker.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } TEST_F(PipelineDependenciesTest, ShouldNotRequireTextScoreIfAvailableButDefinitelyNotNeeded) { @@ -2673,7 +2673,7 @@ TEST_F(PipelineDependenciesTest, ShouldNotRequireTextScoreIfAvailableButDefinite // 'stripsTextScore' claims that no further stage will need metadata information, so we // shouldn't have the text score as a dependency. - ASSERT_FALSE(depsTracker.getNeedTextScore()); + ASSERT_FALSE(depsTracker.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } } // namespace Dependencies diff --git a/src/mongo/embedded/capi_test.cpp b/src/mongo/embedded/capi_test.cpp index 0efaacba25f..2d35c8495bd 100644 --- a/src/mongo/embedded/capi_test.cpp +++ b/src/mongo/embedded/capi_test.cpp @@ -542,7 +542,6 @@ TEST_F(MongodbCAPITest, RunListCommands) { "explain", "find", "findAndModify", - "geoNear", "getLastError", "getMore", "getParameter", diff --git a/src/mongo/s/commands/SConscript b/src/mongo/s/commands/SConscript index 9a6680c10ed..cfb4aca929c 100644 --- a/src/mongo/s/commands/SConscript +++ b/src/mongo/s/commands/SConscript @@ -51,7 +51,6 @@ env.Library( 'cluster_flush_router_config_cmd.cpp', 'cluster_fsync_cmd.cpp', 'cluster_ftdc_commands.cpp', - 'cluster_geo_near_cmd.cpp', 'cluster_get_last_error_cmd.cpp', 'cluster_get_prev_error_cmd.cpp', 'cluster_get_shard_version_cmd.cpp', diff --git a/src/mongo/s/commands/cluster_geo_near_cmd.cpp b/src/mongo/s/commands/cluster_geo_near_cmd.cpp deleted file mode 100644 index 6871be7565e..00000000000 --- a/src/mongo/s/commands/cluster_geo_near_cmd.cpp +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Copyright (C) 2018 MongoDB Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * As a special exception, the copyright holders give permission to link the - * code of portions of this program with the OpenSSL library under certain - * conditions as described in each individual source file and distribute - * linked combinations including the program with the OpenSSL library. You - * must comply with the GNU Affero General Public License in all respects for - * all of the code used other than as permitted herein. If you modify file(s) - * with this exception, you may extend this exception to your version of the - * file(s), but you are not obligated to do so. If you do not wish to do so, - * delete this exception statement from your version. If you delete this - * exception statement from all source files in the program, then also delete - * it in the license file. - */ - -#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kCommand - -#include "mongo/platform/basic.h" - -#include "mongo/db/commands.h" -#include "mongo/rpc/get_status_from_command_result.h" -#include "mongo/s/commands/cluster_commands_helpers.h" -#include "mongo/s/grid.h" -#include "mongo/util/log.h" - -namespace mongo { -namespace { - -class Geo2dFindNearCmd : public BasicCommand { -public: - Geo2dFindNearCmd() : BasicCommand("geoNear") {} - - std::string help() const override { - return "The geoNear command is deprecated. See " - "http://dochub.mongodb.org/core/geoNear-deprecation for more detail on its " - "replacement."; - } - - AllowedOnSecondary secondaryAllowed(ServiceContext*) const override { - return AllowedOnSecondary::kAlways; - } - - bool adminOnly() const override { - return false; - } - - bool supportsWriteConcern(const BSONObj& cmd) const override { - return false; - } - - std::string parseNs(const std::string& dbname, const BSONObj& cmdObj) const override { - return CommandHelpers::parseNsCollectionRequired(dbname, cmdObj).ns(); - } - - void addRequiredPrivileges(const std::string& dbname, - const BSONObj& cmdObj, - std::vector<Privilege>* out) const override { - ActionSet actions; - actions.addAction(ActionType::find); - out->push_back(Privilege(parseResourcePattern(dbname, cmdObj), actions)); - } - - bool run(OperationContext* opCtx, - const std::string& dbName, - const BSONObj& cmdObj, - BSONObjBuilder& result) override { - RARELY { - warning() << "Support for the geoNear command has been deprecated. Please plan to " - "rewrite geoNear commands using the $near query operator, the $nearSphere " - "query operator, or the $geoNear aggregation stage. See " - "http://dochub.mongodb.org/core/geoNear-deprecation."; - } - - const NamespaceString nss(parseNs(dbName, cmdObj)); - - // We support both "num" and "limit" options to control limit - long long limit = 100; - if (cmdObj["num"].isNumber()) - limit = cmdObj["num"].safeNumberLong(); - else if (cmdObj["limit"].isNumber()) - limit = cmdObj["limit"].safeNumberLong(); - - const auto query = extractQuery(cmdObj); - const auto collation = extractCollation(cmdObj); - - const auto routingInfo = - uassertStatusOK(Grid::get(opCtx)->catalogCache()->getCollectionRoutingInfo(opCtx, nss)); - - auto shardResponses = scatterGatherVersionedTargetByRoutingTable( - opCtx, - nss.db(), - nss, - routingInfo, - CommandHelpers::filterCommandRequestForPassthrough(cmdObj), - ReadPreferenceSetting::get(opCtx), - Shard::RetryPolicy::kIdempotent, - query, - collation); - - std::multimap<double, BSONObj> results; - BSONArrayBuilder shardArray; - std::string nearStr; - double time = 0; - double btreelocs = 0; - double nscanned = 0; - double objectsLoaded = 0; - - for (const auto& shardResponse : shardResponses) { - const auto response = uassertStatusOK(shardResponse.swResponse); - uassertStatusOK(getStatusFromCommandResult(response.data)); - - shardArray.append(shardResponse.shardId.toString()); - const auto& shardResult = response.data; - - if (shardResult.hasField("near")) { - nearStr = shardResult["near"].String(); - } - time += shardResult["stats"]["time"].Number(); - if (!shardResult["stats"]["btreelocs"].eoo()) { - btreelocs += shardResult["stats"]["btreelocs"].Number(); - } - nscanned += shardResult["stats"]["nscanned"].Number(); - if (!shardResult["stats"]["objectsLoaded"].eoo()) { - objectsLoaded += shardResult["stats"]["objectsLoaded"].Number(); - } - - BSONForEach(obj, shardResult["results"].embeddedObject()) { - results.insert( - std::make_pair(obj["dis"].Number(), obj.embeddedObject().getOwned())); - } - - // TODO: maybe shrink results if size() > limit - } - - result.append("ns", nss.ns()); - result.append("near", nearStr); - - long long outCount = 0; - double totalDistance = 0; - double maxDistance = 0; - { - BSONArrayBuilder sub(result.subarrayStart("results")); - for (std::multimap<double, BSONObj>::const_iterator it(results.begin()), - end(results.end()); - it != end && outCount < limit; - ++it, ++outCount) { - totalDistance += it->first; - maxDistance = it->first; // guaranteed to be highest so far - - sub.append(it->second); - } - sub.done(); - } - - { - BSONObjBuilder sub(result.subobjStart("stats")); - sub.append("time", time); - sub.append("btreelocs", btreelocs); - sub.append("nscanned", nscanned); - sub.append("objectsLoaded", objectsLoaded); - sub.append("avgDistance", (outCount == 0) ? 0 : (totalDistance / outCount)); - sub.append("maxDistance", maxDistance); - sub.append("shards", shardArray.arr()); - sub.done(); - } - - return true; - } - -} geo2dFindNearCmd; - -} // namespace -} // namespace mongo |