Coverage Report

Created: 2026-10-15 09:05

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/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
36
namespace doris {
37
using namespace segment_v2;
38
39
namespace {
40
41
struct SearchInputBundle {
42
    std::unordered_map<std::string, IndexIterator*> iterators;
43
    std::unordered_map<std::string, IndexFieldNameAndTypePair> field_types;
44
    std::unordered_map<std::string, int> field_name_to_column_id;
45
    std::vector<int> column_indexes;
46
    ColumnsWithTypeAndName literal_args;
47
};
48
49
void add_search_binding_diagnostic(const IndexExecContext* index_context,
50
42
                                   const std::string& diagnostic) {
51
42
    VLOG_DEBUG << diagnostic;
52
42
    if (index_context == nullptr) {
53
0
        return;
54
0
    }
55
42
    const auto& index_query_context = index_context->get_index_query_context();
56
42
    if (index_query_context != nullptr && index_query_context->stats != nullptr) {
57
39
        index_query_context->stats->inverted_index_stats.add_binding_diagnostic(diagnostic);
58
39
    }
59
42
}
60
61
Status collect_slot_search_input(const VSearchExpr& expr, const VSlotRef& slot_ref,
62
                                 const TSearchFieldBinding* binding,
63
1.65k
                                 IndexExecContext* index_context, SearchInputBundle* bundle) {
64
1.65k
    DCHECK(index_context != nullptr);
65
1.65k
    DCHECK(bundle != nullptr);
66
67
    // VSlotRef::column_id() is the scan-schema position used by IndexExecContext.
68
1.65k
    const int column_index = slot_ref.column_id();
69
1.65k
    const std::string field_name =
70
1.65k
            binding != nullptr ? binding->field_name : slot_ref.column_name();
71
1.65k
    const bool is_variant_subcolumn = binding != nullptr && binding->__isset.is_variant_subcolumn &&
72
1.65k
                                      binding->is_variant_subcolumn;
73
74
1.65k
    bundle->field_name_to_column_id[field_name] = column_index;
75
76
1.65k
    auto* iterator = index_context->get_inverted_index_iterator_by_column_id(column_index);
77
1.65k
    if (iterator == nullptr) {
78
        // For example, `data.items.message` has its own SlotRef in the scan schema. The
79
        // storage layer may inherit index metadata from `data`, but it still constructs a
80
        // child iterator whose stored field name contains the complete Variant path.
81
2
        if (is_variant_subcolumn) {
82
1
            add_search_binding_diagnostic(
83
1
                    index_context,
84
1
                    fmt::format("[VariantSearchBinding] phase=collect_inputs "
85
1
                                "result=no_iterator logical_field={} column_index={} "
86
1
                                "reason=slot_iterator_missing",
87
1
                                field_name, column_index));
88
1
        }
89
2
        return Status::OK();
90
2
    }
91
92
1.65k
    const auto* storage_name_type =
93
1.65k
            index_context->get_storage_name_and_type_by_column_id(column_index);
94
1.65k
    if (storage_name_type == nullptr) {
95
1
        return Status::InternalError("storage_name_type not found for column {} in {}",
96
1
                                     column_index, expr.expr_name());
97
1
    }
98
99
1.65k
    bundle->iterators.emplace(field_name, iterator);
100
1.65k
    bundle->field_types.emplace(field_name, *storage_name_type);
101
1.65k
    bundle->column_indexes.emplace_back(column_index);
102
1.65k
    if (is_variant_subcolumn) {
103
39
        add_search_binding_diagnostic(
104
39
                index_context,
105
39
                fmt::format("[VariantSearchBinding] phase=collect_inputs "
106
39
                            "result=direct_iterator logical_field={} column_index={} "
107
39
                            "stored_field={}",
108
39
                            field_name, column_index, storage_name_type->first));
109
39
    }
110
1.65k
    return Status::OK();
111
1.65k
}
112
113
Status collect_search_inputs(const VSearchExpr& expr, VExprContext* context,
114
1.15k
                             SearchInputBundle* bundle) {
115
1.15k
    DCHECK(bundle != nullptr);
116
117
1.15k
    auto index_context = context->get_index_context();
118
1.15k
    if (index_context == nullptr) {
119
0
        LOG(WARNING) << "collect_search_inputs: No inverted index context available";
120
0
        return Status::InternalError("No inverted index context available");
121
0
    }
122
123
1.15k
    const auto& search_param = expr.get_search_param();
124
1.15k
    const auto& field_bindings = search_param.field_bindings;
125
126
1.15k
    size_t child_index = 0;
127
1.67k
    for (const auto& child : expr.children()) {
128
1.67k
        if (child->is_slot_ref()) {
129
1.65k
            auto* column_slot_ref = assert_cast<VSlotRef*>(child.get());
130
1.65k
            const TSearchFieldBinding* binding =
131
18.4E
                    child_index < field_bindings.size() ? &field_bindings[child_index] : nullptr;
132
1.65k
            RETURN_IF_ERROR(collect_slot_search_input(expr, *column_slot_ref, binding,
133
1.65k
                                                      index_context.get(), bundle));
134
1.65k
            ++child_index;
135
1.65k
        } else if (child->is_literal()) {
136
0
            auto* literal = assert_cast<VLiteral*>(child.get());
137
0
            bundle->literal_args.emplace_back(literal->get_column_ptr(), literal->get_data_type(),
138
0
                                              literal->expr_name());
139
15
        } else {
140
            // Check if this is ElementAt expression (for variant subcolumn access)
141
15
            if (child->expr_name() == "element_at" && child_index < field_bindings.size() &&
142
15
                field_bindings[child_index].__isset.is_variant_subcolumn &&
143
15
                field_bindings[child_index].is_variant_subcolumn) {
144
                // Variant subcolumn not materialized - skip, will create empty BitSetQuery in function_search
145
0
                add_search_binding_diagnostic(
146
0
                        index_context.get(),
147
0
                        fmt::format("[VariantSearchBinding] phase=collect_inputs "
148
0
                                    "result=unmaterialized_element_at logical_field={} "
149
0
                                    "parent_field={} sub_path={} reason=no_slot_ref",
150
0
                                    field_bindings[child_index].field_name,
151
0
                                    field_bindings[child_index].__isset.parent_field_name
152
0
                                            ? field_bindings[child_index].parent_field_name
153
0
                                            : "",
154
0
                                    field_bindings[child_index].__isset.subcolumn_path
155
0
                                            ? field_bindings[child_index].subcolumn_path
156
0
                                            : ""));
157
0
                ++child_index;
158
0
                continue;
159
0
            }
160
161
            // Not a supported child type
162
15
            return Status::InvalidArgument("Unsupported child node type: {}", child->expr_name());
163
15
        }
164
1.67k
    }
165
166
1.14k
    return Status::OK();
167
1.15k
}
168
169
14
bool search_status_allows_row_fallback(const Status& status) {
170
14
    DORIS_CHECK(!status.ok());
171
14
    return status.is<ErrorCode::INVERTED_INDEX_BYPASS>() ||
172
14
           status.is<ErrorCode::INVERTED_INDEX_EVALUATE_SKIPPED>() ||
173
14
           status.is<ErrorCode::INVERTED_INDEX_FILE_CORRUPTED>() ||
174
14
           status.is<ErrorCode::INVERTED_INDEX_FILE_NOT_FOUND>() ||
175
14
           status.is<ErrorCode::NOT_IMPLEMENTED_ERROR>();
176
14
}
177
178
14
Status prevent_search_row_fallback(Status status) {
179
14
    DORIS_CHECK(!status.ok());
180
14
    if (!search_status_allows_row_fallback(status)) {
181
9
        return status;
182
9
    }
183
5
    return Status::Error<ErrorCode::INVERTED_INDEX_NOT_SUPPORTED>(
184
5
            "SEARCH cannot fall back to row execution: {}", status.to_string());
185
14
}
186
187
} // namespace
188
189
550
VSearchExpr::VSearchExpr(const TExprNode& node) : VExpr(node) {
190
550
    if (node.__isset.search_param) {
191
546
        _search_param = node.search_param;
192
546
        _original_dsl = _search_param.original_dsl;
193
546
    }
194
550
}
195
196
Status VSearchExpr::prepare(RuntimeState* state, const RowDescriptor& row_desc,
197
457
                            VExprContext* context) {
198
457
    RETURN_IF_ERROR(VExpr::prepare(state, row_desc, context));
199
457
    const auto& query_options = state->query_options();
200
457
    if (query_options.__isset.enable_inverted_index_query_cache) {
201
457
        _enable_cache = query_options.enable_inverted_index_query_cache;
202
457
    }
203
457
    return Status::OK();
204
457
}
205
206
97
const std::string& VSearchExpr::expr_name() const {
207
97
    static const std::string name = "VSearchExpr";
208
97
    return name;
209
97
}
210
211
Status VSearchExpr::execute_column(VExprContext* context, const Block* block, Selector* selector,
212
6
                                   size_t count, ColumnPtr& result_column) const {
213
6
    if (fast_execute(context, selector, count, result_column)) {
214
4
        return Status::OK();
215
4
    }
216
217
2
    return Status::InternalError("SearchExpr should not be executed without inverted index");
218
6
}
219
220
1.19k
Status VSearchExpr::evaluate_inverted_index(VExprContext* context, uint32_t segment_num_rows) {
221
1.19k
    if (_search_param.original_dsl.empty()) {
222
3
        return prevent_search_row_fallback(Status::InvalidArgument("search DSL is empty"));
223
3
    }
224
225
1.19k
    auto index_context = context->get_index_context();
226
1.19k
    if (!index_context) {
227
15
        LOG(WARNING) << "VSearchExpr: No inverted index context available";
228
15
        return Status::OK();
229
15
    }
230
231
1.17k
    SearchInputBundle bundle;
232
1.17k
    if (auto status = collect_search_inputs(*this, context, &bundle); !status.ok()) {
233
3
        return prevent_search_row_fallback(std::move(status));
234
3
    }
235
236
1.17k
    VLOG_DEBUG << "VSearchExpr: bundle.iterators.size()=" << bundle.iterators.size();
237
238
1.17k
    const bool is_nested_query = _search_param.root.clause_type == "NESTED";
239
1.17k
    if (bundle.iterators.empty() && !is_nested_query) {
240
2
        LOG(WARNING) << "VSearchExpr: No indexed columns available for evaluation, DSL: "
241
2
                     << _original_dsl;
242
2
        add_search_binding_diagnostic(
243
2
                index_context.get(),
244
2
                fmt::format("[VariantSearchBinding] phase=evaluate_search result=no_iterator "
245
2
                            "dsl={} reason=no_indexed_columns",
246
2
                            _original_dsl));
247
2
        auto empty_bitmap = InvertedIndexResultBitmap(std::make_shared<roaring::Roaring>(),
248
2
                                                      std::make_shared<roaring::Roaring>());
249
2
        index_context->set_index_result_for_expr(this, std::move(empty_bitmap));
250
2
        return Status::OK();
251
2
    }
252
253
1.17k
    auto index_query_context = index_context->get_index_query_context();
254
255
1.17k
    auto function = std::make_shared<FunctionSearch>();
256
1.17k
    auto result_bitmap = InvertedIndexResultBitmap();
257
1.17k
    auto status = function->evaluate_inverted_index_with_search_param(
258
1.17k
            _search_param, bundle.field_types, bundle.iterators, segment_num_rows, result_bitmap,
259
1.17k
            _enable_cache, index_context.get(), bundle.field_name_to_column_id,
260
1.17k
            index_query_context);
261
262
1.17k
    if (!status.ok()) {
263
8
        LOG(WARNING) << "VSearchExpr: Function evaluation failed: " << status.to_string();
264
8
        return prevent_search_row_fallback(std::move(status));
265
8
    }
266
267
1.16k
    index_context->set_index_result_for_expr(this, result_bitmap);
268
1.68k
    for (int column_index : bundle.column_indexes) {
269
1.68k
        index_context->set_true_for_index_status(this, column_index);
270
1.68k
    }
271
272
1.16k
    return Status::OK();
273
1.17k
}
274
275
} // namespace doris