Coverage Report

Created: 2026-10-22 21:46

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
#include "storage/segment/segment.h"
37
#include "storage/segment/variant/variant_column_reader.h"
38
39
namespace doris {
40
using namespace segment_v2;
41
42
namespace {
43
44
struct SearchInputBundle {
45
    std::unordered_map<std::string, IndexIterator*> iterators;
46
    std::unordered_map<std::string, IndexFieldNameAndTypePair> field_types;
47
    const TabletColumn* nested_column = nullptr;
48
    std::vector<int> column_indexes;
49
    ColumnsWithTypeAndName literal_args;
50
};
51
52
void add_search_binding_diagnostic(const IndexExecContext* index_context,
53
487
                                   const std::string& diagnostic) {
54
18.4E
    VLOG_DEBUG << diagnostic;
55
487
    if (index_context == nullptr) {
56
0
        return;
57
0
    }
58
487
    const auto& index_query_context = index_context->get_index_query_context();
59
487
    if (index_query_context != nullptr && index_query_context->stats != nullptr) {
60
479
        index_query_context->stats->inverted_index_stats.add_binding_diagnostic(diagnostic);
61
479
    }
62
487
}
63
64
// A missing iterator is not evidence of a missing value: doc/sparse paths and
65
// old rowsets may contain values without an executable SEARCH index.
66
Status verify_missing_variant_path(const TSearchFieldBinding& binding, int column_index,
67
303
                                   IndexExecContext* index_context) {
68
303
    auto* segment = index_context->segment();
69
303
    if (segment == nullptr || !binding.__isset.parent_field_name ||
70
303
        !binding.__isset.subcolumn_path) {
71
1
        return Status::Error<ErrorCode::INVERTED_INDEX_NOT_SUPPORTED>(
72
1
                "SEARCH cannot establish missing path '{}'", binding.field_name);
73
1
    }
74
302
    const auto& read_schema = index_context->read_schema();
75
302
    DORIS_CHECK_GE(column_index, 0);
76
302
    DORIS_CHECK_LT(static_cast<size_t>(column_index), read_schema->num_column_ids());
77
302
    const auto& column = *read_schema->column(read_schema->column_id(column_index));
78
302
    const auto& opts = index_context->column_iter_opts();
79
302
    DORIS_CHECK(opts.stats != nullptr);
80
302
    const int32_t root_uid =
81
302
            column.parent_unique_id() >= 0 ? column.parent_unique_id() : column.unique_id();
82
    // DROP/ADD can leave an old segment without the current root UID. Do not open its
83
    // historical same-named column or initialize readers for a root that is absent.
84
302
    if (!segment->tablet_schema()->has_column_unique_id(root_uid)) {
85
4
        return Status::OK();
86
4
    }
87
298
    std::shared_ptr<segment_v2::ColumnReader> column_reader;
88
298
    Status st = segment->get_column_reader(root_uid, &column_reader, opts.stats, &opts.io_ctx);
89
298
    if (st.is<ErrorCode::NOT_FOUND>()) {
90
        // After DROP v(uid=1), ADD v(uid=2), an old segment cannot contain this scan's root.
91
        // Resolve by the current uid so historical values of the same name cannot match.
92
0
        return Status::OK();
93
0
    }
94
298
    RETURN_IF_ERROR(st);
95
298
    auto* reader = dynamic_cast<VariantColumnReader*>(column_reader.get());
96
298
    DORIS_CHECK(reader != nullptr);
97
298
    RETURN_IF_ERROR(reader->load_external_meta_once(opts.stats, &opts.io_ctx));
98
298
    const PathInData path(binding.subcolumn_path);
99
298
    const auto* stats = reader->get_stats();
100
    // Doc statistics enumerate stored paths without the sparse statistics limit. For example,
101
    // a segment containing only `string4` can prove `string5` absent even in doc mode.
102
    // An opaque root/ancestor value may contain the requested path, so it remains unavailable.
103
298
    bool in_doc = stats->existed_in_doc_value_column("") ||
104
298
                  stats->has_prefix_path_in_doc_value_column(path.get_path() + ".");
105
298
    std::string ancestor = path.get_path();
106
304
    while (!in_doc) {
107
304
        in_doc = stats->existed_in_doc_value_column(ancestor);
108
304
        const auto dot = ancestor.rfind('.');
109
304
        if (dot == std::string::npos) {
110
298
            break;
111
298
        }
112
6
        ancestor.resize(dot);
113
6
    }
114
    // Truncated sparse statistics cannot prove absence, nor can an existing unindexed path.
115
298
    if (reader->get_subcolumn_meta_by_path(path) != nullptr ||
116
298
        reader->has_prefix_path(path, opts.stats, &opts.io_ctx) ||
117
298
        reader->exist_in_sparse_column(path) || reader->is_exceeded_sparse_column_limit() ||
118
298
        in_doc || stats->has_nested_group) {
119
100
        return Status::Error<ErrorCode::INVERTED_INDEX_NOT_SUPPORTED>(
120
100
                "SEARCH index unavailable for Variant field '{}'", binding.field_name);
121
100
    }
122
198
    return Status::OK();
123
298
}
124
125
Status bind_nested_column(const VSearchExpr& expr, const VSlotRef& slot_ref,
126
                          const TSearchFieldBinding* binding, const IndexExecContext& index_context,
127
2.98k
                          SearchInputBundle* bundle) {
128
2.98k
    const auto& clause = expr.get_search_param().root;
129
2.98k
    if (clause.clause_type != "NESTED") {
130
2.98k
        return Status::OK();
131
2.98k
    }
132
18.4E
    const auto root_name = clause.nested_path.substr(0, clause.nested_path.find('.'));
133
18.4E
    const auto& parent_name = binding != nullptr && binding->__isset.parent_field_name
134
18.4E
                                      ? binding->parent_field_name
135
18.4E
                                      : slot_ref.column_name();
136
18.4E
    if (parent_name != root_name) {
137
0
        return Status::OK();
138
0
    }
139
140
18.4E
    const auto& schema = index_context.read_schema();
141
18.4E
    if (schema == nullptr) {
142
0
        return Status::InvalidArgument("NESTED query requires a read schema");
143
0
    }
144
18.4E
    const int ordinal = slot_ref.column_id();
145
18.4E
    if (ordinal < 0 || static_cast<size_t>(ordinal) >= schema->num_column_ids()) {
146
0
        return Status::InvalidArgument("NESTED query column index {} is outside the read schema",
147
0
                                       ordinal);
148
0
    }
149
18.4E
    const auto* column = schema->column(schema->column_id(ordinal));
150
18.4E
    if (bundle->nested_column != nullptr) {
151
0
        const auto root_uid = [](const TabletColumn& col) {
152
0
            return col.parent_unique_id() >= 0 ? col.parent_unique_id() : col.unique_id();
153
0
        };
154
0
        if (root_uid(*bundle->nested_column) != root_uid(*column)) {
155
0
            return Status::InvalidArgument(
156
0
                    "NESTED query binds columns from different VARIANT roots");
157
0
        }
158
0
    }
159
18.4E
    bundle->nested_column = column;
160
18.4E
    return Status::OK();
161
18.4E
}
162
163
Status collect_slot_search_input(const VSearchExpr& expr, const VSlotRef& slot_ref,
164
                                 const TSearchFieldBinding* binding,
165
3.02k
                                 IndexExecContext* index_context, SearchInputBundle* bundle) {
166
3.02k
    DCHECK(index_context != nullptr);
167
3.02k
    DCHECK(bundle != nullptr);
168
169
    // VSlotRef::column_id() is the scan-schema position used by IndexExecContext.
170
3.02k
    const int column_index = slot_ref.column_id();
171
3.02k
    const std::string field_name =
172
3.02k
            binding != nullptr ? binding->field_name : slot_ref.column_name();
173
3.02k
    const bool is_variant_subcolumn = binding != nullptr && binding->__isset.is_variant_subcolumn &&
174
3.02k
                                      binding->is_variant_subcolumn;
175
176
3.02k
    RETURN_IF_ERROR(bind_nested_column(expr, slot_ref, binding, *index_context, bundle));
177
178
3.02k
    auto* iterator = index_context->get_inverted_index_iterator_by_column_id(column_index);
179
3.02k
    if (iterator == nullptr) {
180
        // For example, `data.items.message` has its own SlotRef in the scan schema. The
181
        // storage layer may inherit index metadata from `data`, but it still constructs a
182
        // child iterator whose stored field name contains the complete Variant path.
183
304
        if (is_variant_subcolumn) {
184
303
            add_search_binding_diagnostic(
185
303
                    index_context,
186
303
                    fmt::format("[VariantSearchBinding] phase=collect_inputs "
187
303
                                "result=no_iterator logical_field={} column_index={} "
188
303
                                "reason=slot_iterator_missing",
189
303
                                field_name, column_index));
190
303
        }
191
304
        if (!is_variant_subcolumn) {
192
1
            return Status::Error<ErrorCode::INVERTED_INDEX_NOT_SUPPORTED>(
193
1
                    "SEARCH index unavailable for field '{}'", field_name);
194
1
        }
195
303
        RETURN_IF_ERROR(verify_missing_variant_path(*binding, column_index, index_context));
196
202
        return Status::OK();
197
303
    }
198
199
2.71k
    const auto* storage_name_type =
200
2.71k
            index_context->get_storage_name_and_type_by_column_id(column_index);
201
2.71k
    if (storage_name_type == nullptr) {
202
1
        return Status::InternalError("storage_name_type not found for column {} in {}",
203
1
                                     column_index, expr.expr_name());
204
1
    }
205
206
2.71k
    bundle->iterators.emplace(field_name, iterator);
207
2.71k
    bundle->field_types.emplace(field_name, *storage_name_type);
208
2.71k
    bundle->column_indexes.emplace_back(column_index);
209
2.71k
    if (is_variant_subcolumn) {
210
185
        add_search_binding_diagnostic(
211
185
                index_context,
212
185
                fmt::format("[VariantSearchBinding] phase=collect_inputs "
213
185
                            "result=direct_iterator logical_field={} column_index={} "
214
185
                            "stored_field={}",
215
185
                            field_name, column_index, storage_name_type->first));
216
185
    }
217
2.71k
    return Status::OK();
218
2.71k
}
219
220
Status bind_nested_element_at(const VSearchExpr& expr, const VExpr& child,
221
                              const TSearchFieldBinding& binding,
222
0
                              const IndexExecContext& index_context, SearchInputBundle* bundle) {
223
0
    if (expr.get_search_param().root.clause_type != "NESTED") {
224
0
        return Status::OK();
225
0
    }
226
0
    const VExpr* root_expr = &child;
227
0
    while (root_expr->expr_name() == "element_at") {
228
0
        if (root_expr->children().empty()) {
229
0
            return Status::InvalidArgument("NESTED query has an incomplete VARIANT path");
230
0
        }
231
0
        root_expr = root_expr->children().front().get();
232
0
    }
233
0
    if (!root_expr->is_slot_ref()) {
234
0
        return Status::InvalidArgument("NESTED query requires a slot reference root");
235
0
    }
236
0
    return bind_nested_column(expr, *assert_cast<const VSlotRef*>(root_expr), &binding,
237
0
                              index_context, bundle);
238
0
}
239
240
0
const VSlotRef& variant_root_slot(const VExpr& expr) {
241
0
    const VExpr* root = &expr;
242
0
    while (root->expr_name() == "element_at") {
243
0
        DORIS_CHECK(!root->children().empty());
244
0
        root = root->children().front().get();
245
0
    }
246
0
    DORIS_CHECK(root->is_slot_ref());
247
0
    return *assert_cast<const VSlotRef*>(root);
248
0
}
249
250
Status collect_search_inputs(const VSearchExpr& expr, VExprContext* context,
251
2.18k
                             SearchInputBundle* bundle) {
252
2.18k
    DCHECK(bundle != nullptr);
253
254
2.18k
    auto index_context = context->get_index_context();
255
2.18k
    if (index_context == nullptr) {
256
0
        LOG(WARNING) << "collect_search_inputs: No inverted index context available";
257
0
        return Status::InternalError("No inverted index context available");
258
0
    }
259
260
2.18k
    const auto& search_param = expr.get_search_param();
261
2.18k
    const auto& field_bindings = search_param.field_bindings;
262
263
2.18k
    size_t child_index = 0;
264
3.02k
    for (const auto& child : expr.children()) {
265
3.02k
        if (child->is_slot_ref()) {
266
3.02k
            auto* column_slot_ref = assert_cast<VSlotRef*>(child.get());
267
3.02k
            const TSearchFieldBinding* binding =
268
3.02k
                    child_index < field_bindings.size() ? &field_bindings[child_index] : nullptr;
269
3.02k
            RETURN_IF_ERROR(collect_slot_search_input(expr, *column_slot_ref, binding,
270
3.02k
                                                      index_context.get(), bundle));
271
2.92k
            ++child_index;
272
2.92k
        } else if (child->is_literal()) {
273
0
            auto* literal = assert_cast<VLiteral*>(child.get());
274
0
            bundle->literal_args.emplace_back(literal->get_column_ptr(), literal->get_data_type(),
275
0
                                              literal->expr_name());
276
1
        } else {
277
            // Check if this is ElementAt expression (for variant subcolumn access)
278
1
            if (child->expr_name() == "element_at" && child_index < field_bindings.size() &&
279
1
                field_bindings[child_index].__isset.is_variant_subcolumn &&
280
1
                field_bindings[child_index].is_variant_subcolumn) {
281
0
                RETURN_IF_ERROR(bind_nested_element_at(expr, *child, field_bindings[child_index],
282
0
                                                       *index_context, bundle));
283
0
                const auto& root_slot = variant_root_slot(*child);
284
                // Variant subcolumn not materialized - skip, will create empty BitSetQuery in function_search
285
0
                add_search_binding_diagnostic(
286
0
                        index_context.get(),
287
0
                        fmt::format("[VariantSearchBinding] phase=collect_inputs "
288
0
                                    "result=unmaterialized_element_at logical_field={} "
289
0
                                    "parent_field={} sub_path={} reason=no_slot_ref",
290
0
                                    field_bindings[child_index].field_name,
291
0
                                    field_bindings[child_index].__isset.parent_field_name
292
0
                                            ? field_bindings[child_index].parent_field_name
293
0
                                            : "",
294
0
                                    field_bindings[child_index].__isset.subcolumn_path
295
0
                                            ? field_bindings[child_index].subcolumn_path
296
0
                                            : ""));
297
0
                RETURN_IF_ERROR(verify_missing_variant_path(
298
0
                        field_bindings[child_index], root_slot.column_id(), index_context.get()));
299
0
                ++child_index;
300
0
                continue;
301
0
            }
302
303
            // Not a supported child type
304
1
            return Status::InvalidArgument("Unsupported child node type: {}", child->expr_name());
305
1
        }
306
3.02k
    }
307
308
2.08k
    return Status::OK();
309
2.18k
}
310
311
116
bool search_status_allows_row_fallback(const Status& status) {
312
116
    DORIS_CHECK(!status.ok());
313
116
    return status.is<ErrorCode::INVERTED_INDEX_BYPASS>() ||
314
116
           status.is<ErrorCode::INVERTED_INDEX_EVALUATE_SKIPPED>() ||
315
116
           status.is<ErrorCode::INVERTED_INDEX_FILE_CORRUPTED>() ||
316
116
           status.is<ErrorCode::INVERTED_INDEX_FILE_NOT_FOUND>() ||
317
116
           status.is<ErrorCode::NOT_IMPLEMENTED_ERROR>();
318
116
}
319
320
116
Status prevent_search_row_fallback(Status status) {
321
116
    DORIS_CHECK(!status.ok());
322
116
    if (!search_status_allows_row_fallback(status)) {
323
111
        return status;
324
111
    }
325
5
    return Status::Error<ErrorCode::INVERTED_INDEX_NOT_SUPPORTED>(
326
5
            "SEARCH cannot fall back to row execution: {}", status.to_string());
327
116
}
328
329
} // namespace
330
331
1.34k
VSearchExpr::VSearchExpr(const TExprNode& node) : VExpr(node) {
332
1.34k
    if (node.__isset.search_param) {
333
1.34k
        _search_param = node.search_param;
334
1.34k
        _original_dsl = _search_param.original_dsl;
335
1.34k
    }
336
1.34k
}
337
338
Status VSearchExpr::prepare(RuntimeState* state, const RowDescriptor& row_desc,
339
1.24k
                            VExprContext* context) {
340
1.24k
    RETURN_IF_ERROR(VExpr::prepare(state, row_desc, context));
341
1.24k
    const auto& query_options = state->query_options();
342
1.24k
    if (query_options.__isset.enable_inverted_index_query_cache) {
343
1.24k
        _enable_cache = query_options.enable_inverted_index_query_cache;
344
1.24k
    }
345
1.24k
    return Status::OK();
346
1.24k
}
347
348
398
const std::string& VSearchExpr::expr_name() const {
349
398
    static const std::string name = "VSearchExpr";
350
398
    return name;
351
398
}
352
353
Status VSearchExpr::execute_column(VExprContext* context, const Block* block, Selector* selector,
354
9
                                   size_t count, ColumnPtr& result_column) const {
355
9
    if (fast_execute(context, selector, count, result_column)) {
356
7
        return Status::OK();
357
7
    }
358
359
2
    return Status::InternalError("SearchExpr should not be executed without inverted index");
360
9
}
361
362
2.22k
Status VSearchExpr::evaluate_inverted_index(VExprContext* context, uint32_t segment_num_rows) {
363
2.22k
    if (_search_param.original_dsl.empty()) {
364
3
        return prevent_search_row_fallback(Status::InvalidArgument("search DSL is empty"));
365
3
    }
366
367
2.22k
    auto index_context = context->get_index_context();
368
2.22k
    if (!index_context) {
369
15
        LOG(WARNING) << "VSearchExpr: No inverted index context available";
370
15
        return Status::OK();
371
15
    }
372
373
2.20k
    SearchInputBundle bundle;
374
2.20k
    if (auto status = collect_search_inputs(*this, context, &bundle); !status.ok()) {
375
105
        return prevent_search_row_fallback(std::move(status));
376
105
    }
377
378
18.4E
    VLOG_DEBUG << "VSearchExpr: bundle.iterators.size()=" << bundle.iterators.size();
379
380
2.10k
    auto index_query_context = index_context->get_index_query_context();
381
382
2.10k
    auto function = std::make_shared<FunctionSearch>();
383
2.10k
    auto result_bitmap = InvertedIndexResultBitmap();
384
2.10k
    auto status = function->evaluate_inverted_index_with_search_param(
385
2.10k
            _search_param, bundle.field_types, bundle.iterators, segment_num_rows, result_bitmap,
386
2.10k
            _enable_cache, index_context.get(), bundle.nested_column, index_query_context);
387
388
2.10k
    if (!status.ok()) {
389
8
        LOG(WARNING) << "VSearchExpr: Function evaluation failed: " << status.to_string();
390
8
        return prevent_search_row_fallback(std::move(status));
391
8
    }
392
393
2.09k
    index_context->set_index_result_for_expr(this, result_bitmap);
394
2.69k
    for (int column_index : bundle.column_indexes) {
395
2.69k
        index_context->set_true_for_index_status(this, column_index);
396
2.69k
    }
397
398
2.09k
    return Status::OK();
399
2.10k
}
400
401
} // namespace doris