Coverage Report

Created: 2026-07-15 06:34

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/exprs/function/function_search.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/function/function_search.h"
19
20
#include <CLucene/config/repl_wchar.h>
21
#include <CLucene/search/Scorer.h>
22
#include <fmt/format.h>
23
#include <gen_cpp/Exprs_types.h>
24
#include <glog/logging.h>
25
26
#include <limits>
27
#include <memory>
28
#include <roaring/roaring.hh>
29
#include <set>
30
#include <string>
31
#include <unordered_map>
32
#include <unordered_set>
33
#include <vector>
34
35
#include "common/status.h"
36
#include "core/block/columns_with_type_and_name.h"
37
#include "core/column/column_const.h"
38
#include "core/data_type/data_type_array.h"
39
#include "core/data_type/data_type_nullable.h"
40
#include "core/data_type/data_type_string.h"
41
#include "exprs/function/simple_function_factory.h"
42
#include "exprs/function/variant_inverted_index_search.h"
43
#include "exprs/vexpr_context.h"
44
#include "runtime/runtime_profile.h"
45
#include "storage/index/index_file_reader.h"
46
#include "storage/index/index_query_context.h"
47
#include "storage/index/inverted/analyzer/analyzer.h"
48
#include "storage/index/inverted/inverted_index_compound_reader.h"
49
#include "storage/index/inverted/inverted_index_iterator.h"
50
#include "storage/index/inverted/inverted_index_parser.h"
51
#include "storage/index/inverted/inverted_index_reader.h"
52
#include "storage/index/inverted/inverted_index_searcher.h"
53
#include "storage/index/inverted/query/query_helper.h"
54
#include "storage/index/inverted/query_v2/all_query/all_query.h"
55
#include "storage/index/inverted/query_v2/bit_set_query/bit_set_query.h"
56
#include "storage/index/inverted/query_v2/boolean_query/boolean_query_builder.h"
57
#include "storage/index/inverted/query_v2/boolean_query/operator.h"
58
#include "storage/index/inverted/query_v2/collect/doc_set_collector.h"
59
#include "storage/index/inverted/query_v2/collect/top_k_collector.h"
60
#include "storage/index/inverted/query_v2/phrase_query/multi_phrase_query.h"
61
#include "storage/index/inverted/query_v2/phrase_query/phrase_query.h"
62
#include "storage/index/inverted/query_v2/regexp_query/regexp_query.h"
63
#include "storage/index/inverted/query_v2/term_query/term_query.h"
64
#include "storage/index/inverted/query_v2/wildcard_query/wildcard_query.h"
65
#include "storage/index/inverted/util/string_helper.h"
66
#include "storage/olap_common.h"
67
#include "storage/segment/variant/nested_group_provider.h"
68
#include "storage/types.h"
69
#include "util/debug_points.h"
70
#include "util/string_parser.hpp"
71
#include "util/string_util.h"
72
#include "util/thrift_util.h"
73
74
namespace doris {
75
76
// Build canonical DSL signature for cache key.
77
// Serializes the entire TSearchParam via Thrift binary protocol so that
78
// every field (DSL, AST root, field bindings, default_operator,
79
// minimum_should_match, etc.) is included automatically.
80
1.23k
static std::string build_dsl_signature(const TSearchParam& param) {
81
1.23k
    ThriftSerializer ser(false, 1024);
82
1.23k
    TSearchParam copy = param;
83
1.23k
    std::string sig;
84
1.23k
    auto st = ser.serialize(&copy, &sig);
85
1.23k
    if (UNLIKELY(!st.ok())) {
86
0
        LOG(WARNING) << "build_dsl_signature: Thrift serialization failed: " << st.to_string()
87
0
                     << ", caching disabled for this query";
88
0
        return "";
89
0
    }
90
1.23k
    return sig;
91
1.23k
}
92
93
// Extract segment path prefix from the first available inverted index iterator.
94
// All fields in the same segment share the same path prefix.
95
static std::string extract_segment_prefix(
96
1.22k
        const std::unordered_map<std::string, IndexIterator*>& iterators) {
97
1.22k
    for (const auto& [field_name, iter] : iterators) {
98
1.22k
        auto* inv_iter = dynamic_cast<InvertedIndexIterator*>(iter);
99
1.22k
        if (!inv_iter) continue;
100
        // Try fulltext reader first, then string type
101
1.20k
        for (auto type :
102
1.53k
             {InvertedIndexReaderType::FULLTEXT, InvertedIndexReaderType::STRING_TYPE}) {
103
1.53k
            IndexReaderType reader_type = type;
104
1.53k
            auto reader = inv_iter->get_reader(reader_type);
105
1.53k
            if (!reader) continue;
106
1.20k
            auto inv_reader = std::dynamic_pointer_cast<InvertedIndexReader>(reader);
107
1.20k
            if (!inv_reader) continue;
108
1.20k
            auto file_reader = inv_reader->get_index_file_reader();
109
1.20k
            if (!file_reader) continue;
110
1.20k
            return file_reader->get_index_path_prefix();
111
1.20k
        }
112
1.20k
    }
113
18.4E
    VLOG_DEBUG << "extract_segment_prefix: no suitable inverted index reader found across "
114
18.4E
               << iterators.size() << " iterators, caching disabled for this query";
115
21
    return "";
116
1.22k
}
117
118
namespace {
119
120
3
bool is_nested_group_search_supported() {
121
3
    auto provider = segment_v2::create_nested_group_read_provider();
122
3
    return provider != nullptr && provider->should_enable_nested_group_read_path();
123
3
}
124
125
2
query_v2::QueryPtr make_unknown_query(uint32_t num_rows) {
126
2
    auto null_bitmap = std::make_shared<roaring::Roaring>();
127
2
    if (num_rows > 0) {
128
2
        null_bitmap->addRange(0, num_rows);
129
2
    }
130
2
    return std::make_shared<query_v2::BitSetQuery>(std::make_shared<roaring::Roaring>(),
131
2
                                                   std::move(null_bitmap));
132
2
}
133
134
2
DataTypePtr unwrap_direct_index_value_type(DataTypePtr column_type) {
135
2
    DataTypePtr value_type = remove_nullable(std::move(column_type));
136
5
    while (value_type != nullptr &&
137
5
           value_type->get_storage_field_type() == FieldType::OLAP_FIELD_TYPE_ARRAY) {
138
3
        const auto* array_type = dynamic_cast<const DataTypeArray*>(value_type.get());
139
3
        if (array_type == nullptr) {
140
0
            return value_type;
141
0
        }
142
3
        value_type = remove_nullable(array_type->get_nested_type());
143
3
    }
144
2
    return value_type;
145
2
}
146
147
template <PrimitiveType primitive_type, typename CppType>
148
1
Status parse_integral_search_value(const std::string& value, Field* field) {
149
1
    StringParser::ParseResult parse_result = StringParser::PARSE_FAILURE;
150
1
    CppType parsed =
151
1
            StringParser::string_to_int<CppType>(value.data(), value.size(), &parse_result);
152
1
    if (parse_result != StringParser::PARSE_SUCCESS) {
153
0
        return Status::InvalidArgument("failed to parse '{}' as {}", value,
154
0
                                       type_to_string(primitive_type));
155
0
    }
156
1
    *field = Field::create_field<primitive_type>(parsed);
157
1
    return Status::OK();
158
1
}
Unexecuted instantiation: function_search.cpp:_ZN5doris12_GLOBAL__N_127parse_integral_search_valueILNS_13PrimitiveTypeE3EaEENS_6StatusERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEPNS_5FieldE
Unexecuted instantiation: function_search.cpp:_ZN5doris12_GLOBAL__N_127parse_integral_search_valueILNS_13PrimitiveTypeE4EsEENS_6StatusERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEPNS_5FieldE
function_search.cpp:_ZN5doris12_GLOBAL__N_127parse_integral_search_valueILNS_13PrimitiveTypeE5EiEENS_6StatusERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEPNS_5FieldE
Line
Count
Source
148
1
Status parse_integral_search_value(const std::string& value, Field* field) {
149
1
    StringParser::ParseResult parse_result = StringParser::PARSE_FAILURE;
150
1
    CppType parsed =
151
1
            StringParser::string_to_int<CppType>(value.data(), value.size(), &parse_result);
152
1
    if (parse_result != StringParser::PARSE_SUCCESS) {
153
0
        return Status::InvalidArgument("failed to parse '{}' as {}", value,
154
0
                                       type_to_string(primitive_type));
155
0
    }
156
1
    *field = Field::create_field<primitive_type>(parsed);
157
1
    return Status::OK();
158
1
}
Unexecuted instantiation: function_search.cpp:_ZN5doris12_GLOBAL__N_127parse_integral_search_valueILNS_13PrimitiveTypeE6ElEENS_6StatusERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEPNS_5FieldE
Unexecuted instantiation: function_search.cpp:_ZN5doris12_GLOBAL__N_127parse_integral_search_valueILNS_13PrimitiveTypeE7EnEENS_6StatusERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEPNS_5FieldE
159
160
Status parse_scalar_search_value(const DataTypePtr& column_type, const std::string& value,
161
2
                                 Field* field) {
162
2
    if (column_type == nullptr || field == nullptr) {
163
0
        return Status::InvalidArgument("missing column type for scalar search value");
164
0
    }
165
166
2
    switch (column_type->get_storage_field_type()) {
167
1
    case FieldType::OLAP_FIELD_TYPE_BOOL: {
168
1
        StringParser::ParseResult parse_result = StringParser::PARSE_FAILURE;
169
1
        bool parsed = StringParser::string_to_bool(value.data(), value.size(), &parse_result);
170
1
        if (parse_result != StringParser::PARSE_SUCCESS) {
171
0
            return Status::InvalidArgument("failed to parse '{}' as bool", value);
172
0
        }
173
1
        *field = Field::create_field<TYPE_BOOLEAN>(parsed);
174
1
        return Status::OK();
175
1
    }
176
0
    case FieldType::OLAP_FIELD_TYPE_TINYINT:
177
0
        return parse_integral_search_value<TYPE_TINYINT, Int8>(value, field);
178
0
    case FieldType::OLAP_FIELD_TYPE_SMALLINT:
179
0
        return parse_integral_search_value<TYPE_SMALLINT, Int16>(value, field);
180
1
    case FieldType::OLAP_FIELD_TYPE_INT:
181
1
        return parse_integral_search_value<TYPE_INT, Int32>(value, field);
182
0
    case FieldType::OLAP_FIELD_TYPE_BIGINT:
183
0
        return parse_integral_search_value<TYPE_BIGINT, Int64>(value, field);
184
0
    case FieldType::OLAP_FIELD_TYPE_LARGEINT:
185
0
        return parse_integral_search_value<TYPE_LARGEINT, Int128>(value, field);
186
0
    case FieldType::OLAP_FIELD_TYPE_FLOAT: {
187
0
        StringParser::ParseResult parse_result = StringParser::PARSE_FAILURE;
188
0
        Float32 parsed =
189
0
                StringParser::string_to_float<Float32>(value.data(), value.size(), &parse_result);
190
0
        if (parse_result != StringParser::PARSE_SUCCESS) {
191
0
            return Status::InvalidArgument("failed to parse '{}' as float", value);
192
0
        }
193
0
        *field = Field::create_field<TYPE_FLOAT>(parsed);
194
0
        return Status::OK();
195
0
    }
196
0
    case FieldType::OLAP_FIELD_TYPE_DOUBLE: {
197
0
        StringParser::ParseResult parse_result = StringParser::PARSE_FAILURE;
198
0
        Float64 parsed =
199
0
                StringParser::string_to_float<Float64>(value.data(), value.size(), &parse_result);
200
0
        if (parse_result != StringParser::PARSE_SUCCESS) {
201
0
            return Status::InvalidArgument("failed to parse '{}' as double", value);
202
0
        }
203
0
        *field = Field::create_field<TYPE_DOUBLE>(parsed);
204
0
        return Status::OK();
205
0
    }
206
0
    default:
207
0
        return Status::NotSupported("scalar search does not support storage field type {}",
208
0
                                    static_cast<int>(column_type->get_storage_field_type()));
209
2
    }
210
2
}
211
212
3
InvertedIndexQueryType direct_index_query_type_for_clause(const std::string& clause_type) {
213
3
    if (clause_type == "TERM" || clause_type == "EXACT") {
214
2
        return InvertedIndexQueryType::EQUAL_QUERY;
215
2
    }
216
1
    return InvertedIndexQueryType::UNKNOWN_QUERY;
217
3
}
218
219
} // namespace
220
221
Status FunctionSearch::execute_impl(FunctionContext* /*context*/, Block& /*block*/,
222
                                    const ColumnNumbers& /*arguments*/, uint32_t /*result*/,
223
4
                                    size_t /*input_rows_count*/) const {
224
4
    return Status::RuntimeError("only inverted index queries are supported");
225
4
}
226
227
// Enhanced implementation: Handle new parameter structure (DSL + SlotReferences)
228
Status FunctionSearch::evaluate_inverted_index(
229
        const ColumnsWithTypeAndName& arguments,
230
        const std::vector<IndexFieldNameAndTypePair>& data_type_with_names,
231
        std::vector<IndexIterator*> iterators, uint32_t num_rows,
232
        const InvertedIndexAnalyzerCtx* /*analyzer_ctx*/,
233
1
        InvertedIndexResultBitmap& bitmap_result) const {
234
1
    return Status::OK();
235
1
}
236
237
Status FunctionSearch::evaluate_inverted_index_with_search_param(
238
        const TSearchParam& search_param,
239
        const std::unordered_map<std::string, IndexFieldNameAndTypePair>& data_type_with_names,
240
        std::unordered_map<std::string, IndexIterator*> iterators, uint32_t num_rows,
241
31
        InvertedIndexResultBitmap& bitmap_result, bool enable_cache) const {
242
31
    static const std::unordered_map<std::string, int> empty_field_to_column_id;
243
31
    return evaluate_inverted_index_with_search_param(
244
31
            search_param, data_type_with_names, std::move(iterators), num_rows, bitmap_result,
245
31
            enable_cache, nullptr, empty_field_to_column_id);
246
31
}
247
248
Status FunctionSearch::evaluate_inverted_index_with_search_param(
249
        const TSearchParam& search_param,
250
        const std::unordered_map<std::string, IndexFieldNameAndTypePair>& data_type_with_names,
251
        std::unordered_map<std::string, IndexIterator*> iterators, uint32_t num_rows,
252
        InvertedIndexResultBitmap& bitmap_result, bool enable_cache,
253
        const IndexExecContext* index_exec_ctx,
254
        const std::unordered_map<std::string, int>& field_name_to_column_id,
255
1.25k
        const std::shared_ptr<IndexQueryContext>& index_query_context) const {
256
1.25k
    const bool is_nested_query = search_param.root.clause_type == "NESTED";
257
1.25k
    if (is_nested_query && !is_nested_group_search_supported()) {
258
3
        return Status::NotSupported(
259
3
                "NESTED query requires NestedGroup support, which is unavailable in this build");
260
3
    }
261
262
1.24k
    if (!is_nested_query && (iterators.empty() || data_type_with_names.empty())) {
263
5
        LOG(INFO) << "No indexed columns or iterators available, returning empty result, dsl:"
264
5
                  << search_param.original_dsl;
265
5
        bitmap_result = InvertedIndexResultBitmap(std::make_shared<roaring::Roaring>(),
266
5
                                                  std::make_shared<roaring::Roaring>());
267
5
        return Status::OK();
268
5
    }
269
270
    // Track overall query time (equivalent to inverted_index_query_timer in MATCH path).
271
    // Must be declared before the DSL cache lookup so that cache-hit fast paths are
272
    // also covered by the timer.
273
1.24k
    int64_t query_timer_dummy = 0;
274
1.24k
    OlapReaderStatistics* outer_stats = index_query_context ? index_query_context->stats : nullptr;
275
1.24k
    SCOPED_RAW_TIMER(outer_stats ? &outer_stats->inverted_index_query_timer : &query_timer_dummy);
276
277
1.24k
    const bool need_similarity_score =
278
1.24k
            index_query_context && index_query_context->collection_similarity;
279
280
    // DSL result cache only stores bitmap/null bitmap. It does not store BM25 scores,
281
    // so score() queries must execute scorers to populate CollectionSimilarity.
282
1.24k
    auto* dsl_cache = (enable_cache && !need_similarity_score) ? InvertedIndexQueryCache::instance()
283
1.24k
                                                               : nullptr;
284
1.24k
    std::string seg_prefix;
285
1.24k
    std::string dsl_sig;
286
1.24k
    InvertedIndexQueryCache::CacheKey dsl_cache_key;
287
1.24k
    bool cache_usable = false;
288
1.24k
    if (dsl_cache) {
289
1.22k
        seg_prefix = extract_segment_prefix(iterators);
290
1.22k
        dsl_sig = build_dsl_signature(search_param);
291
1.22k
        if (!seg_prefix.empty() && !dsl_sig.empty()) {
292
1.20k
            dsl_cache_key = InvertedIndexQueryCache::CacheKey {
293
1.20k
                    seg_prefix, "__search_dsl__", InvertedIndexQueryType::SEARCH_DSL_QUERY,
294
1.20k
                    dsl_sig};
295
1.20k
            cache_usable = true;
296
1.20k
            InvertedIndexQueryCacheHandle dsl_cache_handle;
297
1.20k
            bool dsl_hit = false;
298
1.20k
            {
299
1.20k
                int64_t lookup_dummy = 0;
300
1.20k
                SCOPED_RAW_TIMER(outer_stats ? &outer_stats->inverted_index_lookup_timer
301
1.20k
                                             : &lookup_dummy);
302
1.20k
                dsl_hit = dsl_cache->lookup(dsl_cache_key, &dsl_cache_handle);
303
1.20k
            }
304
1.20k
            if (dsl_hit) {
305
168
                auto cached_bitmap = dsl_cache_handle.get_bitmap();
306
168
                if (cached_bitmap) {
307
168
                    if (outer_stats) {
308
168
                        outer_stats->inverted_index_query_cache_hit++;
309
168
                    }
310
                    // Also retrieve cached null bitmap for three-valued SQL logic
311
                    // (needed by compound operators NOT, OR, AND in VCompoundPred)
312
168
                    auto null_cache_key = InvertedIndexQueryCache::CacheKey {
313
168
                            seg_prefix, "__search_dsl__", InvertedIndexQueryType::SEARCH_DSL_QUERY,
314
168
                            dsl_sig + "__null"};
315
168
                    InvertedIndexQueryCacheHandle null_cache_handle;
316
168
                    std::shared_ptr<roaring::Roaring> null_bitmap;
317
168
                    if (dsl_cache->lookup(null_cache_key, &null_cache_handle)) {
318
168
                        null_bitmap = null_cache_handle.get_bitmap();
319
168
                    }
320
168
                    if (!null_bitmap) {
321
0
                        null_bitmap = std::make_shared<roaring::Roaring>();
322
0
                    }
323
168
                    bitmap_result =
324
168
                            InvertedIndexResultBitmap(cached_bitmap, std::move(null_bitmap));
325
168
                    return Status::OK();
326
168
                }
327
168
            }
328
1.04k
            if (outer_stats) {
329
1.04k
                outer_stats->inverted_index_query_cache_miss++;
330
1.04k
            }
331
1.03k
        }
332
1.22k
    }
333
334
1.07k
    std::shared_ptr<IndexQueryContext> context;
335
1.07k
    if (index_query_context) {
336
1.04k
        context = index_query_context;
337
1.04k
    } else {
338
30
        context = std::make_shared<IndexQueryContext>();
339
30
        context->collection_statistics = std::make_shared<CollectionStatistics>();
340
30
        context->collection_similarity = std::make_shared<CollectionSimilarity>();
341
30
    }
342
343
1.07k
    const auto* effective_data_type_with_names = &data_type_with_names;
344
345
    // Pass field_bindings to resolver for variant subcolumn detection
346
1.07k
    FieldReaderResolver resolver(*effective_data_type_with_names, iterators, context,
347
1.07k
                                 search_param.field_bindings);
348
349
1.07k
    if (is_nested_query) {
350
0
        std::shared_ptr<roaring::Roaring> row_bitmap;
351
0
        VariantNestedSearchEvaluator nested_evaluator(*this);
352
0
        RETURN_IF_ERROR(nested_evaluator.evaluate(search_param, search_param.root, context,
353
0
                                                  resolver, num_rows, index_exec_ctx,
354
0
                                                  field_name_to_column_id, row_bitmap));
355
0
        bitmap_result = InvertedIndexResultBitmap(std::move(row_bitmap),
356
0
                                                  std::make_shared<roaring::Roaring>());
357
0
        bitmap_result.mask_out_null();
358
0
        return Status::OK();
359
0
    }
360
361
    // Extract default_operator from TSearchParam (default: "or")
362
1.07k
    std::string default_operator = "or";
363
1.07k
    if (search_param.__isset.default_operator && !search_param.default_operator.empty()) {
364
1.04k
        default_operator = search_param.default_operator;
365
1.04k
    }
366
    // Extract minimum_should_match from TSearchParam (-1 means not set)
367
1.07k
    int32_t minimum_should_match = -1;
368
1.07k
    if (search_param.__isset.minimum_should_match) {
369
48
        minimum_should_match = search_param.minimum_should_match;
370
48
    }
371
372
1.07k
    auto* stats = context->stats;
373
1.07k
    int64_t dummy_timer = 0;
374
1.07k
    SCOPED_RAW_TIMER(stats ? &stats->inverted_index_searcher_search_timer : &dummy_timer);
375
376
1.07k
    query_v2::QueryPtr root_query;
377
1.07k
    std::string root_binding_key;
378
1.07k
    {
379
1.07k
        int64_t init_dummy = 0;
380
1.07k
        SCOPED_RAW_TIMER(stats ? &stats->inverted_index_searcher_search_init_timer : &init_dummy);
381
1.07k
        RETURN_IF_ERROR(build_query_recursive(search_param.root, context, resolver, &root_query,
382
1.07k
                                              &root_binding_key, default_operator,
383
1.07k
                                              minimum_should_match, num_rows));
384
1.07k
    }
385
1.05k
    if (root_query == nullptr) {
386
0
        LOG(INFO) << "search: Query tree resolved to empty query, dsl:"
387
0
                  << search_param.original_dsl;
388
0
        bitmap_result = InvertedIndexResultBitmap(std::make_shared<roaring::Roaring>(),
389
0
                                                  std::make_shared<roaring::Roaring>());
390
0
        return Status::OK();
391
0
    }
392
393
1.05k
    VariantSearchNullBitmapAdapter null_resolver(resolver);
394
1.05k
    query_v2::QueryExecutionContext exec_ctx =
395
1.05k
            build_variant_search_query_execution_context(num_rows, resolver, &null_resolver);
396
397
1.05k
    bool enable_scoring = false;
398
1.05k
    bool is_asc = false;
399
1.05k
    size_t top_k = 0;
400
1.05k
    if (index_query_context) {
401
1.04k
        enable_scoring = index_query_context->collection_similarity != nullptr;
402
1.04k
        is_asc = index_query_context->is_asc;
403
1.04k
        top_k = index_query_context->query_limit;
404
1.04k
    }
405
406
1.05k
    auto weight = root_query->weight(enable_scoring);
407
1.05k
    if (!weight) {
408
0
        LOG(WARNING) << "search: Failed to build query weight";
409
0
        bitmap_result = InvertedIndexResultBitmap(std::make_shared<roaring::Roaring>(),
410
0
                                                  std::make_shared<roaring::Roaring>());
411
0
        return Status::OK();
412
0
    }
413
414
1.05k
    std::shared_ptr<roaring::Roaring> roaring = std::make_shared<roaring::Roaring>();
415
1.05k
    {
416
1.05k
        int64_t exec_dummy = 0;
417
1.05k
        SCOPED_RAW_TIMER(stats ? &stats->inverted_index_searcher_search_exec_timer : &exec_dummy);
418
1.05k
        if (enable_scoring && !is_asc && top_k > 0) {
419
5
            bool use_wand = index_query_context->runtime_state != nullptr &&
420
5
                            index_query_context->runtime_state->query_options()
421
5
                                    .enable_inverted_index_wand_query;
422
5
            query_v2::collect_multi_segment_top_k(
423
5
                    weight, exec_ctx, root_binding_key, top_k, roaring,
424
5
                    index_query_context->collection_similarity, use_wand);
425
1.04k
        } else {
426
1.04k
            query_v2::collect_multi_segment_doc_set(
427
1.04k
                    weight, exec_ctx, root_binding_key, roaring,
428
1.04k
                    index_query_context ? index_query_context->collection_similarity : nullptr,
429
1.04k
                    enable_scoring);
430
1.04k
        }
431
1.05k
    }
432
433
18.4E
    VLOG_DEBUG << "search: Query completed, matched " << roaring->cardinality() << " documents";
434
435
    // Extract NULL bitmap from three-valued logic scorer
436
    // The scorer correctly computes which documents evaluate to NULL based on query logic
437
    // For example: TRUE OR NULL = TRUE (not NULL), FALSE OR NULL = NULL
438
1.05k
    std::shared_ptr<roaring::Roaring> null_bitmap = std::make_shared<roaring::Roaring>();
439
1.05k
    if (exec_ctx.null_resolver) {
440
1.05k
        auto scorer = weight->scorer(exec_ctx, root_binding_key);
441
1.05k
        if (scorer && scorer->has_null_bitmap(exec_ctx.null_resolver)) {
442
208
            const auto* bitmap = scorer->get_null_bitmap(exec_ctx.null_resolver);
443
208
            if (bitmap != nullptr) {
444
207
                *null_bitmap = *bitmap;
445
18.4E
                VLOG_TRACE << "search: Extracted NULL bitmap with " << null_bitmap->cardinality()
446
18.4E
                           << " NULL documents";
447
207
            }
448
208
        }
449
1.05k
    }
450
451
18.4E
    VLOG_TRACE << "search: Before mask - true_bitmap=" << roaring->cardinality()
452
18.4E
               << ", null_bitmap=" << null_bitmap->cardinality();
453
454
    // Create result and mask out NULLs (SQL WHERE clause semantics: only TRUE rows)
455
1.05k
    bitmap_result = InvertedIndexResultBitmap(std::move(roaring), std::move(null_bitmap));
456
1.05k
    bitmap_result.mask_out_null();
457
458
18.4E
    VLOG_TRACE << "search: After mask - result_bitmap="
459
18.4E
               << bitmap_result.get_data_bitmap()->cardinality();
460
461
    // Insert post-mask_out_null result into DSL cache for future reuse
462
    // Cache both data bitmap and null bitmap so compound operators (NOT, OR, AND)
463
    // can apply correct three-valued SQL logic on cache hit
464
1.05k
    if (dsl_cache && cache_usable) {
465
1.04k
        InvertedIndexQueryCacheHandle insert_handle;
466
1.04k
        dsl_cache->insert(dsl_cache_key, bitmap_result.get_data_bitmap(), &insert_handle);
467
1.04k
        if (bitmap_result.get_null_bitmap()) {
468
1.04k
            auto null_cache_key = InvertedIndexQueryCache::CacheKey {
469
1.04k
                    seg_prefix, "__search_dsl__", InvertedIndexQueryType::SEARCH_DSL_QUERY,
470
1.04k
                    dsl_sig + "__null"};
471
1.04k
            InvertedIndexQueryCacheHandle null_insert_handle;
472
1.04k
            dsl_cache->insert(null_cache_key, bitmap_result.get_null_bitmap(), &null_insert_handle);
473
1.04k
        }
474
1.04k
    }
475
476
1.05k
    return Status::OK();
477
1.05k
}
478
479
// Aligned with FE QsClauseType enum - uses enum.name() as clause_type
480
FunctionSearch::ClauseTypeCategory FunctionSearch::get_clause_type_category(
481
7.07k
        const std::string& clause_type) const {
482
7.07k
    if (clause_type == "AND" || clause_type == "OR" || clause_type == "NOT" ||
483
7.07k
        clause_type == "OCCUR_BOOLEAN" || clause_type == "NESTED") {
484
146
        return ClauseTypeCategory::COMPOUND;
485
6.92k
    } else if (clause_type == "TERM" || clause_type == "PREFIX" || clause_type == "WILDCARD" ||
486
6.92k
               clause_type == "REGEXP" || clause_type == "RANGE" || clause_type == "LIST" ||
487
6.92k
               clause_type == "EXACT") {
488
        // Non-tokenized queries: exact matching, pattern matching, range, list operations
489
6.66k
        return ClauseTypeCategory::NON_TOKENIZED;
490
6.66k
    } else if (clause_type == "PHRASE" || clause_type == "MATCH" || clause_type == "ANY" ||
491
270
               clause_type == "ALL") {
492
        // Tokenized queries: phrase search, full-text search, multi-value matching
493
        // Note: ANY and ALL require tokenization of their input values
494
270
        return ClauseTypeCategory::TOKENIZED;
495
18.4E
    } else {
496
        // Default to NON_TOKENIZED for unknown types
497
18.4E
        LOG(WARNING) << "Unknown clause type '" << clause_type
498
18.4E
                     << "', defaulting to NON_TOKENIZED category";
499
18.4E
        return ClauseTypeCategory::NON_TOKENIZED;
500
18.4E
    }
501
7.07k
}
502
503
// Analyze query type for a specific field in the search clause
504
InvertedIndexQueryType FunctionSearch::analyze_field_query_type(const std::string& field_name,
505
5.28k
                                                                const TSearchClause& clause) const {
506
5.28k
    const std::string& clause_type = clause.clause_type;
507
5.28k
    ClauseTypeCategory category = get_clause_type_category(clause_type);
508
509
    // Handle leaf queries - use direct mapping
510
5.28k
    if (category != ClauseTypeCategory::COMPOUND) {
511
        // Check if this clause targets the specific field
512
5.14k
        if (clause.field_name == field_name) {
513
            // Use direct mapping from clause_type to InvertedIndexQueryType
514
163
            return clause_type_to_query_type(clause_type);
515
163
        }
516
5.14k
    }
517
518
    // Handle boolean queries - recursively analyze children
519
5.11k
    if (!clause.children.empty()) {
520
5.09k
        for (const auto& child_clause : clause.children) {
521
            // Recursively analyze each child
522
5.09k
            InvertedIndexQueryType child_type = analyze_field_query_type(field_name, child_clause);
523
            // If this child targets the field (not default EQUAL_QUERY), return its query type
524
5.09k
            if (child_type != InvertedIndexQueryType::UNKNOWN_QUERY) {
525
124
                return child_type;
526
124
            }
527
5.09k
        }
528
132
    }
529
530
    // If no children target this field, return UNKNOWN_QUERY as default
531
4.99k
    return InvertedIndexQueryType::UNKNOWN_QUERY;
532
5.11k
}
533
534
// Map clause_type string to InvertedIndexQueryType
535
InvertedIndexQueryType FunctionSearch::clause_type_to_query_type(
536
2.00k
        const std::string& clause_type) const {
537
    // Use static map for better performance and maintainability
538
2.00k
    static const std::unordered_map<std::string, InvertedIndexQueryType> clause_type_map = {
539
            // Boolean operations
540
2.00k
            {"AND", InvertedIndexQueryType::BOOLEAN_QUERY},
541
2.00k
            {"OR", InvertedIndexQueryType::BOOLEAN_QUERY},
542
2.00k
            {"NOT", InvertedIndexQueryType::BOOLEAN_QUERY},
543
2.00k
            {"OCCUR_BOOLEAN", InvertedIndexQueryType::BOOLEAN_QUERY},
544
2.00k
            {"NESTED", InvertedIndexQueryType::BOOLEAN_QUERY},
545
546
            // Non-tokenized queries (exact matching, pattern matching)
547
2.00k
            {"TERM", InvertedIndexQueryType::EQUAL_QUERY},
548
2.00k
            {"PREFIX", InvertedIndexQueryType::MATCH_PHRASE_PREFIX_QUERY},
549
2.00k
            {"WILDCARD", InvertedIndexQueryType::WILDCARD_QUERY},
550
2.00k
            {"REGEXP", InvertedIndexQueryType::MATCH_REGEXP_QUERY},
551
2.00k
            {"RANGE", InvertedIndexQueryType::RANGE_QUERY},
552
2.00k
            {"LIST", InvertedIndexQueryType::LIST_QUERY},
553
554
            // Tokenized queries (full-text search, phrase search)
555
2.00k
            {"PHRASE", InvertedIndexQueryType::MATCH_PHRASE_QUERY},
556
2.00k
            {"MATCH", InvertedIndexQueryType::MATCH_ANY_QUERY},
557
2.00k
            {"ANY", InvertedIndexQueryType::MATCH_ANY_QUERY},
558
2.00k
            {"ALL", InvertedIndexQueryType::MATCH_ALL_QUERY},
559
560
            // Exact match without tokenization
561
2.00k
            {"EXACT", InvertedIndexQueryType::EQUAL_QUERY},
562
2.00k
    };
563
564
2.00k
    auto it = clause_type_map.find(clause_type);
565
2.00k
    if (it != clause_type_map.end()) {
566
1.98k
        return it->second;
567
1.98k
    }
568
569
    // Unknown clause type
570
2.00k
    LOG(WARNING) << "Unknown clause type '" << clause_type << "', defaulting to EQUAL_QUERY";
571
16
    return InvertedIndexQueryType::EQUAL_QUERY;
572
2.00k
}
573
574
// Map Thrift TSearchOccur to query_v2::Occur
575
702
static query_v2::Occur map_thrift_occur(TSearchOccur::type thrift_occur) {
576
702
    switch (thrift_occur) {
577
270
    case TSearchOccur::MUST:
578
270
        return query_v2::Occur::MUST;
579
391
    case TSearchOccur::SHOULD:
580
391
        return query_v2::Occur::SHOULD;
581
41
    case TSearchOccur::MUST_NOT:
582
41
        return query_v2::Occur::MUST_NOT;
583
0
    default:
584
0
        return query_v2::Occur::MUST;
585
702
    }
586
702
}
587
588
Status FunctionSearch::build_query_recursive(
589
        const TSearchClause& clause, const std::shared_ptr<IndexQueryContext>& context,
590
        FieldReaderResolver& resolver, inverted_index::query_v2::QueryPtr* out,
591
        std::string* binding_key, const std::string& default_operator, int32_t minimum_should_match,
592
2.50k
        uint32_t num_rows) const {
593
2.50k
    DCHECK(out != nullptr);
594
2.50k
    *out = nullptr;
595
2.52k
    if (binding_key) {
596
2.52k
        binding_key->clear();
597
2.52k
    }
598
599
2.50k
    const std::string& clause_type = clause.clause_type;
600
601
    // Handle MATCH_ALL_DOCS - matches all documents in the segment
602
2.50k
    if (clause_type == "MATCH_ALL_DOCS") {
603
34
        *out = std::make_shared<query_v2::AllQuery>();
604
34
        return Status::OK();
605
34
    }
606
607
    // Handle OCCUR_BOOLEAN - Lucene-style boolean query with MUST/SHOULD/MUST_NOT
608
2.46k
    if (clause_type == "OCCUR_BOOLEAN") {
609
346
        auto builder = segment_v2::inverted_index::query_v2::create_occur_boolean_query_builder();
610
611
        // Set minimum_should_match if specified
612
346
        if (clause.__isset.minimum_should_match) {
613
334
            builder->set_minimum_number_should_match(clause.minimum_should_match);
614
334
        }
615
616
348
        if (clause.__isset.children) {
617
704
            for (const auto& child_clause : clause.children) {
618
704
                query_v2::QueryPtr child_query;
619
704
                std::string child_binding_key;
620
704
                RETURN_IF_ERROR(build_query_recursive(child_clause, context, resolver, &child_query,
621
704
                                                      &child_binding_key, default_operator,
622
704
                                                      minimum_should_match, num_rows));
623
624
                // Determine occur type from child clause
625
703
                query_v2::Occur occur = query_v2::Occur::MUST; // default
626
703
                if (child_clause.__isset.occur) {
627
700
                    occur = map_thrift_occur(child_clause.occur);
628
700
                }
629
630
703
                builder->add(child_query, occur, std::move(child_binding_key));
631
703
            }
632
348
        }
633
634
345
        *out = builder->build();
635
345
        return Status::OK();
636
346
    }
637
638
2.12k
    if (clause_type == "NESTED") {
639
1
        return Status::InvalidArgument("NESTED clause must be evaluated at top level");
640
1
    }
641
642
    // Handle standard boolean operators (AND/OR/NOT)
643
2.11k
    if (clause_type == "AND" || clause_type == "OR" || clause_type == "NOT") {
644
346
        query_v2::OperatorType op = query_v2::OperatorType::OP_AND;
645
346
        if (clause_type == "OR") {
646
187
            op = query_v2::OperatorType::OP_OR;
647
187
        } else if (clause_type == "NOT") {
648
77
            op = query_v2::OperatorType::OP_NOT;
649
77
        }
650
651
346
        auto builder = create_operator_boolean_query_builder(op);
652
346
        if (clause.__isset.children) {
653
740
            for (const auto& child_clause : clause.children) {
654
740
                query_v2::QueryPtr child_query;
655
740
                std::string child_binding_key;
656
740
                RETURN_IF_ERROR(build_query_recursive(child_clause, context, resolver, &child_query,
657
740
                                                      &child_binding_key, default_operator,
658
740
                                                      minimum_should_match, num_rows));
659
                // Add all children including empty BitSetQuery
660
                // BooleanQuery will handle the logic:
661
                // - AND with empty bitmap → result is empty
662
                // - OR with empty bitmap → empty bitmap is ignored by OR logic
663
                // - NOT with empty bitmap → NOT(empty) = all rows (handled by BooleanQuery)
664
739
                builder->add(child_query, std::move(child_binding_key));
665
739
            }
666
346
        }
667
668
345
        *out = builder->build();
669
345
        return Status::OK();
670
346
    }
671
672
1.77k
    return build_leaf_query(clause, context, resolver, out, binding_key, default_operator,
673
1.77k
                            minimum_should_match, num_rows);
674
2.11k
}
675
676
Status FunctionSearch::build_leaf_query(const TSearchClause& clause,
677
                                        const std::shared_ptr<IndexQueryContext>& context,
678
                                        FieldReaderResolver& resolver,
679
                                        inverted_index::query_v2::QueryPtr* out,
680
                                        std::string* binding_key,
681
                                        const std::string& default_operator,
682
1.80k
                                        int32_t minimum_should_match, uint32_t num_rows) const {
683
1.80k
    DCHECK(out != nullptr);
684
1.80k
    *out = nullptr;
685
1.81k
    if (binding_key) {
686
1.81k
        binding_key->clear();
687
1.81k
    }
688
689
1.80k
    if (!clause.__isset.field_name || !clause.__isset.value) {
690
0
        return Status::InvalidArgument("search clause missing field_name or value");
691
0
    }
692
693
1.80k
    const std::string& field_name = clause.field_name;
694
1.80k
    const std::string& value = clause.value;
695
1.80k
    const std::string& clause_type = clause.clause_type;
696
697
1.80k
    auto query_type = clause_type_to_query_type(clause_type);
698
    // TERM, WILDCARD, PREFIX, and REGEXP in search DSL operate on individual index terms
699
    // (like Lucene TermQuery, WildcardQuery, PrefixQuery, RegexpQuery).
700
    // Override to MATCH_ANY_QUERY so select_best_reader() prefers the FULLTEXT reader
701
    // when multiple indexes exist on the same column (one tokenized, one untokenized).
702
    // Without this, these queries would select the untokenized index and try to match
703
    // patterns like "h*llo" against full strings ("hello world") instead of individual
704
    // tokens ("hello"), returning empty results.
705
    // EXACT must remain EQUAL_QUERY to prefer the untokenized STRING_TYPE reader.
706
    //
707
    // Safe for single-index columns: select_best_reader() has a single-reader fast path
708
    // that returns the only reader directly, bypassing the query_type preference logic.
709
1.80k
    if (clause_type == "TERM" || clause_type == "WILDCARD" || clause_type == "PREFIX" ||
710
1.80k
        clause_type == "REGEXP") {
711
1.46k
        query_type = InvertedIndexQueryType::MATCH_ANY_QUERY;
712
1.46k
    }
713
714
1.80k
    auto finish_leaf_query = [&](query_v2::QueryPtr query) -> Status {
715
1.76k
        *out = std::move(query);
716
1.76k
        return resolver.map_leaf_query(field_name, out);
717
1.76k
    };
718
719
1.80k
    FieldReaderBinding binding;
720
1.80k
    RETURN_IF_ERROR(resolver.resolve(field_name, query_type, &binding));
721
722
1.78k
    if (!binding.is_bound()) {
723
1
        LOG(INFO) << "search: No inverted index for field '" << field_name
724
1
                  << "' in this segment, clause_type='" << clause_type
725
1
                  << "', query_type=" << static_cast<int>(query_type)
726
1
                  << ", returning UNKNOWN bitmap";
727
1
        if (binding_key) {
728
1
            binding_key->clear();
729
1
        }
730
1
        return finish_leaf_query(make_unknown_query(num_rows));
731
1
    }
732
733
1.78k
    if (binding_key) {
734
1.77k
        *binding_key = binding.binding_key;
735
1.77k
    }
736
737
1.78k
    if (binding.use_direct_index_reader()) {
738
3
        auto direct_query_type = direct_index_query_type_for_clause(clause_type);
739
3
        if (direct_query_type == InvertedIndexQueryType::UNKNOWN_QUERY) {
740
1
            return finish_leaf_query(make_unknown_query(num_rows));
741
1
        }
742
743
2
        auto value_type = unwrap_direct_index_value_type(binding.column_type);
744
2
        Field param_value;
745
2
        auto parse_status = parse_scalar_search_value(value_type, value, &param_value);
746
2
        if (!parse_status.ok()) {
747
0
            LOG(INFO) << "search: scalar leaf value is unsupported, field=" << field_name
748
0
                      << ", value='" << value << "', reason=" << parse_status.to_string();
749
0
            return finish_leaf_query(make_unknown_query(num_rows));
750
0
        }
751
752
2
        auto* iterator = resolver.get_iterator(field_name);
753
2
        if (iterator == nullptr) {
754
0
            return finish_leaf_query(make_unknown_query(num_rows));
755
0
        }
756
757
2
        segment_v2::InvertedIndexParam param;
758
2
        param.column_name = binding.stored_field_name;
759
2
        param.column_type = value_type;
760
2
        param.query_value = param_value;
761
2
        param.query_type = direct_query_type;
762
2
        param.num_rows = num_rows;
763
2
        param.roaring = std::make_shared<roaring::Roaring>();
764
2
        RETURN_IF_ERROR(iterator->read_from_index(segment_v2::IndexParam {&param}));
765
766
2
        std::shared_ptr<roaring::Roaring> null_bitmap = std::make_shared<roaring::Roaring>();
767
2
        auto has_null = iterator->has_null();
768
2
        if (has_null.has_value() && has_null.value()) {
769
0
            segment_v2::InvertedIndexQueryCacheHandle null_bitmap_cache_handle;
770
0
            RETURN_IF_ERROR(iterator->read_null_bitmap(&null_bitmap_cache_handle));
771
0
            if (auto bitmap = null_bitmap_cache_handle.get_bitmap(); bitmap != nullptr) {
772
0
                null_bitmap = bitmap;
773
0
            }
774
0
        }
775
2
        return finish_leaf_query(std::make_shared<query_v2::BitSetQuery>(std::move(param.roaring),
776
2
                                                                         std::move(null_bitmap)));
777
2
    }
778
779
1.77k
    if (binding.lucene_reader == nullptr) {
780
0
        return finish_leaf_query(make_unknown_query(num_rows));
781
0
    }
782
783
1.77k
    FunctionSearch::ClauseTypeCategory category = get_clause_type_category(clause_type);
784
1.77k
    std::wstring field_wstr = binding.stored_field_wstr;
785
1.77k
    std::wstring value_wstr = StringHelper::to_wstring(value);
786
787
1.77k
    auto make_term_query = [&](const std::wstring& term) -> query_v2::QueryPtr {
788
1.73k
        return std::make_shared<query_v2::TermQuery>(context, field_wstr, term);
789
1.73k
    };
790
791
1.77k
    if (clause_type == "TERM") {
792
1.35k
        bool should_analyze =
793
1.35k
                inverted_index::InvertedIndexAnalyzer::should_analyzer(binding.index_properties);
794
1.35k
        if (should_analyze) {
795
1.10k
            if (binding.index_properties.empty()) {
796
0
                LOG(WARNING) << "search: analyzer required but index properties empty for field '"
797
0
                             << field_name << "'";
798
0
                return finish_leaf_query(make_term_query(value_wstr));
799
0
            }
800
801
1.10k
            std::vector<TermInfo> term_infos =
802
1.10k
                    inverted_index::InvertedIndexAnalyzer::get_analyse_result(
803
1.10k
                            value, binding.index_properties);
804
1.10k
            if (term_infos.empty()) {
805
0
                LOG(WARNING) << "search: No terms found after tokenization for TERM query, field="
806
0
                             << field_name << ", value='" << value
807
0
                             << "', returning empty BitSetQuery";
808
0
                return finish_leaf_query(
809
0
                        std::make_shared<query_v2::BitSetQuery>(roaring::Roaring()));
810
0
            }
811
812
1.10k
            if (term_infos.size() == 1) {
813
1.08k
                std::wstring term_wstr = StringHelper::to_wstring(term_infos[0].get_single_term());
814
1.08k
                return finish_leaf_query(make_term_query(term_wstr));
815
1.08k
            }
816
817
            // When minimum_should_match is specified, use OccurBooleanQuery
818
            // ES behavior: msm only applies to SHOULD clauses
819
15
            if (minimum_should_match > 0) {
820
0
                auto builder =
821
0
                        segment_v2::inverted_index::query_v2::create_occur_boolean_query_builder();
822
0
                builder->set_minimum_number_should_match(minimum_should_match);
823
0
                query_v2::Occur occur = (default_operator == "and") ? query_v2::Occur::MUST
824
0
                                                                    : query_v2::Occur::SHOULD;
825
0
                for (const auto& term_info : term_infos) {
826
0
                    std::wstring term_wstr = StringHelper::to_wstring(term_info.get_single_term());
827
0
                    builder->add(make_term_query(term_wstr), occur);
828
0
                }
829
0
                return finish_leaf_query(builder->build());
830
0
            }
831
832
            // Use default_operator to determine how to combine tokenized terms
833
15
            query_v2::OperatorType op_type = (default_operator == "and")
834
15
                                                     ? query_v2::OperatorType::OP_AND
835
15
                                                     : query_v2::OperatorType::OP_OR;
836
15
            auto builder = create_operator_boolean_query_builder(op_type);
837
20
            for (const auto& term_info : term_infos) {
838
20
                std::wstring term_wstr = StringHelper::to_wstring(term_info.get_single_term());
839
20
                builder->add(make_term_query(term_wstr), binding.binding_key);
840
20
            }
841
842
15
            return finish_leaf_query(builder->build());
843
15
        }
844
845
248
        return finish_leaf_query(make_term_query(value_wstr));
846
1.35k
    }
847
848
428
    if (category == FunctionSearch::ClauseTypeCategory::TOKENIZED) {
849
247
        if (clause_type == "PHRASE") {
850
104
            bool should_analyze = inverted_index::InvertedIndexAnalyzer::should_analyzer(
851
104
                    binding.index_properties);
852
104
            if (!should_analyze) {
853
10
                VLOG_DEBUG << "search: PHRASE on non-tokenized field '" << field_name
854
0
                           << "', falling back to TERM";
855
10
                return finish_leaf_query(make_term_query(value_wstr));
856
10
            }
857
858
94
            if (binding.index_properties.empty()) {
859
0
                LOG(WARNING) << "search: analyzer required but index properties empty for PHRASE "
860
0
                                "query on field '"
861
0
                             << field_name << "'";
862
0
                return finish_leaf_query(make_term_query(value_wstr));
863
0
            }
864
865
94
            std::vector<TermInfo> term_infos =
866
94
                    inverted_index::InvertedIndexAnalyzer::get_analyse_result(
867
94
                            value, binding.index_properties);
868
94
            if (term_infos.empty()) {
869
8
                LOG(WARNING) << "search: No terms found after tokenization for PHRASE query, field="
870
8
                             << field_name << ", value='" << value
871
8
                             << "', returning empty BitSetQuery";
872
8
                return finish_leaf_query(
873
8
                        std::make_shared<query_v2::BitSetQuery>(roaring::Roaring()));
874
8
            }
875
876
86
            std::vector<TermInfo> phrase_term_infos =
877
86
                    QueryHelper::build_phrase_term_infos(term_infos);
878
86
            if (phrase_term_infos.size() == 1) {
879
63
                const auto& term_info = phrase_term_infos[0];
880
63
                if (term_info.is_single_term()) {
881
62
                    std::wstring term_wstr = StringHelper::to_wstring(term_info.get_single_term());
882
62
                    return finish_leaf_query(
883
62
                            std::make_shared<query_v2::TermQuery>(context, field_wstr, term_wstr));
884
62
                } else {
885
1
                    auto builder =
886
1
                            create_operator_boolean_query_builder(query_v2::OperatorType::OP_OR);
887
1
                    for (const auto& term : term_info.get_multi_terms()) {
888
0
                        std::wstring term_wstr = StringHelper::to_wstring(term);
889
0
                        builder->add(make_term_query(term_wstr), binding.binding_key);
890
0
                    }
891
1
                    return finish_leaf_query(builder->build());
892
1
                }
893
63
            } else {
894
23
                if (QueryHelper::is_simple_phrase(phrase_term_infos)) {
895
21
                    return finish_leaf_query(std::make_shared<query_v2::PhraseQuery>(
896
21
                            context, field_wstr, phrase_term_infos));
897
21
                } else {
898
2
                    return finish_leaf_query(std::make_shared<query_v2::MultiPhraseQuery>(
899
2
                            context, field_wstr, phrase_term_infos));
900
2
                }
901
23
            }
902
903
0
            return Status::OK();
904
86
        }
905
143
        if (clause_type == "MATCH") {
906
0
            VLOG_DEBUG << "search: MATCH clause not implemented, fallback to TERM";
907
0
            return finish_leaf_query(make_term_query(value_wstr));
908
0
        }
909
910
144
        if (clause_type == "ANY" || clause_type == "ALL") {
911
144
            bool should_analyze = inverted_index::InvertedIndexAnalyzer::should_analyzer(
912
144
                    binding.index_properties);
913
144
            if (!should_analyze) {
914
1
                return finish_leaf_query(make_term_query(value_wstr));
915
1
            }
916
917
143
            if (binding.index_properties.empty()) {
918
0
                LOG(WARNING) << "search: index properties empty for tokenized clause '"
919
0
                             << clause_type << "' field=" << field_name;
920
0
                return finish_leaf_query(make_term_query(value_wstr));
921
0
            }
922
923
143
            std::vector<TermInfo> term_infos =
924
143
                    inverted_index::InvertedIndexAnalyzer::get_analyse_result(
925
143
                            value, binding.index_properties);
926
143
            if (term_infos.empty()) {
927
0
                LOG(WARNING) << "search: tokenization yielded no terms for clause '" << clause_type
928
0
                             << "', field=" << field_name << ", returning empty BitSetQuery";
929
0
                return finish_leaf_query(
930
0
                        std::make_shared<query_v2::BitSetQuery>(roaring::Roaring()));
931
0
            }
932
933
143
            query_v2::OperatorType bool_type = query_v2::OperatorType::OP_OR;
934
143
            if (clause_type == "ALL") {
935
70
                bool_type = query_v2::OperatorType::OP_AND;
936
70
            }
937
938
143
            if (term_infos.size() == 1) {
939
10
                std::wstring term_wstr = StringHelper::to_wstring(term_infos[0].get_single_term());
940
10
                return finish_leaf_query(make_term_query(term_wstr));
941
10
            }
942
943
133
            auto builder = create_operator_boolean_query_builder(bool_type);
944
280
            for (const auto& term_info : term_infos) {
945
280
                std::wstring term_wstr = StringHelper::to_wstring(term_info.get_single_term());
946
280
                builder->add(make_term_query(term_wstr), binding.binding_key);
947
280
            }
948
133
            return finish_leaf_query(builder->build());
949
143
        }
950
951
        // Default tokenized clause fallback
952
18.4E
        return finish_leaf_query(make_term_query(value_wstr));
953
143
    }
954
955
181
    if (category == FunctionSearch::ClauseTypeCategory::NON_TOKENIZED) {
956
171
        if (clause_type == "EXACT") {
957
            // EXACT match: exact string matching without tokenization
958
            // Note: EXACT prefers untokenized index (STRING_TYPE) which doesn't support lowercase
959
            // If only tokenized index exists, EXACT may return empty results because
960
            // tokenized indexes store individual tokens, not complete strings
961
82
            VLOG_DEBUG << "search: EXACT clause processed, field=" << field_name << ", value='"
962
0
                       << value << "'";
963
82
            return finish_leaf_query(make_term_query(value_wstr));
964
82
        }
965
89
        if (clause_type == "PREFIX") {
966
            // Apply lowercase only if:
967
            // 1. There's a parser/analyzer (otherwise lower_case has no effect on indexing)
968
            // 2. lower_case is explicitly set to "true"
969
36
            bool has_parser = inverted_index::InvertedIndexAnalyzer::should_analyzer(
970
36
                    binding.index_properties);
971
36
            std::string lowercase_setting =
972
36
                    get_parser_lowercase_from_properties(binding.index_properties);
973
36
            bool should_lowercase = has_parser && (lowercase_setting == INVERTED_INDEX_PARSER_TRUE);
974
36
            std::string pattern = should_lowercase ? to_lower(value) : value;
975
36
            VLOG_DEBUG << "search: PREFIX clause processed, field=" << field_name << ", pattern='"
976
0
                       << pattern << "' (original='" << value << "', has_parser=" << has_parser
977
0
                       << ", lower_case=" << lowercase_setting << ")";
978
36
            return finish_leaf_query(
979
36
                    std::make_shared<query_v2::WildcardQuery>(context, field_wstr, pattern));
980
36
        }
981
982
53
        if (clause_type == "WILDCARD") {
983
            // Standalone wildcard "*" matches all non-null values for this field
984
            // Consistent with ES query_string behavior where field:* becomes FieldExistsQuery
985
22
            if (value == "*") {
986
0
                VLOG_DEBUG << "search: WILDCARD '*' converted to AllQuery(nullable=true), field="
987
0
                           << field_name;
988
0
                return finish_leaf_query(std::make_shared<query_v2::AllQuery>(field_wstr, true));
989
0
            }
990
            // Apply lowercase only if:
991
            // 1. There's a parser/analyzer (otherwise lower_case has no effect on indexing)
992
            // 2. lower_case is explicitly set to "true"
993
22
            bool has_parser = inverted_index::InvertedIndexAnalyzer::should_analyzer(
994
22
                    binding.index_properties);
995
22
            std::string lowercase_setting =
996
22
                    get_parser_lowercase_from_properties(binding.index_properties);
997
22
            bool should_lowercase = has_parser && (lowercase_setting == INVERTED_INDEX_PARSER_TRUE);
998
22
            std::string pattern = should_lowercase ? to_lower(value) : value;
999
22
            VLOG_DEBUG << "search: WILDCARD clause processed, field=" << field_name << ", pattern='"
1000
0
                       << pattern << "' (original='" << value << "', has_parser=" << has_parser
1001
0
                       << ", lower_case=" << lowercase_setting << ")";
1002
22
            return finish_leaf_query(
1003
22
                    std::make_shared<query_v2::WildcardQuery>(context, field_wstr, pattern));
1004
22
        }
1005
1006
31
        if (clause_type == "REGEXP") {
1007
            // ES-compatible: regex patterns are NOT lowercased (case-sensitive matching)
1008
            // This matches ES query_string behavior where regex patterns bypass analysis
1009
29
            VLOG_DEBUG << "search: REGEXP clause processed, field=" << field_name << ", pattern='"
1010
0
                       << value << "'";
1011
29
            return finish_leaf_query(
1012
29
                    std::make_shared<query_v2::RegexpQuery>(context, field_wstr, value));
1013
29
        }
1014
1015
3
        if (clause_type == "RANGE" || clause_type == "LIST") {
1016
3
            VLOG_DEBUG << "search: clause type '" << clause_type
1017
0
                       << "' not implemented, fallback to TERM";
1018
3
        }
1019
2
        return finish_leaf_query(make_term_query(value_wstr));
1020
31
    }
1021
1022
181
    LOG(WARNING) << "search: Unexpected clause type '" << clause_type << "', using TERM fallback";
1023
10
    return finish_leaf_query(make_term_query(value_wstr));
1024
181
}
1025
1026
7
void register_function_search(SimpleFunctionFactory& factory) {
1027
7
    factory.register_function<FunctionSearch>();
1028
7
}
1029
1030
} // namespace doris