SERVER-121851 Handle empty pipelines on views in $rankFusion and $scoreFusion (#49860)

GitOrigin-RevId: 2174bf3f624044c535139925e58e12ba92d07550
This commit is contained in:
Daniel Segel 2026-03-18 20:50:45 -04:00 committed by MongoDB Bot
parent df69233f0a
commit c26523df06
3 changed files with 56 additions and 21 deletions

View File

@ -295,3 +295,19 @@ const runRankFusionViewTest = (testName, inputPipelines, viewPipeline, checkCorr
};
testHybridSearchViewWithSubsequentUnionOnDifferentView(rankFusionInputPipelines, createRankFusionPipeline);
})();
// $rankFusion with an empty input pipeline should trigger the correct error.
(function testRankFusionEmptyPipelineOnView() {
const collName = jsTestName() + "_empty_pipeline_coll";
const viewName = jsTestName() + "_empty_pipeline_view";
db[collName].drop();
assert.commandWorked(db.createView(viewName, collName, []));
assert.commandFailedWithCode(
db.runCommand({
aggregate: viewName,
pipeline: [{$rankFusion: {input: {pipelines: {p: []}}}}],
cursor: {},
}),
9834300,
);
})();

View File

@ -345,3 +345,19 @@ const runScoreFusionViewTest = (testName, inputPipelines, viewPipeline, checkCor
};
testHybridSearchViewWithSubsequentUnionOnDifferentView(scoreFusionInputPipelines, createScoreFusionPipeline);
})();
// $scoreFusion with an empty input pipeline should trigger the correct error.
(function testScoreFusionEmptyPipelineOnView() {
const collName = jsTestName() + "_empty_pipeline_coll";
const viewName = jsTestName() + "_empty_pipeline_view";
db[collName].drop();
assert.commandWorked(db.createView(viewName, collName, []));
assert.commandFailedWithCode(
db.runCommand({
aggregate: viewName,
pipeline: [{$scoreFusion: {input: {pipelines: {p: []}, normalization: "none"}}}],
cursor: {},
}),
9402503,
);
})();

View File

@ -157,28 +157,31 @@ inline bool isMongotPipeline(const std::shared_ptr<IncrementalFeatureRolloutCont
// ]
// }
// }
//}) will become the first firstStageBson in the final desugared ouput. Thus, there is an
// extra
// recursive call to check for this.
return isMongotPipeline(
ifrContext,
std::vector<BSONObj>{firstStageBson[DocumentSourceRankFusion::kStageName]
[RankFusionSpec::kInputFieldName]
[RankFusionInputSpec::kPipelinesFieldName]
.Obj()
.firstElement()
.Array()[0]
.Obj()});
//})
// will become the first firstStageBson in the final desugared ouput. Thus, there is an
// extra recursive call to check for this.
auto rankFusionFirstPipeline =
firstStageBson[DocumentSourceRankFusion::kStageName][RankFusionSpec::kInputFieldName]
[RankFusionInputSpec::kPipelinesFieldName]
.Obj()
.firstElement()
.Array();
if (rankFusionFirstPipeline.empty()) {
return false;
}
return isMongotPipeline(ifrContext, std::vector<BSONObj>{rankFusionFirstPipeline[0].Obj()});
} else if (is<DocumentSourceScoreFusion>(firstStageBson)) {
return isMongotPipeline(
ifrContext,
std::vector<BSONObj>{firstStageBson[DocumentSourceScoreFusion::kStageName]
[ScoreFusionSpec::kInputFieldName]
[ScoreFusionInputsSpec::kPipelinesFieldName]
.Obj()
.firstElement()
.Array()[0]
.Obj()});
auto scoreFusionFirstPipeline =
firstStageBson[DocumentSourceScoreFusion::kStageName][ScoreFusionSpec::kInputFieldName]
[ScoreFusionInputsSpec::kPipelinesFieldName]
.Obj()
.firstElement()
.Array();
if (scoreFusionFirstPipeline.empty()) {
return false;
}
return isMongotPipeline(ifrContext,
std::vector<BSONObj>{scoreFusionFirstPipeline[0].Obj()});
} else {
return is<DocumentSourceSearch>(firstStageBson) ||
is<DocumentSourceSearchMeta>(firstStageBson) ||