Files
mongo/jstests/aggregation/bugs/server11675.js

225 lines
8.1 KiB
JavaScript

// 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.commandWorked(coll.insert({_id: 1, text: "apple", words: 1}));
assert.commandWorked(coll.insert({_id: 2, text: "banana", words: 1}));
assert.commandWorked(coll.insert({_id: 3, text: "apple banana", words: 2}));
assert.commandWorked(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'}}}
])
.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.commandWorked(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);
})();