summaryrefslogtreecommitdiff
path: root/jstests/noPassthroughWithMongod/all_paths_and_text_indexes.js
blob: 21f6a149aa9b1ae5b24edc234ba7da49642ee4f1 (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
/**
 * Tests that a {$**: 1} index can coexist with a {$**: 'text'} index in the same collection.
 *
 * Tagged as 'assumes_unsharded_collection' so that the expected relationship between the number of
 * IXSCANs in the explain output and the number of fields in the indexed documents is not distorted
 * by being spread across multiple shards.
 *
 * @tags: [assumes_unsharded_collection]
 *
 * TODO: SERVER-36198: Move this test back to jstests/core/
 */
(function() {
    "use strict";

    load("jstests/aggregation/extras/utils.js");  // For arrayEq.
    load("jstests/libs/analyze_plan.js");         // For getPlanStages and planHasStage.

    const assertArrayEq = (l, r) => assert(arrayEq(l, r), tojson(l) + " != " + tojson(r));

    const coll = db.all_paths_and_text_indexes;
    coll.drop();

    // Runs a single allPaths query test, confirming that an indexed solution exists, that the $**
    // index on the given 'expectedPath' was used to answer the query, and that the results are
    // identical to those obtained via COLLSCAN.
    function assertAllPathsQuery(query, expectedPath) {
        // Explain the query, and determine whether an indexed solution is available.
        const explainOutput = coll.find(query).explain("executionStats");
        const ixScans = getPlanStages(explainOutput.queryPlanner.winningPlan, "IXSCAN");
        // Verify that the winning plan uses the $** index with the expected path.
        assert.eq(ixScans.length, 1);
        assert.docEq(ixScans[0].keyPattern, {"$_path": 1, [expectedPath]: 1});
        // Verify that the results obtained from the $** index are identical to a COLLSCAN.
        assertArrayEq(coll.find(query).toArray(), coll.find(query).hint({$natural: 1}).toArray());
    }

    // Insert documents containing the field '_fts', which is reserved when using a $text index.
    assert.commandWorked(coll.insert({_id: 1, a: 1, _fts: 1, textToSearch: "banana"}));
    assert.commandWorked(coll.insert({_id: 2, a: 1, _fts: 2, textToSearch: "bananas"}));
    assert.commandWorked(coll.insert({_id: 3, a: 1, _fts: 3}));

    // Required in order to build $** indexes.
    assert.commandWorked(
        db.adminCommand({setParameter: 1, internalQueryAllowAllPathsIndexes: true}));
    try {
        // Build an allPaths index, and verify that it can be used to query for the field '_fts'.
        assert.commandWorked(coll.createIndex({"$**": 1}));
        assertAllPathsQuery({_fts: {$gt: 0, $lt: 4}}, '_fts');

        // Perform the tests below for simple and compound $text indexes.
        for (let textIndex of[{'$**': 'text'}, {a: 1, '$**': 'text'}]) {
            // Build the appropriate text index.
            assert.commandWorked(coll.createIndex(textIndex, {name: "textIndex"}));

            // Confirm that the $** index can still be used to query for the '_fts' field outside of
            // a $text query.
            assertAllPathsQuery({_fts: {$gt: 0, $lt: 4}}, '_fts');

            // Confirm that $** does not generate a candidate plan for $text search, including cases
            // when the query filter contains a compound field in the $text index.
            const textQuery =
                Object.assign(textIndex.a ? {a: 1} : {}, {$text: {$search: 'banana'}});
            let explainOut = assert.commandWorked(coll.find(textQuery).explain("executionStats"));
            assert(planHasStage(coll.getDB(), explainOut.queryPlanner.winningPlan, "TEXT"));
            assert.eq(explainOut.queryPlanner.rejectedPlans.length, 0);
            assert.eq(explainOut.executionStats.nReturned, 2);

            // Confirm that $** does not generate a candidate plan for $text search, including cases
            // where the query filter contains a field which is not present in the text index.
            explainOut =
                assert.commandWorked(coll.find(Object.assign({_fts: {$gt: 0, $lt: 4}}, textQuery))
                                         .explain("executionStats"));
            assert(planHasStage(coll.getDB(), explainOut.queryPlanner.winningPlan, "TEXT"));
            assert.eq(explainOut.queryPlanner.rejectedPlans.length, 0);
            assert.eq(explainOut.executionStats.nReturned, 2);

            // Confirm that the $** index can be used alongside a $text predicate in an $or.
            explainOut = assert.commandWorked(
                coll.find({$or: [{_fts: 3}, textQuery]}).explain("executionStats"));
            assert.eq(explainOut.queryPlanner.rejectedPlans.length, 0);
            assert.eq(explainOut.executionStats.nReturned, 3);

            const textOrAllPaths = getPlanStages(explainOut.queryPlanner.winningPlan, "OR").shift();
            assert.eq(textOrAllPaths.inputStages.length, 2);
            const textBranch = (textOrAllPaths.inputStages[0].stage === "TEXT" ? 0 : 1);
            const allPathsBranch = (textBranch + 1) % 2;
            assert.eq(textOrAllPaths.inputStages[textBranch].stage, "TEXT");
            assert.eq(textOrAllPaths.inputStages[allPathsBranch].stage, "IXSCAN");
            assert.eq(textOrAllPaths.inputStages[allPathsBranch].keyPattern, {$_path: 1, _fts: 1});

            // Drop the index so that a different text index can be created.
            assert.commandWorked(coll.dropIndex("textIndex"));
        }
    } finally {
        // Disable $** indexes once the tests have either completed or failed.
        assert.commandWorked(
            db.adminCommand({setParameter: 1, internalQueryAllowAllPathsIndexes: false}));
    }
})();