Coverage Report

Created: 2026-10-20 11:24

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/root/doris/be/src/exprs/vsearch.cpp
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#include "exprs/vsearch.h"
19
20
#include <fmt/format.h>
21
22
#include <memory>
23
#include <roaring/roaring.hh>
24
25
#include "common/logging.h"
26
#include "common/status.h"
27
#include "core/column/column_const.h"
28
#include "exprs/function/search/function_search.h"
29
#include "exprs/vexpr_context.h"
30
#include "exprs/vliteral.h"
31
#include "exprs/vslot_ref.h"
32
#include "glog/logging.h"
33
#include "runtime/runtime_state.h"
34
#include "storage/index/inverted/inverted_index_reader.h"
35
#include "storage/schema.h"
36
37
namespace doris {
38
using namespace segment_v2;
39
40
namespace {
41
42
struct SearchInputBundle {
43
    std::unordered_map<std::string, IndexIterator*> iterators;
44
    std::unordered_map<std::string, IndexFieldNameAndTypePair> field_types;
45
    const TabletColumn* nested_column = nullptr;
46
    std::vector<int> column_indexes;
47
    ColumnsWithTypeAndName literal_args;
48
};
49
50
void add_search_binding_diagnostic(const IndexExecContext* index_context,
51
144
                                   const std::string& diagnostic) {
52
144
    VLOG_DEBUG << diagnostic;
53
144
    if (index_context == nullptr) {
54
0
        return;
55
0
    }
56
144
    const auto& index_query_context = index_context->get_index_query_context();
57
144
    if (index_query_context != nullptr && index_query_context->stats != nullptr) {
58
140
        index_query_context->stats->inverted_index_stats.add_binding_diagnostic(diagnostic);
59
140
    }
60
144
}
61
62
Status bind_nested_column(const VSearchExpr& expr, const VSlotRef& slot_ref,
63
                          const TSearchFieldBinding* binding, const IndexExecContext& index_context,
64
1.85k
                          SearchInputBundle* bundle) {
65
1.85k
    const auto& clause = expr.get_search_param().root;
66
1.85k
    if (clause.clause_type != "NESTED") {
67
1.85k
        return Status::OK();
68
1.85k
    }
69
1
    const auto root_name = clause.nested_path.substr(0, clause.nested_path.find('.'));
70
1
    const auto& parent_name = binding != nullptr && binding->__isset.parent_field_name
71
1
                                      ? binding->parent_field_name
72
1
                                      : slot_ref.column_name();
73
1
    if (parent_name != root_name) {
74
0
        return Status::OK();
75
0
    }
76
77
1
    const auto& schema = index_context.read_schema();
78
1
    if (schema == nullptr) {
79
0
        return Status::InvalidArgument("NESTED query requires a read schema");
80
0
    }
81
1
    const int ordinal = slot_ref.column_id();
82
1
    if (ordinal < 0 || static_cast<size_t>(ordinal) >= schema->num_column_ids()) {
83
0
        return Status::InvalidArgument("NESTED query column index {} is outside the read schema",
84
0
                                       ordinal);
85
0
    }
86
1
    const auto* column = schema->column(schema->column_id(ordinal));
87
1
    if (bundle->nested_column != nullptr) {
88
0
        const auto root_uid = [](const TabletColumn& col) {
89
0
            return col.parent_unique_id() >= 0 ? col.parent_unique_id() : col.unique_id();
90
0
        };
91
0
        if (root_uid(*bundle->nested_column) != root_uid(*column)) {
92
0
            return Status::InvalidArgument(
93
0
                    "NESTED query binds columns from different VARIANT roots");
94
0
        }
95
0
    }
96
1
    bundle->nested_column = column;
97
1
    return Status::OK();
98
1
}
99
100
Status collect_slot_search_input(const VSearchExpr& expr, const VSlotRef& slot_ref,
101
                                 const TSearchFieldBinding* binding,
102
1.86k
                                 IndexExecContext* index_context, SearchInputBundle* bundle) {
103
1.86k
    DCHECK(index_context != nullptr);
104
1.86k
    DCHECK(bundle != nullptr);
105
106
    // VSlotRef::column_id() is the scan-schema position used by IndexExecContext.
107
1.86k
    const int column_index = slot_ref.column_id();
108
1.86k
    const std::string field_name =
109
1.86k
            binding != nullptr ? binding->field_name : slot_ref.column_name();
110
1.91k
    const bool is_variant_subcolumn = binding != nullptr && binding->__isset.is_variant_subcolumn &&
111
1.90k
                                      binding->is_variant_subcolumn;
112
113
1.86k
    RETURN_IF_ERROR(bind_nested_column(expr, slot_ref, binding, *index_context, bundle));
114
115
1.86k
    auto* iterator = index_context->get_inverted_index_iterator_by_column_id(column_index);
116
1.86k
    if (iterator == nullptr) {
117
        // For example, `data.items.message` has its own SlotRef in the scan schema. The
118
        // storage layer may inherit index metadata from `data`, but it still constructs a
119
        // child iterator whose stored field name contains the complete Variant path.
120
29
        if (is_variant_subcolumn) {
121
28
            add_search_binding_diagnostic(
122
28
                    index_context,
123
28
                    fmt::format("[VariantSearchBinding] phase=collect_inputs "
124
28
                                "result=no_iterator logical_field={} column_index={} "
125
28
                                "reason=slot_iterator_missing",
126
28
                                field_name, column_index));
127
28
        }
128
29
        return Status::OK();
129
29
    }
130
131
1.83k
    const auto* storage_name_type =
132
1.83k
            index_context->get_storage_name_and_type_by_column_id(column_index);
133
1.83k
    if (storage_name_type == nullptr) {
134
1
        return Status::InternalError("storage_name_type not found for column {} in {}",
135
1
                                     column_index, expr.expr_name());
136
1
    }
137
138
1.83k
    bundle->iterators.emplace(field_name, iterator);
139
1.83k
    bundle->field_types.emplace(field_name, *storage_name_type);
140
1.83k
    bundle->column_indexes.emplace_back(column_index);
141
1.83k
    if (is_variant_subcolumn) {
142
89
        add_search_binding_diagnostic(
143
89
                index_context,
144
89
                fmt::format("[VariantSearchBinding] phase=collect_inputs "
145
89
                            "result=direct_iterator logical_field={} column_index={} "
146
89
                            "stored_field={}",
147
89
                            field_name, column_index, storage_name_type->first));
148
89
    }
149
1.83k
    return Status::OK();
150
1.83k
}
151
152
Status bind_nested_element_at(const VSearchExpr& expr, const VExpr& child,
153
                              const TSearchFieldBinding& binding,
154
0
                              const IndexExecContext& index_context, SearchInputBundle* bundle) {
155
0
    if (expr.get_search_param().root.clause_type != "NESTED") {
156
0
        return Status::OK();
157
0
    }
158
0
    const VExpr* root_expr = &child;
159
0
    while (root_expr->expr_name() == "element_at") {
160
0
        if (root_expr->children().empty()) {
161
0
            return Status::InvalidArgument("NESTED query has an incomplete VARIANT path");
162
0
        }
163
0
        root_expr = root_expr->children().front().get();
164
0
    }
165
0
    if (!root_expr->is_slot_ref()) {
166
0
        return Status::InvalidArgument("NESTED query requires a slot reference root");
167
0
    }
168
0
    return bind_nested_column(expr, *assert_cast<const VSlotRef*>(root_expr), &binding,
169
0
                              index_context, bundle);
170
0
}
171
172
Status collect_search_inputs(const VSearchExpr& expr, VExprContext* context,
173
1.33k
                             SearchInputBundle* bundle) {
174
1.33k
    DCHECK(bundle != nullptr);
175
176
1.33k
    auto index_context = context->get_index_context();
177
1.33k
    if (index_context == nullptr) {
178
0
        LOG(WARNING) << "collect_search_inputs: No inverted index context available";
179
0
        return Status::InternalError("No inverted index context available");
180
0
    }
181
182
1.33k
    const auto& search_param = expr.get_search_param();
183
1.33k
    const auto& field_bindings = search_param.field_bindings;
184
185
1.33k
    size_t child_index = 0;
186
1.88k
    for (const auto& child : expr.children()) {
187
1.88k
        if (child->is_slot_ref()) {
188
1.87k
            auto* column_slot_ref = assert_cast<VSlotRef*>(child.get());
189
1.87k
            const TSearchFieldBinding* binding =
190
1.87k
                    child_index < field_bindings.size() ? &field_bindings[child_index] : nullptr;
191
1.87k
            RETURN_IF_ERROR(collect_slot_search_input(expr, *column_slot_ref, binding,
192
1.87k
                                                      index_context.get(), bundle));
193
1.87k
            ++child_index;
194
1.87k
        } else if (child->is_literal()) {
195
0
            auto* literal = assert_cast<VLiteral*>(child.get());
196
0
            bundle->literal_args.emplace_back(literal->get_column_ptr(), literal->get_data_type(),
197
0
                                              literal->expr_name());
198
16
        } else {
199
            // Check if this is ElementAt expression (for variant subcolumn access)
200
16
            if (child->expr_name() == "element_at" && child_index < field_bindings.size() &&
201
16
                field_bindings[child_index].__isset.is_variant_subcolumn &&
202
16
                field_bindings[child_index].is_variant_subcolumn) {
203
0
                RETURN_IF_ERROR(bind_nested_element_at(expr, *child, field_bindings[child_index],
204
0
                                                       *index_context, bundle));
205
                // Variant subcolumn not materialized - skip, will create empty BitSetQuery in function_search
206
0
                add_search_binding_diagnostic(
207
0
                        index_context.get(),
208
0
                        fmt::format("[VariantSearchBinding] phase=collect_inputs "
209
0
                                    "result=unmaterialized_element_at logical_field={} "
210
0
                                    "parent_field={} sub_path={} reason=no_slot_ref",
211
0
                                    field_bindings[child_index].field_name,
212
0
                                    field_bindings[child_index].__isset.parent_field_name
213
0
                                            ? field_bindings[child_index].parent_field_name
214
0
                                            : "",
215
0
                                    field_bindings[child_index].__isset.subcolumn_path
216
0
                                            ? field_bindings[child_index].subcolumn_path
217
0
                                            : ""));
218
0
                ++child_index;
219
0
                continue;
220
0
            }
221
222
            // Not a supported child type
223
16
            return Status::InvalidArgument("Unsupported child node type: {}", child->expr_name());
224
16
        }
225
1.88k
    }
226
227
1.32k
    return Status::OK();
228
1.33k
}
229
230
14
bool search_status_allows_row_fallback(const Status& status) {
231
14
    DORIS_CHECK(!status.ok());
232
14
    return status.is<ErrorCode::INVERTED_INDEX_BYPASS>() ||
233
14
           status.is<ErrorCode::INVERTED_INDEX_EVALUATE_SKIPPED>() ||
234
14
           status.is<ErrorCode::INVERTED_INDEX_FILE_CORRUPTED>() ||
235
14
           status.is<ErrorCode::INVERTED_INDEX_FILE_NOT_FOUND>() ||
236
14
           status.is<ErrorCode::NOT_IMPLEMENTED_ERROR>();
237
14
}
238
239
14
Status prevent_search_row_fallback(Status status) {
240
14
    DORIS_CHECK(!status.ok());
241
14
    if (!search_status_allows_row_fallback(status)) {
242
9
        return status;
243
9
    }
244
5
    return Status::Error<ErrorCode::INVERTED_INDEX_NOT_SUPPORTED>(
245
5
            "SEARCH cannot fall back to row execution: {}", status.to_string());
246
14
}
247
248
} // namespace
249
250
612
VSearchExpr::VSearchExpr(const TExprNode& node) : VExpr(node) {
251
612
    if (node.__isset.search_param) {
252
608
        _search_param = node.search_param;
253
608
        _original_dsl = _search_param.original_dsl;
254
608
    }
255
612
}
256
257
Status VSearchExpr::prepare(RuntimeState* state, const RowDescriptor& row_desc,
258
519
                            VExprContext* context) {
259
519
    RETURN_IF_ERROR(VExpr::prepare(state, row_desc, context));
260
519
    const auto& query_options = state->query_options();
261
519
    if (query_options.__isset.enable_inverted_index_query_cache) {
262
518
        _enable_cache = query_options.enable_inverted_index_query_cache;
263
518
    }
264
519
    return Status::OK();
265
519
}
266
267
97
const std::string& VSearchExpr::expr_name() const {
268
97
    static const std::string name = "VSearchExpr";
269
97
    return name;
270
97
}
271
272
Status VSearchExpr::execute_column(VExprContext* context, const Block* block, Selector* selector,
273
6
                                   size_t count, ColumnPtr& result_column) const {
274
6
    if (fast_execute(context, selector, count, result_column)) {
275
4
        return Status::OK();
276
4
    }
277
278
2
    return Status::InternalError("SearchExpr should not be executed without inverted index");
279
6
}
280
281
1.37k
Status VSearchExpr::evaluate_inverted_index(VExprContext* context, uint32_t segment_num_rows) {
282
1.37k
    if (_search_param.original_dsl.empty()) {
283
3
        return prevent_search_row_fallback(Status::InvalidArgument("search DSL is empty"));
284
3
    }
285
286
1.37k
    auto index_context = context->get_index_context();
287
1.37k
    if (!index_context) {
288
15
        LOG(WARNING) << "VSearchExpr: No inverted index context available";
289
15
        return Status::OK();
290
15
    }
291
292
1.35k
    SearchInputBundle bundle;
293
1.35k
    if (auto status = collect_search_inputs(*this, context, &bundle); !status.ok()) {
294
3
        return prevent_search_row_fallback(std::move(status));
295
3
    }
296
297
1.35k
    VLOG_DEBUG << "VSearchExpr: bundle.iterators.size()=" << bundle.iterators.size();
298
299
1.35k
    const bool is_nested_query = _search_param.root.clause_type == "NESTED";
300
1.35k
    if (bundle.iterators.empty() && !is_nested_query) {
301
25
        LOG(WARNING) << "VSearchExpr: No indexed columns available for evaluation, DSL: "
302
25
                     << _original_dsl;
303
25
        add_search_binding_diagnostic(
304
25
                index_context.get(),
305
25
                fmt::format("[VariantSearchBinding] phase=evaluate_search result=no_iterator "
306
25
                            "dsl={} reason=no_indexed_columns",
307
25
                            _original_dsl));
308
25
        auto empty_bitmap = InvertedIndexResultBitmap(std::make_shared<roaring::Roaring>(),
309
25
                                                      std::make_shared<roaring::Roaring>());
310
25
        index_context->set_index_result_for_expr(this, std::move(empty_bitmap));
311
25
        return Status::OK();
312
25
    }
313
314
1.32k
    auto index_query_context = index_context->get_index_query_context();
315
316
1.32k
    auto function = std::make_shared<FunctionSearch>();
317
1.32k
    auto result_bitmap = InvertedIndexResultBitmap();
318
1.32k
    auto status = function->evaluate_inverted_index_with_search_param(
319
1.32k
            _search_param, bundle.field_types, bundle.iterators, segment_num_rows, result_bitmap,
320
1.32k
            _enable_cache, index_context.get(), bundle.nested_column, index_query_context);
321
322
1.32k
    if (!status.ok()) {
323
8
        LOG(WARNING) << "VSearchExpr: Function evaluation failed: " << status.to_string();
324
8
        return prevent_search_row_fallback(std::move(status));
325
8
    }
326
327
1.31k
    index_context->set_index_result_for_expr(this, result_bitmap);
328
1.89k
    for (int column_index : bundle.column_indexes) {
329
1.89k
        index_context->set_true_for_index_status(this, column_index);
330
1.89k
    }
331
332
1.31k
    return Status::OK();
333
1.32k
}
334
335
} // namespace doris