fix: preserve filter bitset when MVCC is disabled (#49736)

issue: #49735

- Preserve upstream scalar filter bitsets when
`common.visibilityFilterEnabled=false`.
- Only mark all rows visible in the disabled visibility-filter path when
`MvccNode` has no upstream filter input.
- Add C++ and Go regression coverage for preserving predicates/filter
bitsets in this path.

Signed-off-by: Shawn Wang <shawn.wang@zilliz.com>
This commit is contained in:
sparknack
2026-05-13 10:52:11 +08:00
committed by GitHub
parent 52ed8a6603
commit ac87f5a2fa
3 changed files with 66 additions and 2 deletions
+4 -2
View File
@@ -82,7 +82,9 @@ PhyMvccNode::GetOutput() {
TargetBitmap(active_count_),
TargetBitmap(active_count_))
: GetColumnVector(input_);
query_context->set_all_rows_visible(true);
if (is_source_node_) {
query_context->set_all_rows_visible(true);
}
is_finished_ = true;
return std::make_shared<RowVector>(std::vector<VectorPtr>{col_input});
}
@@ -133,4 +135,4 @@ PhyMvccNode::IsFinished() {
}
} // namespace exec
} // namespace milvus
} // namespace milvus
@@ -17,6 +17,8 @@
#include "common/Schema.h"
#include "exec/QueryContext.h"
#include "exec/Task.h"
#include "expr/ITypeExpr.h"
#include "pb/plan.pb.h"
#include "plan/PlanNode.h"
#include "segcore/SegcoreConfig.h"
#include "segcore/SegmentSealed.h"
@@ -368,6 +370,59 @@ TEST_F(MvccFastPathTest, VisibilityFilterDisabled_DeletesIgnored) {
<< "visibilityFilterEnabled=false should not mask deleted rows";
}
// ---------------------------------------------------------------------------
// visibilityFilterEnabled=false must not make VectorSearchNode skip an upstream
// scalar filter bitset.
// ---------------------------------------------------------------------------
TEST_F(MvccFastPathTest, VisibilityFilterDisabled_PreservesUpstreamFilter) {
VisibilityFilterGuard guard(false);
auto segment = CreateSealedSegment();
proto::plan::GenericValue value;
value.set_int64_val(N_ / 2);
auto expr = std::make_shared<expr::UnaryRangeFilterExpr>(
expr::ColumnInfo(int64_fid_, DataType::INT64),
proto::plan::OpType::LessThan,
value,
std::vector<proto::plan::GenericValue>{});
auto filter_node = std::make_shared<plan::FilterBitsNode>("filter_1", expr);
auto mvcc_node = std::make_shared<plan::MvccNode>(
"mvcc_1", std::vector<plan::PlanNodePtr>{filter_node});
auto plan = plan::PlanFragment(mvcc_node);
auto query_context = std::make_shared<QueryContext>(
"test_vis_disabled_with_filter",
segment.get(),
N_,
MAX_TIMESTAMP,
0,
0,
query::PlanOptions{false},
std::make_shared<QueryConfig>(
std::unordered_map<std::string, std::string>{}));
auto task =
Task::Create("task_vis_disabled_with_filter", plan, 0, query_context);
RowVectorPtr output;
for (;;) {
auto current = task->Next();
if (!current) {
break;
}
output = current;
}
EXPECT_FALSE(query_context->get_all_rows_visible())
<< "upstream scalar filter bitset must still be passed downstream";
ASSERT_NE(output, nullptr);
auto col = std::dynamic_pointer_cast<ColumnVector>(output->child(0));
ASSERT_NE(col, nullptr);
TargetBitmapView view(col->GetRawData(), col->size());
EXPECT_GT(view.count(), 0)
<< "upstream scalar filter should still filter out some rows";
}
// ---------------------------------------------------------------------------
// visibilityFilterEnabled=false on growing segment: still skips all filtering
// ---------------------------------------------------------------------------
@@ -623,6 +623,13 @@ func (s *TwoStageSearchSuite) TestTwoStageSearch() {
worker1.EXPECT().SearchSegments(mock.Anything, mock.AnythingOfType("*querypb.SearchRequest")).
Run(func(_ context.Context, req *querypb.SearchRequest) {
callCount++
if callCount == 2 {
optimizedPlan := &planpb.PlanNode{}
err := proto.Unmarshal(req.GetReq().GetSerializedExprPlan(), optimizedPlan)
s.Require().NoError(err)
s.NotNil(optimizedPlan.GetVectorAnns().GetPredicates(),
"second-stage search must keep predicates in the serialized plan")
}
}).Return(&internalpb.SearchResults{
FilterValidCounts: []int64{5000},
}, nil)