0
0
mirror of https://github.com/mongodb/mongo.git synced 2024-11-24 08:30:56 +01:00
mongodb/jstests/with_mongot/search_mocked/search_docsRequested.js
Nick Zolnierz 668a6f4e9e SERVER-94971 Add query ownership for files under jstests/libs (#27763)
GitOrigin-RevId: 1cd8a1cdb3d45876003ad3ccddd4d466cd9fb66c
2024-10-03 16:08:45 +00:00

1148 lines
44 KiB
JavaScript

/**
* Tests that if a query has an extractable limit, we send a search command to mongot with that
* information in the docsRequested field.
* All tests are skipped if featureFlagSearchBatchSizeTuning is enabled, since this file only tests
* the docsRequested options, whereas that flag enables the batchSize option.
* TODO SERVER-92576 Remove this test when featureFlagSearchBatchSizeLimit is removed.
* @tags: [requires_fcv_71]
*/
import {FeatureFlagUtil} from "jstests/libs/feature_flag_util.js";
import {checkSbeRestrictedOrFullyEnabled} from "jstests/libs/query/sbe_util.js";
import {getUUIDFromListCollections} from "jstests/libs/uuid_util.js";
import {
getDefaultProtocolVersionForPlanShardedSearch,
mockPlanShardedSearchResponse,
mongotCommandForQuery,
MongotMock
} from "jstests/with_mongot/mongotmock/lib/mongotmock.js";
import {
ShardingTestWithMongotMock
} from "jstests/with_mongot/mongotmock/lib/shardingtest_with_mongotmock.js";
const dbName = "test";
const collName = "search_docsrequested";
const chunkBoundary = 8;
const protocolVersion = getDefaultProtocolVersionForPlanShardedSearch();
const docs = [
{"_id": 1, "title": "cakes"},
{"_id": 2, "title": "cookies and cakes"},
{"_id": 3, "title": "vegetables"},
{"_id": 4, "title": "oranges"},
{"_id": 5, "title": "cakes and oranges"},
{"_id": 6, "title": "cakes and apples"},
{"_id": 7, "title": "apples"},
{"_id": 8, "title": "cakes and xyz"},
{"_id": 9, "title": "cakes and blueberries"},
{"_id": 10, "title": "cakes and strawberries"},
{"_id": 11, "title": "cakes and raspberries"},
{"_id": 12, "title": "cakes and cakes"},
{"_id": 13, "title": "cakes and elderberries"},
{"_id": 14, "title": "cakes and carrots"},
{"_id": 15, "title": "cakes and more cakes"},
{"_id": 16, "title": "cakes and even more cakes"},
];
const foreignCollectionDocs = [
{"_id": 1, "fruit": "raspberries"},
{"_id": 2, "fruit": "blueberries"},
{"_id": 3, "fruit": "strawberries"},
{"_id": 4, "fruit": "gooseberries"},
{"_id": 5, "fruit": "mangos"},
];
const foreignCollName = "fruits";
const foreignChunkBoundary = 3;
const searchQuery = {
query: "cakes",
path: "title"
};
// All the documents that would be returned by the search query above.
let relevantDocs = [];
let relevantSearchDocs = [];
let relevantSearchDocsShard0 = [];
let relevantSearchDocsShard1 = [];
let searchScore = 0.300;
for (let i = 0; i < docs.length; i++) {
if (docs[i]["title"].includes(searchQuery.query)) {
relevantDocs.push(docs[i]);
// Standalone case.
relevantSearchDocs.push({_id: docs[i]._id, $searchScore: searchScore});
// Sharded environment case.
if (docs[i]._id < chunkBoundary) {
relevantSearchDocsShard0.push({_id: docs[i]._id, $searchScore: searchScore});
} else {
relevantSearchDocsShard1.push({_id: docs[i]._id, $searchScore: searchScore});
}
// The documents with lower _id will have a higher search score.
searchScore = searchScore - 0.001;
}
}
assert.eq(13, relevantSearchDocs.length);
assert.eq(4, relevantSearchDocsShard0.length);
assert.eq(9, relevantSearchDocsShard1.length);
// Mongot may return slightly more documents than mongod requests as an optimization for the case
// when $idLookup filters out some of them.
function calcNumDocsMongotShouldReturn(extractedLimit) {
return Math.max(Math.ceil(1.064 * extractedLimit), 10);
}
function buildHistoryStandalone(coll, collUUID, extractedLimit, mongotConn) {
let mongotReturnedDocs = calcNumDocsMongotShouldReturn(extractedLimit);
{
const cursorId = NumberLong(123);
const history = [
{
expectedCommand: mongotCommandForQuery({
query: searchQuery,
collName: collName,
db: dbName,
collectionUUID: collUUID,
cursorOptions: {docsRequested: NumberInt(extractedLimit)}
}),
response: {
cursor: {
id: NumberLong(0),
ns: coll.getFullName(),
nextBatch: relevantSearchDocs.slice(0, mongotReturnedDocs),
},
ok: 1
}
},
];
assert.commandWorked(
mongotConn.adminCommand({setMockResponses: 1, cursorId: cursorId, history: history}));
}
}
function buildHistoryShardedEnv(coll, collUUID, extractedLimit, stWithMock) {
let mongotReturnedDocs = calcNumDocsMongotShouldReturn(extractedLimit);
{
const cursorId = NumberLong(123);
const metaId = NumberLong(2);
// Set history for shard 0.
const history0 = [
{
expectedCommand: mongotCommandForQuery({
query: searchQuery,
collName: collName,
db: dbName,
collectionUUID: collUUID,
protocolVersion: protocolVersion,
cursorOptions: {docsRequested: NumberInt(extractedLimit)}
}),
response: {
cursors: [
{
cursor: {
id: NumberLong(0),
type: "results",
ns: coll.getFullName(),
nextBatch: relevantSearchDocsShard0.slice(0, mongotReturnedDocs),
},
ok: 1
},
{
cursor: {
id: NumberLong(0),
ns: coll.getFullName(),
type: "meta",
nextBatch: [{metaVal: 1}],
},
ok: 1
}
]
}
},
];
const s0Mongot = stWithMock.getMockConnectedToHost(stWithMock.st.rs0.getPrimary());
s0Mongot.setMockResponses(history0, cursorId, metaId);
// Set history for shard 1.
const history1 = [
{
expectedCommand: mongotCommandForQuery({
query: searchQuery,
collName: collName,
db: dbName,
collectionUUID: collUUID,
protocolVersion: protocolVersion,
cursorOptions: {docsRequested: NumberInt(extractedLimit)}
}),
response: {
cursors: [
{
cursor: {
id: NumberLong(0),
type: "results",
ns: coll.getFullName(),
nextBatch: relevantSearchDocsShard1.slice(0, mongotReturnedDocs),
},
ok: 1
},
{
cursor: {
id: NumberLong(0),
ns: coll.getFullName(),
type: "meta",
nextBatch: [{metaVal: 2}],
},
ok: 1
}
]
}
},
];
const s1Mongot = stWithMock.getMockConnectedToHost(stWithMock.st.rs1.getPrimary());
s1Mongot.setMockResponses(history1, cursorId, metaId);
mockPlanShardedSearchResponse(
collName, searchQuery, dbName, undefined /*sortSpec*/, stWithMock);
}
}
function runAndAssert(
pipeline, extractedLimit, expectedResults, coll, collUUID, standaloneConn, stConn) {
// Only one of standaloneConn and stConn can be non-null.
if (standaloneConn != null) {
assert(stConn == null);
buildHistoryStandalone(coll, collUUID, extractedLimit, standaloneConn);
} else {
assert(standaloneConn == null);
buildHistoryShardedEnv(coll, collUUID, extractedLimit, stConn);
}
let cursor = coll.aggregate(pipeline);
assert.eq(expectedResults, cursor.toArray());
}
// The extractable limit optimization cannot be done if there is a stage between $search and
// $limit that would change the number of documents, such as $match. Thus, there should be no
// 'docsRequested' field in the command sent to mongot in this case.
function expectNoDocsRequestedInCommand(coll, collUUID, mongotConn, stWithMock) {
let pipeline = [{$search: searchQuery}, {$match: {title: {$regex: "more cakes"}}}, {$limit: 1}];
let cursorId = NumberLong(123);
// Only one of mongotConn and stWithMock can be non-null.
if (mongotConn != null) {
assert(stWithMock == null);
const history = [
{
expectedCommand: {
search: coll.getName(),
collectionUUID: collUUID,
query: searchQuery,
$db: dbName,
},
response: {
cursor: {
id: NumberLong(0),
ns: coll.getFullName(),
nextBatch: [
{_id: 15, $searchScore: 0.789},
{_id: 16, $searchScore: 0.123},
]
},
ok: 1
}
},
];
assert.commandWorked(
mongotConn.adminCommand({setMockResponses: 1, cursorId: cursorId, history: history}));
} else {
assert(mongotConn == null);
const metaId = NumberLong(2);
// Set history for shard 0.
const history0 = [
{
expectedCommand: {
search: coll.getName(),
collectionUUID: collUUID,
query: searchQuery,
$db: dbName,
intermediate: protocolVersion
},
response: {
cursors: [
{
cursor: {
id: NumberLong(0),
type: "results",
ns: coll.getFullName(),
nextBatch: [],
},
ok: 1
},
{
cursor: {
id: NumberLong(0),
ns: coll.getFullName(),
type: "meta",
nextBatch: [{metaVal: 1}],
},
ok: 1
}
]
}
},
];
const s0Mongot = stWithMock.getMockConnectedToHost(stWithMock.st.rs0.getPrimary());
s0Mongot.setMockResponses(history0, cursorId, metaId);
// Set history for shard 1.
const history1 = [
{
expectedCommand: {
search: coll.getName(),
collectionUUID: collUUID,
query: searchQuery,
$db: dbName,
intermediate: protocolVersion
},
response: {
cursors: [
{
cursor: {
id: NumberLong(0),
type: "results",
ns: coll.getFullName(),
nextBatch: [
{_id: 15, $searchScore: 0.789},
{_id: 16, $searchScore: 0.123},
]
},
ok: 1
},
{
cursor: {
id: NumberLong(0),
ns: coll.getFullName(),
type: "meta",
nextBatch: [{metaVal: 1}],
},
ok: 1
}
]
}
},
];
const s1Mongot = stWithMock.getMockConnectedToHost(stWithMock.st.rs1.getPrimary());
s1Mongot.setMockResponses(history1, cursorId, metaId);
mockPlanShardedSearchResponse(
collName, searchQuery, dbName, undefined /*sortSpec*/, stWithMock);
}
let cursor = coll.aggregate(pipeline);
const expected = [
{"_id": 15, "title": "cakes and more cakes"},
];
assert.eq(expected, cursor.toArray());
}
// Perform a $search query where $$SEARCH_META is referenced after a $limit stage.
function searchMetaAfterLimit(coll, collUUID, stWithMock) {
let st = stWithMock.st;
let limit = 3;
let mongotReturnedDocs = calcNumDocsMongotShouldReturn(limit);
let pipeline =
[{$search: searchQuery}, {$limit: limit}, {$project: {_id: 1, meta: "$$SEARCH_META"}}];
let expected = relevantDocs.slice(0, limit);
// Modify the expected documents to reflect the $project stage in the pipeline.
for (let i = 0; i < expected.length; i++) {
expected[i] = {_id: expected[i]["_id"], meta: {value: 0}};
}
// Set history for shard 0.
{
const resultsID = NumberLong(11);
const metaID = NumberLong(12);
const historyResults = [
{
expectedCommand: mongotCommandForQuery({
query: searchQuery,
collName: collName,
db: dbName,
collectionUUID: collUUID,
protocolVersion: protocolVersion,
cursorOptions: {docsRequested: NumberInt(limit)}
}),
response: {
ok: 1,
cursors: [
{
cursor: {
id: NumberLong(0),
type: "results",
ns: coll.getFullName(),
nextBatch: relevantSearchDocsShard0.slice(0, mongotReturnedDocs)
},
ok: 1
},
{
cursor: {
id: NumberLong(0),
ns: coll.getFullName(),
type: "meta",
nextBatch: [{value: 0}],
},
ok: 1
}
]
}
},
];
const mongot = stWithMock.getMockConnectedToHost(st.rs0.getPrimary());
mongot.setMockResponses(historyResults, resultsID, metaID);
}
// Set history for shard 1.
{
const resultsID = NumberLong(21);
const metaID = NumberLong(22);
const historyResults = [
{
expectedCommand: mongotCommandForQuery({
query: searchQuery,
collName: collName,
db: dbName,
collectionUUID: collUUID,
protocolVersion: protocolVersion,
cursorOptions: {docsRequested: NumberInt(limit)}
}),
response: {
ok: 1,
cursors: [
{
cursor: {
id: NumberLong(0),
type: "results",
ns: coll.getFullName(),
nextBatch: relevantSearchDocsShard1.slice(0, mongotReturnedDocs)
},
ok: 1
},
{
cursor: {
id: NumberLong(0),
ns: coll.getFullName(),
type: "meta",
nextBatch: [{value: 0}],
},
ok: 1
}
]
}
},
];
const mongot = stWithMock.getMockConnectedToHost(st.rs1.getPrimary());
mongot.setMockResponses(historyResults, resultsID, metaID);
}
// Set history for mongos.
{
const mergingPipelineHistory = [{
expectedCommand: {
planShardedSearch: collName,
query: searchQuery,
$db: dbName,
searchFeatures: {shardedSort: 1}
},
response: {
ok: 1,
protocolVersion: NumberInt(1),
// This does not represent an actual merging pipeline. The merging pipeline is
// arbitrary, it just must only generate one document.
metaPipeline: [{$limit: 1}]
}
}];
const mongot = stWithMock.getMockConnectedToHost(stWithMock.st.s);
mongot.setMockResponses(mergingPipelineHistory, 1);
}
let cursor = coll.aggregate(pipeline);
assert.eq(expected, cursor.toArray());
}
function buildHistorySearchWithinLookupStandalone(db, mongotConn, searchLookupQuery, numBerries) {
let foreignColl = db.getCollection(foreignCollName);
assert.commandWorked(foreignColl.insertMany(foreignCollectionDocs));
let foreignCollUUID = getUUIDFromListCollections(db, foreignCollName);
{
const history = [
{
expectedCommand: {
search: foreignCollName,
collectionUUID: foreignCollUUID,
query: searchLookupQuery,
$db: dbName,
cursorOptions: {docsRequested: numBerries},
},
response: {
cursor: {
id: NumberLong(0),
ns: foreignColl.getFullName(),
nextBatch: [
{"_id": 1, "$searchScore": 0.300},
{"_id": 2, "$searchScore": 0.299},
{"_id": 3, "$searchScore": 0.298},
{"_id": 4, "$searchScore": 0.297},
// We set mongotmock to return 4 documents here because of the
// oversubscription that mongot would do (mongot would want to return 10
// documents in this case because 3 * 1.064 < 10, but there are only 4
// that satisfy the query so we return those 4).
],
},
ok: 1
}
},
];
// Only one response is needed, as $lookup executes $search once and caches the response.
assert.commandWorked(mongotConn.adminCommand(
{setMockResponses: 1, cursorId: NumberLong(123), history: history}));
}
}
function buildHistorySearchWithinLookupShardedEnv(db, stWithMock, searchLookupQuery, numBerries) {
let st = stWithMock.st;
let foreignColl = db.getCollection(foreignCollName);
assert.commandWorked(foreignColl.insertMany(foreignCollectionDocs));
let foreignCollUUID =
getUUIDFromListCollections(st.rs0.getPrimary().getDB(dbName), foreignCollName);
// Shard the foreign collection for the $lookup test and move the higher chunk to shard1.
st.shardColl(
foreignColl, {_id: 1}, {_id: foreignChunkBoundary}, {_id: foreignChunkBoundary + 1});
const planShardedSearchHistory = [{
expectedCommand: {
planShardedSearch: foreignCollName,
query: searchLookupQuery,
$db: dbName,
searchFeatures: {shardedSort: 1}
},
response: {ok: 1, protocolVersion: NumberInt(1), metaPipeline: [{$limit: 1}]}
}];
function history(cursorId, docsToReturn) {
// We will set mongotmock to return 2 of the documents given in the first batch, and the
// rest as a response to the expected getMore.
let docsInFirstBatch = 2;
assert(docsToReturn.length >= docsInFirstBatch);
return [
{
expectedCommand: {
search: foreignCollName,
collectionUUID: foreignCollUUID,
query: searchLookupQuery,
$db: dbName,
cursorOptions: {docsRequested: numBerries},
intermediate: protocolVersion
},
response: {
cursors: [
{
cursor: {
id: NumberLong(cursorId),
type: "results",
ns: foreignColl.getFullName(),
nextBatch: docsToReturn.slice(0, 2),
},
ok: 1
},
{
cursor: {
id: NumberLong(0),
ns: foreignColl.getFullName(),
type: "meta",
nextBatch: [{metaVal: 1}],
},
ok: 1
}
]
}
},
{
expectedCommand: {
getMore: NumberLong(cursorId),
collection: foreignCollName,
cursorOptions: {docsRequested: numBerries - docsInFirstBatch}
},
response: {
cursor: {
id: NumberLong(0),
ns: foreignColl.getFullName(),
nextBatch: docsToReturn.slice(2),
},
ok: 1
}
},
];
}
const s0Mongot = stWithMock.getMockConnectedToHost(stWithMock.st.rs0.getPrimary());
const s1Mongot = stWithMock.getMockConnectedToHost(stWithMock.st.rs1.getPrimary());
// We need a new cursorId for each setMockResponses below because calling setMockResponses with
// the same cursorId twice overwrites the first mock response.
let cursorId = 123;
let metaId = 2;
// Each shard will invoke PSS during execution of $search in $lookup. Only one response is
// necessary since $search is executed once and cached.
// Mock the responses for the commands resulting from parsing the local collection documents on
// shard0.
s0Mongot.setMockResponses(planShardedSearchHistory, NumberLong(cursorId++));
// Mock the responses for the commands resulting from parsing the local collection
// documents on shard1.
s1Mongot.setMockResponses(planShardedSearchHistory, NumberLong(cursorId++));
// As part of $lookup execution, each shard will send a search command to itself and to the
// other shard. Only occurs once per shard as $search is executed once and cached.
for (let i = 0; i < 2; i++) {
s0Mongot.setMockResponses(
history(cursorId, [{_id: 1, $searchScore: 0.3}, {_id: 2, $searchScore: 0.299}]),
NumberLong(cursorId++),
NumberLong(metaId++));
s1Mongot.setMockResponses(
history(cursorId, [{_id: 3, $searchScore: 0.298}, {_id: 4, $searchScore: 0.297}]),
NumberLong(cursorId++),
NumberLong(metaId++));
}
stWithMock.getMockConnectedToHost(stWithMock.st.rs0.getPrimary()).disableOrderCheck();
stWithMock.getMockConnectedToHost(stWithMock.st.rs1.getPrimary()).disableOrderCheck();
}
function testSearchWithinLookup(db, coll, mongotConn, stWithMock) {
// The $lookup subpipeline produces one document of the form {three_berries: {berries: [...]}}
// where the contents of the array are the first 3 fruits (3 from the limit stage) in the
// foreign collection documents that contain the substring "berries".
// Each document in the local collection has this document produced from the subpipeline after
// the $lookup stage. The $unwind stages unwind the array in that document. The $match stage
// filters out documents where the title field does not contain any of the first 3 berries as a
// substring. Thus, we are left with documents in the local collection where the titles contain
// one of the first 3 berries from the foreign collection documents.
const searchLookupQuery = {query: "berries", path: "fruit"};
const numBerries = 3;
const expected = [
{"title": "cakes and blueberries"},
{"title": "cakes and strawberries"},
{"title": "cakes and raspberries"},
];
const pipeline = [
{
$lookup: {
from: foreignCollName,
pipeline: [
{$search: searchLookupQuery},
{$limit: numBerries},
{$group: {_id: null, berries: {$addToSet: "$fruit"}}},
{$project: {_id: 0}}
],
as: "three_berries"
}
},
{$unwind: {path: "$three_berries"}},
{$unwind: {path: "$three_berries.berries"}},
{$match: {$expr: {$ne: [{$indexOfCP: ["$title", "$three_berries.berries"]}, -1]}}},
{$project: {_id: 0, title: 1}}
];
// Exactly one of mongotConn and stWithMock is expected to be null.
if (mongotConn != null) {
assert(stWithMock == null);
buildHistorySearchWithinLookupStandalone(db, mongotConn, searchLookupQuery, numBerries);
} else {
assert(mongotConn == null);
buildHistorySearchWithinLookupShardedEnv(db, stWithMock, searchLookupQuery, numBerries);
}
let cursor = coll.aggregate(pipeline);
assert.eq(expected, cursor.toArray());
}
// Perform a $search query where a getMore is required.
function getMoreCaseBuildHistoryStandalone(coll, collUUID, mongotConn, limitVal, orphanDocs) {
const cursorId = NumberLong(123);
// This tests that mongod will getMore thrice as necessary to obtain all the relevent documents.
// There are 13 total documents that should be returned by the search query and the limit in the
// query is 15. We define the first batch returned by mongot to be 10 of the documents actually
// in the collection and 6 documents that will not go through the $idLookup stage because they
// don't exist in the collection (this simulates the case where the mongot index of the data is
// stale and some documents have been deleted from the collection but the index has not yet been
// updated to reflect this). 6 comes from the oversubscription that mongot will do (limitVal
// * 1.064, rounded up, minus the 10 real documents). This will cause mongod to getMore with 5
// documents since only 10 in the first batch were valid. To this, mongot will return 10
// documents (as that is the minumum number of documents mongot returns for a query with an
// extractable limit), but all of these will be more orphan documents that aren't in the
// collection. mongod will send another getMore for 5 documents and to this mongot will return a
// batch with only 1 valid document (to exercise the case where mongot returns fewer documents
// than requested, but there are still more to be returned. This could happen if the batch
// exceeds the 16MB limit.) mongod will send another getMore for 4 documents and to this mongot
// will send the remaining 2 documents in the collection that satisfy the search query.
const batch1 = relevantSearchDocs.slice(0, 10).concat(orphanDocs.slice(0, 6));
const batch2 = orphanDocs.slice(6);
const batch3 = relevantSearchDocs.slice(10, 11);
const batch4 = relevantSearchDocs.slice(11);
const history = [
{
expectedCommand: mongotCommandForQuery({
query: searchQuery,
collName: collName,
db: dbName,
collectionUUID: collUUID,
cursorOptions: {docsRequested: NumberInt(limitVal)}
}),
response: {
cursor: {
id: cursorId,
ns: coll.getFullName(),
nextBatch: batch1,
},
ok: 1
}
},
{
expectedCommand:
{getMore: cursorId, collection: coll.getName(), cursorOptions: {docsRequested: 5}},
response: {
ok: 1,
cursor: {
id: cursorId,
ns: coll.getFullName(),
nextBatch: batch2,
},
}
},
{
expectedCommand:
{getMore: cursorId, collection: coll.getName(), cursorOptions: {docsRequested: 5}},
response: {
ok: 1,
cursor: {
id: cursorId,
ns: coll.getFullName(),
nextBatch: batch3,
},
}
},
{
expectedCommand:
{getMore: cursorId, collection: coll.getName(), cursorOptions: {docsRequested: 4}},
response: {
ok: 1,
cursor: {
id: NumberLong(0), // We have exhausted the cursor.
ns: coll.getFullName(),
nextBatch: batch4,
},
}
},
];
assert.commandWorked(
mongotConn.adminCommand({setMockResponses: 1, cursorId: cursorId, history: history}));
}
function getMoreCaseBuildHistoryShardedEnv(coll, collUUID, stWithMock, limitVal, orphanDocs) {
const cursorId = NumberLong(123);
const metaId = NumberLong(2);
// This is a similar situation to the standlone case.
// For the first batch, mongot will return 10 total real documents to the shards (3 for shard0
// and 7 for shard1) and 6 total documents that are not in the collection (3 for each shard).
const batch1shard0 = relevantSearchDocsShard0.slice(0, 3).concat(orphanDocs.slice(0, 3));
const batch1shard1 = relevantSearchDocsShard1.slice(0, 7).concat(orphanDocs.slice(3, 6));
// The amount of documents that each shard will request in the getMore will be the difference
// between the limit in the pipeline and the number of valid documents each shard got back from
// the first batch.
const docsRequestedShard0 = limitVal - 3;
const docsRequestedShard1 = limitVal - 7;
// For the second batch, mongot will return 5 orphan documents to each shard.
const batch2shard0 = orphanDocs.slice(6, 11);
const batch2shard1 = orphanDocs.slice(11);
// For the third batch, mongot will return 1 of the documents on shard 1.
const batch3shard0 = [];
const batch3shard1 = relevantSearchDocsShard1.slice(7, 8);
// For the third batch, mongot will return the last remaining document on shard0 and the last
// remaining document on shard1.
const batch4shard0 = relevantSearchDocsShard0.slice(3);
const batch4shard1 = relevantSearchDocsShard1.slice(8);
// Set history for shard 0.
const history0 = [
{
expectedCommand: mongotCommandForQuery({
query: searchQuery,
collName: collName,
db: dbName,
collectionUUID: collUUID,
protocolVersion: protocolVersion,
cursorOptions: {docsRequested: limitVal}
}),
response: {
cursors: [
{
cursor: {
id: cursorId,
type: "results",
ns: coll.getFullName(),
nextBatch: batch1shard0,
},
ok: 1
},
{
cursor: {
id: NumberLong(0),
ns: coll.getFullName(),
type: "meta",
nextBatch: [{metaVal: 1}],
},
ok: 1
}
]
}
},
{
expectedCommand: {
getMore: cursorId,
collection: coll.getName(),
cursorOptions: {docsRequested: docsRequestedShard0}
},
response: {
ok: 1,
cursor: {
id: cursorId,
ns: coll.getFullName(),
nextBatch: batch2shard0,
},
}
},
{
expectedCommand: {
getMore: cursorId,
collection: coll.getName(),
cursorOptions: {docsRequested: docsRequestedShard0}
},
response: {
ok: 1,
cursor: {
id: cursorId,
ns: coll.getFullName(),
nextBatch: batch3shard0,
},
}
},
{
expectedCommand: {
getMore: cursorId,
collection: coll.getName(),
// Since the previous batch returned no documents for this shard, this docsRequested
// value will be the same as the previous.
cursorOptions: {docsRequested: docsRequestedShard0}
},
response: {
ok: 1,
cursor: {
id: NumberLong(0), // We have exhausted the cursor.
ns: coll.getFullName(),
nextBatch: batch4shard0,
},
}
},
];
const s0Mongot = stWithMock.getMockConnectedToHost(stWithMock.st.rs0.getPrimary());
s0Mongot.setMockResponses(history0, cursorId, metaId);
// Set history for shard 1.
const history1 = [
{
expectedCommand: mongotCommandForQuery({
query: searchQuery,
collName: collName,
db: dbName,
collectionUUID: collUUID,
protocolVersion: protocolVersion,
cursorOptions: {docsRequested: NumberInt(limitVal)}
}),
response: {
cursors: [
{
cursor: {
id: cursorId,
type: "results",
ns: coll.getFullName(),
nextBatch: batch1shard1,
},
ok: 1
},
{
cursor: {
id: NumberLong(0),
ns: coll.getFullName(),
type: "meta",
nextBatch: [{metaVal: 1}],
},
ok: 1
}
]
}
},
{
expectedCommand: {
getMore: cursorId,
collection: coll.getName(),
cursorOptions: {docsRequested: docsRequestedShard1}
},
response: {
ok: 1,
cursor: {
id: cursorId,
ns: coll.getFullName(),
nextBatch: batch2shard1,
},
}
},
{
expectedCommand: {
getMore: cursorId,
collection: coll.getName(),
cursorOptions: {docsRequested: docsRequestedShard1}
},
response: {
ok: 1,
cursor: {
id: cursorId,
ns: coll.getFullName(),
nextBatch: batch3shard1,
},
}
},
{
expectedCommand: {
getMore: cursorId,
collection: coll.getName(),
// Since the previous batch returned one valid document for this shard, this
// docsRequested value will be one less than the previous.
cursorOptions: {docsRequested: docsRequestedShard1 - 1}
},
response: {
ok: 1,
cursor: {
id: NumberLong(0), // We have exhausted the cursor.
ns: coll.getFullName(),
nextBatch: batch4shard1,
},
}
},
];
const s1Mongot = stWithMock.getMockConnectedToHost(stWithMock.st.rs1.getPrimary());
s1Mongot.setMockResponses(history1, cursorId, metaId);
mockPlanShardedSearchResponse(
collName, searchQuery, dbName, undefined /*sortSpec*/, stWithMock);
}
// Perform a $search query where a getMore is required.
function getMoreCase(coll, collUUID, standaloneConn, stConn) {
const limitVal = 15;
const pipeline = [{$search: searchQuery}, {$limit: limitVal}];
// Construct 16 fake documents that aren't in the collection for mongot to return (mimicking an
// out-of-date index) that will not pass the idLookup stage.
let orphanDocs = [];
for (let i = 20; i < 36; i++) {
orphanDocs.push({_id: i, $searchScore: 0.3});
}
// Exactly one of standaloneConn and stConn is execpted to be null.
if (standaloneConn != null) {
assert(stConn == null);
getMoreCaseBuildHistoryStandalone(coll, collUUID, standaloneConn, limitVal, orphanDocs);
} else {
assert(standaloneConn == null);
getMoreCaseBuildHistoryShardedEnv(coll, collUUID, stConn, limitVal, orphanDocs);
}
let cursor = coll.aggregate(pipeline);
// All relevant documents are expected to be returned by the query.
assert.eq(relevantDocs, cursor.toArray());
}
function runTest(db, collUUID, standaloneConn, stConn) {
let coll = db.getCollection(collName);
function runSearchQueries(limitVal, otherLimitVal, skipVal) {
// Perform a $search query with a limit stage.
let pipeline = [{$search: searchQuery}, {$limit: limitVal}];
// The extracted limit here comes from the limit value in the pipeline.
let expected = relevantDocs.slice(0, limitVal);
runAndAssert(pipeline, limitVal, expected, coll, collUUID, standaloneConn, stConn);
// Perform a $search query with a $skip followed by $limit.
pipeline = [{$search: searchQuery}, {$skip: skipVal}, {$limit: limitVal}];
// The extracted limit here comes from the sum of the limit and skip values in the pipeline.
expected = relevantDocs.slice(skipVal).slice(0, limitVal);
runAndAssert(
pipeline, limitVal + skipVal, expected, coll, collUUID, standaloneConn, stConn);
// Perform a $search query with multiple limit stages.
pipeline = [{$search: searchQuery}, {$limit: limitVal}, {$limit: otherLimitVal}];
// The extracted limit here comes from the minimum of the two limit values in the pipeline.
expected = relevantDocs.slice(0, Math.min(limitVal, otherLimitVal));
runAndAssert(pipeline,
Math.min(limitVal, otherLimitVal),
expected,
coll,
collUUID,
standaloneConn,
stConn);
// Perform a $search query with a limit and multiple skip stages.
pipeline = [{$search: searchQuery}, {$skip: skipVal}, {$skip: skipVal}, {$limit: limitVal}];
// The extracted limit here comes from the value of the limit plus the values of the two
// skip stages in the pipeline.
expected = relevantDocs.slice(skipVal + skipVal).slice(0, limitVal);
runAndAssert(pipeline,
skipVal + skipVal + limitVal,
expected,
coll,
collUUID,
standaloneConn,
stConn);
// Perform a $search query with multiple limit stages and multiple skip stages.
pipeline = [
{$search: searchQuery},
{$skip: skipVal},
{$skip: skipVal},
{$limit: limitVal},
{$limit: otherLimitVal}
];
// The extracted limit here comes from the minimum of the two limit values plus the values
// of the two skip stages in the pipeline.
expected =
relevantDocs.slice(skipVal + skipVal).slice(0, Math.min(limitVal, otherLimitVal));
runAndAssert(pipeline,
skipVal + skipVal + Math.min(limitVal, otherLimitVal),
expected,
coll,
collUUID,
standaloneConn,
stConn);
}
// Run the search queries with limit and skip values such that mongod will extract a user limit
// of less than 10, which means we will exercise the branch where mongot returns a minimum of 10
// documents.
runSearchQueries(3 /* limitVal */, 2 /* otherLimitVal */, 1 /* skipVal */);
// Run the search queries with limit and skip values such that mongod will extract a user limit
// of greater than 10, which means we will exercise the branch where mongot the extracted limit
// multiplied by the oversubscription factor.
runSearchQueries(11 /* limitVal */, 10 /* otherLimitVal */, 1 /* skipVal */);
expectNoDocsRequestedInCommand(coll, collUUID, standaloneConn, stConn);
// SERVER-80648 $search in SBE doesn't support the batch size optimization, so skip the tests.
if (!(checkSbeRestrictedOrFullyEnabled(db) &&
FeatureFlagUtil.isPresentAndEnabled(db.getMongo(), 'SearchInSbe'))) {
// Tests that getMore has a correct cursorOptions field.
getMoreCase(coll, collUUID, standaloneConn, stConn);
testSearchWithinLookup(db, coll, standaloneConn, stConn);
}
// Test that the docsRequested field makes it to the shards in a sharded environment when
// $$SEARCH_META is referenced in the query.
if (stConn != null) {
searchMetaAfterLimit(coll, collUUID, stConn);
}
}
function setupAndRunTestStandalone() {
const mongotmock = new MongotMock();
mongotmock.start();
const mongotConn = mongotmock.getConnection();
const conn = MongoRunner.runMongod({setParameter: {mongotHost: mongotConn.host}});
let db = conn.getDB(dbName);
if (FeatureFlagUtil.isPresentAndEnabled(db.getMongo(), 'SearchBatchSizeTuning')) {
jsTestLog("Skipping the test because it only applies when batchSize isn't enabled.");
} else {
let coll = db.getCollection(collName);
// Insert documents.
assert.commandWorked(coll.insertMany(docs));
let collUUID = getUUIDFromListCollections(db, collName);
runTest(db, collUUID, mongotConn, null /* stConn */);
}
MongoRunner.stopMongod(conn);
mongotmock.stop();
}
function setupAndRunTestShardedEnv() {
const stWithMock = new ShardingTestWithMongotMock({
name: "search_docsrequested",
shards: {
rs0: {nodes: 2},
rs1: {nodes: 2},
},
mongos: 1,
other: {
rsOptions: {setParameter: {enableTestCommands: 1}},
}
});
stWithMock.start();
let st = stWithMock.st;
let mongos = st.s;
let db = mongos.getDB(dbName);
if (FeatureFlagUtil.isPresentAndEnabled(db.getMongo(), 'SearchBatchSizeTuning')) {
jsTestLog("Skipping the test because it only applies when batchSize isn't enabled.");
} else {
assert.commandWorked(mongos.getDB("admin").runCommand(
{enableSharding: dbName, primaryShard: st.shard0.name}));
let coll = db.getCollection(collName);
// Insert documents.
assert.commandWorked(coll.insertMany(docs));
// Shard the collection, split it at {_id: chunkBoundary}, and move the higher chunk to
// shard1.
st.shardColl(coll, {_id: 1}, {_id: chunkBoundary}, {_id: chunkBoundary + 1});
let collUUID = getUUIDFromListCollections(st.rs0.getPrimary().getDB(dbName), collName);
runTest(db, collUUID, null /* standaloneConn */, stWithMock);
}
stWithMock.stop();
}
// Test standalone.
setupAndRunTestStandalone();
// Test sharded cluster.
setupAndRunTestShardedEnv();