summaryrefslogtreecommitdiff
path: root/jstests/aggregation/bugs/server11675.js
blob: 2d02a1ff53eaf39fbc8b6afb31cca66709788a5d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
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'}}}
          ])
          .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);
})();