diff options
Diffstat (limited to 'jstests/aggregation/bugs/server11675.js')
-rw-r--r-- | jstests/aggregation/bugs/server11675.js | 441 |
1 files changed, 219 insertions, 222 deletions
diff --git a/jstests/aggregation/bugs/server11675.js b/jstests/aggregation/bugs/server11675.js index 759b4393b30..2d02a1ff53e 100644 --- a/jstests/aggregation/bugs/server11675.js +++ b/jstests/aggregation/bugs/server11675.js @@ -1,227 +1,224 @@ // SERVER-11675 Text search integration with aggregation (function() { - load('jstests/aggregation/extras/utils.js'); // For 'assertErrorCode'. - load('jstests/libs/fixture_helpers.js'); // For 'FixtureHelpers' - - const coll = db.server11675; - coll.drop(); - - assert.writeOK(coll.insert({_id: 1, text: "apple", words: 1})); - assert.writeOK(coll.insert({_id: 2, text: "banana", words: 1})); - assert.writeOK(coll.insert({_id: 3, text: "apple banana", words: 2})); - assert.writeOK(coll.insert({_id: 4, text: "cantaloupe", words: 1})); - - assert.commandWorked(coll.createIndex({text: "text"})); - - // query should have subfields query, project, sort, skip and limit. All but query are optional. - const assertSameAsFind = function(query) { - let cursor = coll.find(query.query); - const pipeline = [{$match: query.query}]; - - if ('project' in query) { - cursor = coll.find(query.query, query.project); // no way to add to constructed cursor - pipeline.push({$project: query.project}); - } - - if ('sort' in query) { - cursor = cursor.sort(query.sort); - pipeline.push({$sort: query.sort}); - } - - if ('skip' in query) { - cursor = cursor.skip(query.skip); - pipeline.push({$skip: query.skip}); - } - - if ('limit' in query) { - cursor = cursor.limit(query.limit); - pipeline.push({$limit: query.limit}); - } - - const findRes = cursor.toArray(); - const aggRes = coll.aggregate(pipeline).toArray(); - - // If the query doesn't specify its own sort, there is a possibility that find() and - // aggregate() will return the same results in different orders. We sort by _id on the - // client side, so that the results still count as equal. - if (!query.hasOwnProperty("sort")) { - findRes.sort(function(a, b) { - return a._id - b._id; - }); - aggRes.sort(function(a, b) { - return a._id - b._id; - }); - } - - assert.docEq(aggRes, findRes); - }; - - assertSameAsFind({query: {}}); // sanity check - assertSameAsFind({query: {$text: {$search: "apple"}}}); - assertSameAsFind({query: {_id: 1, $text: {$search: "apple"}}}); - assertSameAsFind( - {query: {$text: {$search: "apple"}}, project: {_id: 1, score: {$meta: "textScore"}}}); - assertSameAsFind({ - query: {$text: {$search: "apple banana"}}, - project: {_id: 1, score: {$meta: "textScore"}} - }); - assertSameAsFind({ - query: {$text: {$search: "apple banana"}}, - project: {_id: 1, score: {$meta: "textScore"}}, - sort: {score: {$meta: "textScore"}} - }); - assertSameAsFind({ - query: {$text: {$search: "apple banana"}}, - project: {_id: 1, score: {$meta: "textScore"}}, - sort: {score: {$meta: "textScore"}}, - limit: 1 - }); - assertSameAsFind({ - query: {$text: {$search: "apple banana"}}, - project: {_id: 1, score: {$meta: "textScore"}}, - sort: {score: {$meta: "textScore"}}, - skip: 1 - }); - assertSameAsFind({ - query: {$text: {$search: "apple banana"}}, - project: {_id: 1, score: {$meta: "textScore"}}, - sort: {score: {$meta: "textScore"}}, - skip: 1, - limit: 1 - }); - - // $meta sort specification should be rejected if it has additional keys. - assert.throws(function() { - coll.aggregate([ - {$match: {$text: {$search: 'apple banana'}}}, - {$sort: {textScore: {$meta: 'textScore', extra: 1}}} - ]) - .itcount(); - }); - - // $meta sort specification should be rejected if the type of meta sort is not known. - assert.throws(function() { - coll.aggregate([ - {$match: {$text: {$search: 'apple banana'}}}, - {$sort: {textScore: {$meta: 'unknown'}}} - ]) - .itcount(); - }); - - // Sort specification should be rejected if a $-keyword other than $meta is used. - assert.throws(function() { - coll.aggregate([ - {$match: {$text: {$search: 'apple banana'}}}, - {$sort: {textScore: {$notMeta: 'textScore'}}} - ]) - .itcount(); - }); - - // Sort specification should be rejected if it is a string, not an object with $meta. - assert.throws(function() { - coll.aggregate( - [{$match: {$text: {$search: 'apple banana'}}}, {$sort: {textScore: 'textScore'}}]) - .itcount(); - }); - - // sharded find requires projecting the score to sort, but sharded agg does not. - var findRes = coll.find({$text: {$search: "apple banana"}}, {textScore: {$meta: 'textScore'}}) - .sort({textScore: {$meta: 'textScore'}}) - .map(function(obj) { - delete obj.textScore; // remove it to match agg output - return obj; - }); - let res = coll.aggregate([ - {$match: {$text: {$search: 'apple banana'}}}, - {$sort: {textScore: {$meta: 'textScore'}}} - ]) - .toArray(); - assert.eq(res, findRes); - - // Make sure {$meta: 'textScore'} can be used as a sub-expression - res = coll.aggregate([ - {$match: {_id: 1, $text: {$search: 'apple'}}}, - { - $project: { - words: 1, - score: {$meta: 'textScore'}, - wordsTimesScore: {$multiply: ['$words', {$meta: 'textScore'}]} - } - } - ]) - .toArray(); - assert.eq(res[0].wordsTimesScore, res[0].words * res[0].score, tojson(res)); - - // And can be used in $group - res = coll.aggregate([ - {$match: {_id: 1, $text: {$search: 'apple banana'}}}, - {$group: {_id: {$meta: 'textScore'}, score: {$first: {$meta: 'textScore'}}}} - ]) - .toArray(); - assert.eq(res[0]._id, res[0].score, tojson(res)); - - // Make sure metadata crosses shard -> merger boundary - res = coll.aggregate([ - {$match: {_id: 1, $text: {$search: 'apple'}}}, - {$project: {scoreOnShard: {$meta: 'textScore'}}}, - {$limit: 1}, // force a split. later stages run on merger - {$project: {scoreOnShard: 1, scoreOnMerger: {$meta: 'textScore'}}} - ]) - .toArray(); - assert.eq(res[0].scoreOnMerger, res[0].scoreOnShard); - let score = res[0].scoreOnMerger; // save for later tests - - // Make sure metadata crosses shard -> merger boundary even if not used on shard - res = coll.aggregate([ - {$match: {_id: 1, $text: {$search: 'apple'}}}, - {$limit: 1}, // force a split. later stages run on merger - {$project: {scoreOnShard: 1, scoreOnMerger: {$meta: 'textScore'}}} +load('jstests/aggregation/extras/utils.js'); // For 'assertErrorCode'. +load('jstests/libs/fixture_helpers.js'); // For 'FixtureHelpers' + +const coll = db.server11675; +coll.drop(); + +assert.writeOK(coll.insert({_id: 1, text: "apple", words: 1})); +assert.writeOK(coll.insert({_id: 2, text: "banana", words: 1})); +assert.writeOK(coll.insert({_id: 3, text: "apple banana", words: 2})); +assert.writeOK(coll.insert({_id: 4, text: "cantaloupe", words: 1})); + +assert.commandWorked(coll.createIndex({text: "text"})); + +// query should have subfields query, project, sort, skip and limit. All but query are optional. +const assertSameAsFind = function(query) { + let cursor = coll.find(query.query); + const pipeline = [{$match: query.query}]; + + if ('project' in query) { + cursor = coll.find(query.query, query.project); // no way to add to constructed cursor + pipeline.push({$project: query.project}); + } + + if ('sort' in query) { + cursor = cursor.sort(query.sort); + pipeline.push({$sort: query.sort}); + } + + if ('skip' in query) { + cursor = cursor.skip(query.skip); + pipeline.push({$skip: query.skip}); + } + + if ('limit' in query) { + cursor = cursor.limit(query.limit); + pipeline.push({$limit: query.limit}); + } + + const findRes = cursor.toArray(); + const aggRes = coll.aggregate(pipeline).toArray(); + + // If the query doesn't specify its own sort, there is a possibility that find() and + // aggregate() will return the same results in different orders. We sort by _id on the + // client side, so that the results still count as equal. + if (!query.hasOwnProperty("sort")) { + findRes.sort(function(a, b) { + return a._id - b._id; + }); + aggRes.sort(function(a, b) { + return a._id - b._id; + }); + } + + assert.docEq(aggRes, findRes); +}; + +assertSameAsFind({query: {}}); // sanity check +assertSameAsFind({query: {$text: {$search: "apple"}}}); +assertSameAsFind({query: {_id: 1, $text: {$search: "apple"}}}); +assertSameAsFind( + {query: {$text: {$search: "apple"}}, project: {_id: 1, score: {$meta: "textScore"}}}); +assertSameAsFind( + {query: {$text: {$search: "apple banana"}}, project: {_id: 1, score: {$meta: "textScore"}}}); +assertSameAsFind({ + query: {$text: {$search: "apple banana"}}, + project: {_id: 1, score: {$meta: "textScore"}}, + sort: {score: {$meta: "textScore"}} +}); +assertSameAsFind({ + query: {$text: {$search: "apple banana"}}, + project: {_id: 1, score: {$meta: "textScore"}}, + sort: {score: {$meta: "textScore"}}, + limit: 1 +}); +assertSameAsFind({ + query: {$text: {$search: "apple banana"}}, + project: {_id: 1, score: {$meta: "textScore"}}, + sort: {score: {$meta: "textScore"}}, + skip: 1 +}); +assertSameAsFind({ + query: {$text: {$search: "apple banana"}}, + project: {_id: 1, score: {$meta: "textScore"}}, + sort: {score: {$meta: "textScore"}}, + skip: 1, + limit: 1 +}); + +// $meta sort specification should be rejected if it has additional keys. +assert.throws(function() { + coll.aggregate([ + {$match: {$text: {$search: 'apple banana'}}}, + {$sort: {textScore: {$meta: 'textScore', extra: 1}}} + ]) + .itcount(); +}); + +// $meta sort specification should be rejected if the type of meta sort is not known. +assert.throws(function() { + coll.aggregate([ + {$match: {$text: {$search: 'apple banana'}}}, + {$sort: {textScore: {$meta: 'unknown'}}} + ]) + .itcount(); +}); + +// Sort specification should be rejected if a $-keyword other than $meta is used. +assert.throws(function() { + coll.aggregate([ + {$match: {$text: {$search: 'apple banana'}}}, + {$sort: {textScore: {$notMeta: 'textScore'}}} + ]) + .itcount(); +}); + +// Sort specification should be rejected if it is a string, not an object with $meta. +assert.throws(function() { + coll.aggregate( + [{$match: {$text: {$search: 'apple banana'}}}, {$sort: {textScore: 'textScore'}}]) + .itcount(); +}); + +// sharded find requires projecting the score to sort, but sharded agg does not. +var findRes = coll.find({$text: {$search: "apple banana"}}, {textScore: {$meta: 'textScore'}}) + .sort({textScore: {$meta: 'textScore'}}) + .map(function(obj) { + delete obj.textScore; // remove it to match agg output + return obj; + }); +let res = coll.aggregate([ + {$match: {$text: {$search: 'apple banana'}}}, + {$sort: {textScore: {$meta: 'textScore'}}} ]) .toArray(); - assert.eq(res[0].scoreOnMerger, score); - - // Make sure metadata works if first $project doesn't use it. - res = coll.aggregate([ - {$match: {_id: 1, $text: {$search: 'apple'}}}, - {$project: {_id: 1}}, - {$project: {_id: 1, score: {$meta: 'textScore'}}} - ]) - .toArray(); - assert.eq(res[0].score, score); - - // Make sure the pipeline fails if it tries to reference the text score and it doesn't exist. - res = coll.runCommand( - {aggregate: coll.getName(), pipeline: [{$project: {_id: 1, score: {$meta: 'textScore'}}}]}); - assert.commandFailed(res); - - // Make sure the metadata is 'missing()' when it doesn't exist because the document changed - res = coll.aggregate([ - {$match: {_id: 1, $text: {$search: 'apple banana'}}}, - {$group: {_id: 1, score: {$first: {$meta: 'textScore'}}}}, - {$project: {_id: 1, scoreAgain: {$meta: 'textScore'}}}, - ]) - .toArray(); - assert(!("scoreAgain" in res[0])); - - // Make sure metadata works after a $unwind - assert.writeOK(coll.insert({_id: 5, text: 'mango', words: [1, 2, 3]})); - res = coll.aggregate([ - {$match: {$text: {$search: 'mango'}}}, - {$project: {score: {$meta: "textScore"}, _id: 1, words: 1}}, - {$unwind: '$words'}, - {$project: {scoreAgain: {$meta: "textScore"}, score: 1}} - ]) - .toArray(); - assert.eq(res[0].scoreAgain, res[0].score); - - // Error checking - // $match, but wrong position - assertErrorCode( - coll, [{$sort: {text: 1}}, {$match: {$text: {$search: 'apple banana'}}}], 17313); - - // wrong $stage, but correct position - assertErrorCode(coll, - [{$project: {searchValue: {$text: {$search: 'apple banana'}}}}], - ErrorCodes.InvalidPipelineOperator); - assertErrorCode(coll, [{$sort: {$text: {$search: 'apple banana'}}}], 17312); +assert.eq(res, findRes); + +// Make sure {$meta: 'textScore'} can be used as a sub-expression +res = coll.aggregate([ + {$match: {_id: 1, $text: {$search: 'apple'}}}, + { + $project: { + words: 1, + score: {$meta: 'textScore'}, + wordsTimesScore: {$multiply: ['$words', {$meta: 'textScore'}]} + } + } + ]) + .toArray(); +assert.eq(res[0].wordsTimesScore, res[0].words * res[0].score, tojson(res)); + +// And can be used in $group +res = coll.aggregate([ + {$match: {_id: 1, $text: {$search: 'apple banana'}}}, + {$group: {_id: {$meta: 'textScore'}, score: {$first: {$meta: 'textScore'}}}} + ]) + .toArray(); +assert.eq(res[0]._id, res[0].score, tojson(res)); + +// Make sure metadata crosses shard -> merger boundary +res = coll.aggregate([ + {$match: {_id: 1, $text: {$search: 'apple'}}}, + {$project: {scoreOnShard: {$meta: 'textScore'}}}, + {$limit: 1}, // force a split. later stages run on merger + {$project: {scoreOnShard: 1, scoreOnMerger: {$meta: 'textScore'}}} + ]) + .toArray(); +assert.eq(res[0].scoreOnMerger, res[0].scoreOnShard); +let score = res[0].scoreOnMerger; // save for later tests + +// Make sure metadata crosses shard -> merger boundary even if not used on shard +res = coll.aggregate([ + {$match: {_id: 1, $text: {$search: 'apple'}}}, + {$limit: 1}, // force a split. later stages run on merger + {$project: {scoreOnShard: 1, scoreOnMerger: {$meta: 'textScore'}}} + ]) + .toArray(); +assert.eq(res[0].scoreOnMerger, score); + +// Make sure metadata works if first $project doesn't use it. +res = coll.aggregate([ + {$match: {_id: 1, $text: {$search: 'apple'}}}, + {$project: {_id: 1}}, + {$project: {_id: 1, score: {$meta: 'textScore'}}} + ]) + .toArray(); +assert.eq(res[0].score, score); + +// Make sure the pipeline fails if it tries to reference the text score and it doesn't exist. +res = coll.runCommand( + {aggregate: coll.getName(), pipeline: [{$project: {_id: 1, score: {$meta: 'textScore'}}}]}); +assert.commandFailed(res); + +// Make sure the metadata is 'missing()' when it doesn't exist because the document changed +res = coll.aggregate([ + {$match: {_id: 1, $text: {$search: 'apple banana'}}}, + {$group: {_id: 1, score: {$first: {$meta: 'textScore'}}}}, + {$project: {_id: 1, scoreAgain: {$meta: 'textScore'}}}, + ]) + .toArray(); +assert(!("scoreAgain" in res[0])); + +// Make sure metadata works after a $unwind +assert.writeOK(coll.insert({_id: 5, text: 'mango', words: [1, 2, 3]})); +res = coll.aggregate([ + {$match: {$text: {$search: 'mango'}}}, + {$project: {score: {$meta: "textScore"}, _id: 1, words: 1}}, + {$unwind: '$words'}, + {$project: {scoreAgain: {$meta: "textScore"}, score: 1}} + ]) + .toArray(); +assert.eq(res[0].scoreAgain, res[0].score); + +// Error checking +// $match, but wrong position +assertErrorCode(coll, [{$sort: {text: 1}}, {$match: {$text: {$search: 'apple banana'}}}], 17313); + +// wrong $stage, but correct position +assertErrorCode(coll, + [{$project: {searchValue: {$text: {$search: 'apple banana'}}}}], + ErrorCodes.InvalidPipelineOperator); +assertErrorCode(coll, [{$sort: {$text: {$search: 'apple banana'}}}], 17312); })(); |