This is an automated email from the ASF dual-hosted git repository.

eldenmoon pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 30a699a18a7 [refactor](search) Simplify Variant search iterator 
binding (#66449)
30a699a18a7 is described below

commit 30a699a18a7d9219a2188745da9d27ae84673b53
Author: lihangyu <[email protected]>
AuthorDate: Wed Aug 5 17:47:32 2026 +0800

    [refactor](search) Simplify Variant search iterator binding (#66449)
    
    Related PR: #60847, #63660
    Problem Summary:
    
    OlapScanner materializes a requested Variant path as a scan-schema
    column. For example, data.items.message gets its own SlotRef and scan
    column position. VariantColumnReader then resolves either direct
    subcolumn index metadata or metadata inherited from data, and
    SegmentIterator constructs the runtime iterator for that child column
    with the complete stored Variant path.
    
    VSearch duplicated that storage responsibility. When the child iterator
    was absent, it resolved the tablet ColumnId for data, borrowed the
    parent iterator, and synthesized a stored field name. This mixed scan
    column positions with tablet ColumnIds and could bypass the path and
    physical index selection already performed by the storage layer,
    including NestedGroup selection.
    
    This PR removes the expression-level parent rebinding. VSearch now
    consumes only the iterator attached to the SlotRef scan column. If that
    iterator is absent, the field remains an empty index input. It also
    removes the three fallback-only IndexExecContext APIs and separates
    SlotRef collection into a small helper that names the scan column index
    explicitly.
    
    A focused unit test constructs the exact boundary case: the data parent
    iterator exists, while the data.items.message child iterator does not.
    The test verifies that SEARCH does not mark or execute the parent
    iterator.
    
    ### Release note
    
    Variant SEARCH no longer borrows a parent Variant iterator when the
    materialized child iterator is absent.
---
 be/src/exprs/vexpr_context.h        |  26 -----
 be/src/exprs/vsearch.cpp            | 208 +++++++++++-------------------------
 be/test/exprs/vsearch_expr_test.cpp |  61 +++++++++++
 3 files changed, 123 insertions(+), 172 deletions(-)

diff --git a/be/src/exprs/vexpr_context.h b/be/src/exprs/vexpr_context.h
index 5bfd77f923e..3f5e33510cd 100644
--- a/be/src/exprs/vexpr_context.h
+++ b/be/src/exprs/vexpr_context.h
@@ -91,16 +91,6 @@ public:
         return _index_iterators[column_id].get();
     }
 
-    segment_v2::IndexIterator* get_inverted_index_iterator_by_id(ColumnId 
column_id) const {
-        if (column_id >= _index_iterators.size()) {
-            return nullptr;
-        }
-        if (!_index_iterators[column_id]) {
-            return nullptr;
-        }
-        return _index_iterators[column_id].get();
-    }
-
     const IndexFieldNameAndTypePair* get_storage_name_and_type_by_column_id(
             int column_index) const {
         if (column_index < 0 || column_index >= _col_ids.size()) {
@@ -113,22 +103,6 @@ public:
         return &_storage_name_and_type[column_id];
     }
 
-    const IndexFieldNameAndTypePair* get_storage_name_and_type_by_id(ColumnId 
column_id) const {
-        if (column_id >= _storage_name_and_type.size()) {
-            return nullptr;
-        }
-        return &_storage_name_and_type[column_id];
-    }
-
-    int column_index_by_id(ColumnId column_id) const {
-        for (int i = 0; i < _col_ids.size(); ++i) {
-            if (_col_ids[i] == column_id) {
-                return i;
-            }
-        }
-        return -1;
-    }
-
     bool get_column_id(int column_index, ColumnId* column_id) const {
         if (column_id == nullptr) {
             return false;
diff --git a/be/src/exprs/vsearch.cpp b/be/src/exprs/vsearch.cpp
index fdfc36e2098..ecaef392db8 100644
--- a/be/src/exprs/vsearch.cpp
+++ b/be/src/exprs/vsearch.cpp
@@ -32,8 +32,6 @@
 #include "glog/logging.h"
 #include "runtime/runtime_state.h"
 #include "storage/index/inverted/inverted_index_reader.h"
-#include "storage/olap_common.h"
-#include "storage/segment/segment.h"
 
 namespace doris {
 using namespace segment_v2;
@@ -44,7 +42,7 @@ struct SearchInputBundle {
     std::unordered_map<std::string, IndexIterator*> iterators;
     std::unordered_map<std::string, IndexFieldNameAndTypePair> field_types;
     std::unordered_map<std::string, int> field_name_to_column_id;
-    std::vector<int> column_ids;
+    std::vector<int> column_indexes;
     ColumnsWithTypeAndName literal_args;
 };
 
@@ -60,6 +58,58 @@ void add_search_binding_diagnostic(const IndexExecContext* 
index_context,
     }
 }
 
+Status collect_slot_search_input(const VSearchExpr& expr, const VSlotRef& 
slot_ref,
+                                 const TSearchFieldBinding* binding,
+                                 IndexExecContext* index_context, 
SearchInputBundle* bundle) {
+    DCHECK(index_context != nullptr);
+    DCHECK(bundle != nullptr);
+
+    // VSlotRef::column_id() is the scan-schema position used by 
IndexExecContext.
+    const int column_index = slot_ref.column_id();
+    const std::string field_name =
+            binding != nullptr ? binding->field_name : slot_ref.column_name();
+    const bool is_variant_subcolumn = binding != nullptr && 
binding->__isset.is_variant_subcolumn &&
+                                      binding->is_variant_subcolumn;
+
+    bundle->field_name_to_column_id[field_name] = column_index;
+
+    auto* iterator = 
index_context->get_inverted_index_iterator_by_column_id(column_index);
+    if (iterator == nullptr) {
+        // For example, `data.items.message` has its own SlotRef in the scan 
schema. The
+        // storage layer may inherit index metadata from `data`, but it still 
constructs a
+        // child iterator whose stored field name contains the complete 
Variant path.
+        if (is_variant_subcolumn) {
+            add_search_binding_diagnostic(
+                    index_context,
+                    fmt::format("[VariantSearchBinding] phase=collect_inputs "
+                                "result=no_iterator logical_field={} 
column_index={} "
+                                "reason=slot_iterator_missing",
+                                field_name, column_index));
+        }
+        return Status::OK();
+    }
+
+    const auto* storage_name_type =
+            
index_context->get_storage_name_and_type_by_column_id(column_index);
+    if (storage_name_type == nullptr) {
+        return Status::InternalError("storage_name_type not found for column 
{} in {}",
+                                     column_index, expr.expr_name());
+    }
+
+    bundle->iterators.emplace(field_name, iterator);
+    bundle->field_types.emplace(field_name, *storage_name_type);
+    bundle->column_indexes.emplace_back(column_index);
+    if (is_variant_subcolumn) {
+        add_search_binding_diagnostic(
+                index_context,
+                fmt::format("[VariantSearchBinding] phase=collect_inputs "
+                            "result=direct_iterator logical_field={} 
column_index={} "
+                            "stored_field={}",
+                            field_name, column_index, 
storage_name_type->first));
+    }
+    return Status::OK();
+}
+
 Status collect_search_inputs(const VSearchExpr& expr, VExprContext* context,
                              SearchInputBundle* bundle) {
     DCHECK(bundle != nullptr);
@@ -70,152 +120,18 @@ Status collect_search_inputs(const VSearchExpr& expr, 
VExprContext* context,
         return Status::InternalError("No inverted index context available");
     }
 
-    // Get field bindings for variant subcolumn support
     const auto& search_param = expr.get_search_param();
     const auto& field_bindings = search_param.field_bindings;
 
-    std::unordered_map<std::string, ColumnId> parent_to_base_column_id;
-    std::unordered_map<std::string, std::string> 
parent_to_storage_field_prefix;
-
-    // Resolve and cache the base (parent) column id for a variant field 
binding.
-    // This avoids repeated schema lookups when multiple subcolumns share the 
same parent column.
-    auto resolve_parent_column_id = [&](const std::string& parent_field, 
ColumnId* column_id) {
-        // Guard against invalid inputs: variant bindings may miss 
parent_field, and callers must
-        // provide a valid output pointer to receive the resolved id.
-        if (parent_field.empty() || column_id == nullptr) {
-            return false;
-        }
-        auto it = parent_to_base_column_id.find(parent_field);
-        if (it != parent_to_base_column_id.end()) {
-            *column_id = it->second;
-            return true;
-        }
-        if (index_context == nullptr || index_context->segment() == nullptr) {
-            return false;
-        }
-        const int32_t ordinal =
-                
index_context->segment()->tablet_schema()->field_index(parent_field);
-        if (ordinal < 0) {
-            return false;
-        }
-        ColumnId resolved_id = static_cast<ColumnId>(ordinal);
-        parent_to_base_column_id.emplace(parent_field, resolved_id);
-        if (auto* storage_name_type = 
index_context->get_storage_name_and_type_by_id(resolved_id);
-            storage_name_type != nullptr) {
-            parent_to_storage_field_prefix[parent_field] = 
storage_name_type->first;
-        }
-        *column_id = resolved_id;
-        return true;
-    };
-
-    int child_index = 0; // Index for iterating through children
+    size_t child_index = 0;
     for (const auto& child : expr.children()) {
         if (child->is_slot_ref()) {
             auto* column_slot_ref = assert_cast<VSlotRef*>(child.get());
-            int column_id = column_slot_ref->column_id();
-
-            // Determine the field_name from field_bindings (for variant 
subcolumns)
-            // field_bindings and children should have the same order
-            std::string field_name;
-            const TSearchFieldBinding* binding = nullptr;
-            if (child_index < field_bindings.size()) {
-                // Use field_name from binding (may include "parent.subcolumn" 
for variant)
-                binding = &field_bindings[child_index];
-                field_name = binding->field_name;
-            } else {
-                // Fallback to column_name if binding not found
-                field_name = column_slot_ref->column_name();
-            }
-
-            bundle->field_name_to_column_id[field_name] = column_id;
-
-            auto* iterator = 
index_context->get_inverted_index_iterator_by_column_id(column_id);
-            const auto* storage_name_type =
-                    
index_context->get_storage_name_and_type_by_column_id(column_id);
-            bool field_added = false;
-            // For variant subcolumns, slot_ref might not map to a real 
indexed column in the scan schema.
-            // Fall back to the parent variant column's iterator and 
synthesize lucene field name.
-            if (iterator == nullptr && binding != nullptr &&
-                binding->__isset.is_variant_subcolumn && 
binding->is_variant_subcolumn &&
-                binding->__isset.parent_field_name && 
!binding->parent_field_name.empty()) {
-                ColumnId base_column_id = 0;
-                if (resolve_parent_column_id(binding->parent_field_name, 
&base_column_id)) {
-                    iterator = 
index_context->get_inverted_index_iterator_by_id(base_column_id);
-                    const auto* base_storage_name_type =
-                            
index_context->get_storage_name_and_type_by_id(base_column_id);
-                    if (iterator != nullptr && base_storage_name_type != 
nullptr) {
-                        std::string prefix = base_storage_name_type->first;
-                        if (auto pit =
-                                    
parent_to_storage_field_prefix.find(binding->parent_field_name);
-                            pit != parent_to_storage_field_prefix.end() && 
!pit->second.empty()) {
-                            prefix = pit->second;
-                        } else {
-                            
parent_to_storage_field_prefix[binding->parent_field_name] = prefix;
-                        }
-
-                        std::string sub_path;
-                        if (binding->__isset.subcolumn_path) {
-                            sub_path = binding->subcolumn_path;
-                        }
-                        if (sub_path.empty()) {
-                            // Fallback: strip "parent." prefix from logical 
field name
-                            std::string pfx = binding->parent_field_name + ".";
-                            if (field_name.starts_with(pfx)) {
-                                sub_path = field_name.substr(pfx.size());
-                            }
-                        }
-                        if (!sub_path.empty()) {
-                            bundle->iterators[field_name] = iterator;
-                            bundle->field_types[field_name] =
-                                    std::make_pair(prefix + "." + sub_path, 
nullptr);
-                            int base_column_index =
-                                    
index_context->column_index_by_id(base_column_id);
-                            if (base_column_index >= 0) {
-                                
bundle->column_ids.emplace_back(base_column_index);
-                            }
-                            add_search_binding_diagnostic(
-                                    index_context.get(),
-                                    fmt::format("[VariantSearchBinding] 
phase=collect_inputs "
-                                                "result=parent_fallback 
logical_field={} "
-                                                "parent_field={} sub_path={} 
base_column_id={} "
-                                                "stored_field={} 
reason=slot_iterator_missing",
-                                                field_name, 
binding->parent_field_name, sub_path,
-                                                base_column_id, prefix + "." + 
sub_path));
-                            field_added = true;
-                        }
-                    }
-                } else {
-                    add_search_binding_diagnostic(
-                            index_context.get(),
-                            fmt::format("[VariantSearchBinding] 
phase=collect_inputs "
-                                        "result=reject logical_field={} 
parent_field={} "
-                                        "reason=parent_column_not_found",
-                                        field_name, 
binding->parent_field_name));
-                }
-            }
-
-            // Only collect fields that have iterators (materialized columns 
with indexes)
-            if (!field_added && iterator != nullptr) {
-                if (storage_name_type == nullptr) {
-                    return Status::InternalError("storage_name_type not found 
for column {} in {}",
-                                                 column_id, expr.expr_name());
-                }
-
-                bundle->iterators.emplace(field_name, iterator);
-                bundle->field_types.emplace(field_name, *storage_name_type);
-                bundle->column_ids.emplace_back(column_id);
-                if (binding != nullptr && 
binding->__isset.is_variant_subcolumn &&
-                    binding->is_variant_subcolumn) {
-                    add_search_binding_diagnostic(
-                            index_context.get(),
-                            fmt::format("[VariantSearchBinding] 
phase=collect_inputs "
-                                        "result=direct_iterator 
logical_field={} column_id={} "
-                                        "stored_field={}",
-                                        field_name, column_id, 
storage_name_type->first));
-                }
-            }
-
-            child_index++;
+            const TSearchFieldBinding* binding =
+                    child_index < field_bindings.size() ? 
&field_bindings[child_index] : nullptr;
+            RETURN_IF_ERROR(collect_slot_search_input(expr, *column_slot_ref, 
binding,
+                                                      index_context.get(), 
bundle));
+            ++child_index;
         } else if (child->is_literal()) {
             auto* literal = assert_cast<VLiteral*>(child.get());
             bundle->literal_args.emplace_back(literal->get_column_ptr(), 
literal->get_data_type(),
@@ -238,7 +154,7 @@ Status collect_search_inputs(const VSearchExpr& expr, 
VExprContext* context,
                                     
field_bindings[child_index].__isset.subcolumn_path
                                             ? 
field_bindings[child_index].subcolumn_path
                                             : ""));
-                child_index++;
+                ++child_index;
                 continue;
             }
 
@@ -330,8 +246,8 @@ Status VSearchExpr::evaluate_inverted_index(VExprContext* 
context, uint32_t segm
     }
 
     index_context->set_index_result_for_expr(this, result_bitmap);
-    for (int column_id : bundle.column_ids) {
-        index_context->set_true_for_index_status(this, column_id);
+    for (int column_index : bundle.column_indexes) {
+        index_context->set_true_for_index_status(this, column_index);
     }
 
     return Status::OK();
diff --git a/be/test/exprs/vsearch_expr_test.cpp 
b/be/test/exprs/vsearch_expr_test.cpp
index cc3fa5820fc..9703a465093 100644
--- a/be/test/exprs/vsearch_expr_test.cpp
+++ b/be/test/exprs/vsearch_expr_test.cpp
@@ -35,6 +35,7 @@
 #include "exprs/vsearch.h"
 #include "storage/index/index_iterator.h"
 #include "storage/segment/variant/nested_group_provider.h"
+#include "storage/tablet/tablet_schema.h"
 
 #if defined(__clang__)
 #pragma clang diagnostic push
@@ -42,6 +43,7 @@
 #endif
 #define private public
 #include "exprs/vslot_ref.h"
+#include "storage/segment/segment.h"
 #undef private
 #if defined(__clang__)
 #pragma clang diagnostic pop
@@ -105,6 +107,22 @@ std::shared_ptr<IndexExecContext> make_inverted_context(
                                               nullptr, nullptr, 
column_iter_opts);
 }
 
+std::shared_ptr<segment_v2::Segment> make_segment_with_variant_parent() {
+    TabletSchemaPB schema_pb;
+    schema_pb.set_keys_type(KeysType::DUP_KEYS);
+    auto* parent = schema_pb.add_column();
+    parent->set_unique_id(0);
+    parent->set_name("data");
+    parent->set_type("VARIANT");
+    parent->set_is_key(false);
+    parent->set_is_nullable(true);
+
+    auto tablet_schema = std::make_shared<TabletSchema>();
+    tablet_schema->init_from_pb(schema_pb);
+    return std::make_shared<segment_v2::Segment>(0, RowsetId(), tablet_schema,
+                                                 InvertedIndexFileInfo());
+}
+
 } // namespace
 
 class VSearchExprTest : public testing::Test {
@@ -1359,6 +1377,49 @@ TEST_F(VSearchExprTest, 
EvaluateInvertedIndexHandlesMissingIterators) {
     EXPECT_FALSE(status_map[0][expr.get()]);
 }
 
+TEST_F(VSearchExprTest, MissingVariantChildIteratorDoesNotUseParentIterator) {
+    TExprNode variant_node = test_node;
+    variant_node.search_param.original_dsl = "data.items.message:hello";
+    variant_node.search_param.root.field_name = "data.items.message";
+    auto& binding = variant_node.search_param.field_bindings.front();
+    binding.field_name = "data.items.message";
+    binding.__set_is_variant_subcolumn(true);
+    binding.__set_parent_field_name("data");
+    binding.__set_subcolumn_path("items.message");
+
+    auto expr = VSearchExpr::create_shared(variant_node);
+    expr->add_child(create_slot_ref(1, "data.items.message"));
+
+    // Scan column 0 is the Variant parent and has an iterator. Scan column 1 
is the requested
+    // child and intentionally has none. SEARCH must not reinterpret the 
parent iterator as the
+    // child's index.
+    std::vector<ColumnId> col_ids = {0, 1};
+    std::vector<std::unique_ptr<segment_v2::IndexIterator>> index_iterators;
+    index_iterators.emplace_back(std::make_unique<StubIndexIterator>());
+    index_iterators.emplace_back(nullptr);
+    std::vector<IndexFieldNameAndTypePair> storage_types;
+    storage_types.emplace_back("0.data", std::make_shared<DataTypeString>());
+    storage_types.emplace_back("0.data.items.message", 
std::make_shared<DataTypeString>());
+    std::unordered_map<ColumnId, std::unordered_map<const VExpr*, bool>> 
status_map;
+    status_map[0][expr.get()] = false;
+    status_map[1][expr.get()] = false;
+
+    // Keep the parent in the segment schema so a parent-rebinding 
implementation would find it.
+    segment_v2::ColumnIteratorOptions column_iter_opts;
+    auto segment = make_segment_with_variant_parent();
+    auto inverted_ctx =
+            std::make_shared<IndexExecContext>(col_ids, index_iterators, 
storage_types, status_map,
+                                               nullptr, segment.get(), 
column_iter_opts);
+    auto context = std::make_shared<VExprContext>(expr);
+    context->set_index_context(inverted_ctx);
+
+    auto status = expr->evaluate_inverted_index(context.get(), 32);
+    EXPECT_TRUE(status.ok()) << status;
+    EXPECT_TRUE(inverted_ctx->has_index_result_for_expr(expr.get()));
+    EXPECT_FALSE(status_map[0][expr.get()]);
+    EXPECT_FALSE(status_map[1][expr.get()]);
+}
+
 TEST_F(VSearchExprTest, 
EvaluateInvertedIndexNestedFallbackReturnsNotSupportedInCE) {
     TExprNode nested_node = test_node;
     nested_node.num_children = 0;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to