This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new 87a7187512a branch-4.1: [refactor](search) Simplify Variant search
iterator binding #66449 (#66487)
87a7187512a is described below
commit 87a7187512a2d960cc541c3ce7b3d3a10eeccc36
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Aug 6 10:40:32 2026 +0800
branch-4.1: [refactor](search) Simplify Variant search iterator binding
#66449 (#66487)
Cherry-picked from #66449
Co-authored-by: lihangyu <[email protected]>
---
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 358ab7cbc5c..b591cbd31f5 100644
--- a/be/src/exprs/vexpr_context.h
+++ b/be/src/exprs/vexpr_context.h
@@ -90,16 +90,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()) {
@@ -112,22 +102,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 3b5da89d290..3de07df56ab 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;
}
@@ -329,8 +245,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 f8b02ce50d1..74d149e3392 100644
--- a/be/test/exprs/vsearch_expr_test.cpp
+++ b/be/test/exprs/vsearch_expr_test.cpp
@@ -33,6 +33,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
@@ -40,6 +41,7 @@
#endif
#define private public
#include "exprs/vslot_ref.h"
+#include "storage/segment/segment.h"
#undef private
#if defined(__clang__)
#pragma clang diagnostic pop
@@ -103,6 +105,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 {
@@ -1372,6 +1390,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]