This is an automated email from the ASF dual-hosted git repository.
airborne pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 729b9f85b6a [fix](search) Add searcher cache reuse and DSL result
cache for search() function (#60790)
729b9f85b6a is described below
commit 729b9f85b6ad3e8b58ea4c3e749020ad4fb66436
Author: Jack <[email protected]>
AuthorDate: Tue Feb 24 21:51:18 2026 +0800
[fix](search) Add searcher cache reuse and DSL result cache for search()
function (#60790)
### What problem does this PR solve?
Problem Summary:
This PR adds searcher cache reuse and a DSL result cache for the
`search()` function to improve query performance on repeated search
queries against the same segments.
**Key changes:**
1. **DSL result cache**: Caches the final roaring bitmap per (segment,
DSL) pair so repeated identical `search()` queries skip Lucene execution
entirely. Uses length-prefix key encoding to avoid hash collisions.
2. **Deep-copy bitmap semantics**: Bitmaps are deep-copied on both cache
read and write to prevent `mask_out_null()` from polluting cached
entries.
3. **Type-safe cache accessor**: Replaces raw `void*` return with a
template `get_value<T>()` that uses `static_assert` to ensure T derives
from `LRUCacheValueBase`.
4. **Session-level cache toggle**: Adds
`enable_search_function_query_cache` session variable (default: true) to
allow disabling the cache per query via `SET_VAR`.
5. **Const-correctness fix**: Removes unsafe `const_cast` in
`build_dsl_signature` by copying TSearchParam before Thrift
serialization.
6. **Defensive improvements**: Adds null check for `result_bitmap` on
cache hit, logging for serialization fallback and cache bypass paths.
### Release note
Add DSL result cache for search() function to skip repeated Lucene
execution on identical queries.
---
be/src/olap/rowset/segment_v2/index_file_reader.h | 1 +
.../rowset/segment_v2/inverted_index_query_type.h | 4 +
.../olap/rowset/segment_v2/inverted_index_reader.h | 62 ++++--
be/src/runtime/exec_env_init.cpp | 2 +-
be/src/vec/exprs/vsearch.cpp | 14 +-
be/src/vec/exprs/vsearch.h | 5 +
be/src/vec/functions/function_search.cpp | 202 ++++++++++++++------
be/src/vec/functions/function_search.h | 10 +-
.../search_function_query_cache_test.cpp | 207 +++++++++++++++++++++
be/test/vec/function/function_search_test.cpp | 115 ++----------
.../search/test_search_boundary_cases.groovy | 2 +-
.../suites/search/test_search_cache.groovy | 138 ++++++++++++++
.../test_search_default_field_operator.groovy | 2 +-
.../suites/search/test_search_dsl_operators.groovy | 2 +-
.../suites/search/test_search_dsl_syntax.groovy | 2 +-
.../suites/search/test_search_escape.groovy | 2 +-
.../suites/search/test_search_exact_basic.groovy | 2 +-
.../search/test_search_exact_lowercase.groovy | 2 +-
.../suites/search/test_search_exact_match.groovy | 2 +-
.../search/test_search_exact_multi_index.groovy | 2 +-
.../suites/search/test_search_function.groovy | 2 +-
.../search/test_search_inverted_index.groovy | 2 +-
.../suites/search/test_search_lucene_mode.groovy | 2 +-
.../suites/search/test_search_mow_support.groovy | 2 +-
.../suites/search/test_search_multi_field.groovy | 2 +-
.../search/test_search_null_regression.groovy | 2 +-
.../search/test_search_null_semantics.groovy | 2 +-
.../search/test_search_regexp_lowercase.groovy | 2 +-
.../search/test_search_usage_restrictions.groovy | 2 +-
.../test_search_variant_dual_index_reader.groovy | 2 +-
.../test_search_variant_subcolumn_analyzer.groovy | 2 +-
.../search/test_search_vs_match_consistency.groovy | 2 +-
32 files changed, 603 insertions(+), 199 deletions(-)
diff --git a/be/src/olap/rowset/segment_v2/index_file_reader.h
b/be/src/olap/rowset/segment_v2/index_file_reader.h
index 42022224485..2b5f2e183a1 100644
--- a/be/src/olap/rowset/segment_v2/index_file_reader.h
+++ b/be/src/olap/rowset/segment_v2/index_file_reader.h
@@ -71,6 +71,7 @@ public:
Result<InvertedIndexDirectoryMap> get_all_directories();
// open file v2, init _stream
int64_t get_inverted_file_size() const { return _stream == nullptr ? 0 :
_stream->length(); }
+ const std::string& get_index_path_prefix() const { return
_index_path_prefix; }
friend IndexFileWriter;
protected:
diff --git a/be/src/olap/rowset/segment_v2/inverted_index_query_type.h
b/be/src/olap/rowset/segment_v2/inverted_index_query_type.h
index c6b250bb5c2..8fa1a0f9059 100644
--- a/be/src/olap/rowset/segment_v2/inverted_index_query_type.h
+++ b/be/src/olap/rowset/segment_v2/inverted_index_query_type.h
@@ -82,6 +82,7 @@ enum class InvertedIndexQueryType {
WILDCARD_QUERY = 12,
RANGE_QUERY = 13,
LIST_QUERY = 14,
+ SEARCH_DSL_QUERY = 15,
};
inline bool is_equal_query(InvertedIndexQueryType query_type) {
@@ -154,6 +155,9 @@ inline std::string
query_type_to_string(InvertedIndexQueryType query_type) {
case InvertedIndexQueryType::LIST_QUERY: {
return "LIST";
}
+ case InvertedIndexQueryType::SEARCH_DSL_QUERY: {
+ return "SEARCH_DSL";
+ }
default:
return "";
}
diff --git a/be/src/olap/rowset/segment_v2/inverted_index_reader.h
b/be/src/olap/rowset/segment_v2/inverted_index_reader.h
index 8e93ed87f21..6810d6082e0 100644
--- a/be/src/olap/rowset/segment_v2/inverted_index_reader.h
+++ b/be/src/olap/rowset/segment_v2/inverted_index_reader.h
@@ -94,8 +94,12 @@ public:
// Copy constructor
InvertedIndexResultBitmap(const InvertedIndexResultBitmap& other)
- :
_data_bitmap(std::make_shared<roaring::Roaring>(*other._data_bitmap)),
-
_null_bitmap(std::make_shared<roaring::Roaring>(*other._null_bitmap)) {}
+ : _data_bitmap(other._data_bitmap
+ ?
std::make_shared<roaring::Roaring>(*other._data_bitmap)
+ : nullptr),
+ _null_bitmap(other._null_bitmap
+ ?
std::make_shared<roaring::Roaring>(*other._null_bitmap)
+ : nullptr) {}
// Move constructor
InvertedIndexResultBitmap(InvertedIndexResultBitmap&& other) noexcept
@@ -105,8 +109,12 @@ public:
// Copy assignment operator
InvertedIndexResultBitmap& operator=(const InvertedIndexResultBitmap&
other) {
if (this != &other) { // Prevent self-assignment
- _data_bitmap =
std::make_shared<roaring::Roaring>(*other._data_bitmap);
- _null_bitmap =
std::make_shared<roaring::Roaring>(*other._null_bitmap);
+ _data_bitmap = other._data_bitmap
+ ?
std::make_shared<roaring::Roaring>(*other._data_bitmap)
+ : nullptr;
+ _null_bitmap = other._null_bitmap
+ ?
std::make_shared<roaring::Roaring>(*other._null_bitmap)
+ : nullptr;
}
return *this;
}
@@ -122,11 +130,15 @@ public:
// Operator &=
InvertedIndexResultBitmap& operator&=(const InvertedIndexResultBitmap&
other) {
- if (_data_bitmap && _null_bitmap && other._data_bitmap &&
other._null_bitmap) {
- auto new_null_bitmap = (*_data_bitmap & *other._null_bitmap) |
- (*_null_bitmap & *other._data_bitmap) |
- (*_null_bitmap & *other._null_bitmap);
+ if (_data_bitmap && other._data_bitmap) {
+ const auto& my_null = _null_bitmap ? *_null_bitmap :
_empty_bitmap();
+ const auto& ot_null = other._null_bitmap ? *other._null_bitmap :
_empty_bitmap();
+ auto new_null_bitmap = (*_data_bitmap & ot_null) | (my_null &
*other._data_bitmap) |
+ (my_null & ot_null);
*_data_bitmap &= *other._data_bitmap;
+ if (!_null_bitmap) {
+ _null_bitmap = std::make_shared<roaring::Roaring>();
+ }
*_null_bitmap = std::move(new_null_bitmap);
}
return *this;
@@ -134,7 +146,9 @@ public:
// Operator |=
InvertedIndexResultBitmap& operator|=(const InvertedIndexResultBitmap&
other) {
- if (_data_bitmap && _null_bitmap && other._data_bitmap &&
other._null_bitmap) {
+ if (_data_bitmap && other._data_bitmap) {
+ const auto& my_null = _null_bitmap ? *_null_bitmap :
_empty_bitmap();
+ const auto& ot_null = other._null_bitmap ? *other._null_bitmap :
_empty_bitmap();
// SQL three-valued logic for OR:
// - TRUE OR anything = TRUE (not NULL)
// - FALSE OR NULL = NULL
@@ -142,9 +156,11 @@ public:
// Result is NULL when the row is NULL on either side while the
other side
// is not TRUE. Rows that become TRUE must be removed from the
NULL bitmap.
*_data_bitmap |= *other._data_bitmap;
- auto new_null_bitmap =
- (*_null_bitmap - *other._data_bitmap) |
(*other._null_bitmap - *_data_bitmap);
+ auto new_null_bitmap = (my_null - *other._data_bitmap) | (ot_null
- *_data_bitmap);
new_null_bitmap -= *_data_bitmap;
+ if (!_null_bitmap) {
+ _null_bitmap = std::make_shared<roaring::Roaring>();
+ }
*_null_bitmap = std::move(new_null_bitmap);
}
return *this;
@@ -152,8 +168,12 @@ public:
// NOT operation
const InvertedIndexResultBitmap& op_not(const roaring::Roaring* universe)
const {
- if (_data_bitmap && _null_bitmap) {
- *_data_bitmap = *universe - *_data_bitmap - *_null_bitmap;
+ if (_data_bitmap) {
+ if (_null_bitmap) {
+ *_data_bitmap = *universe - *_data_bitmap - *_null_bitmap;
+ } else {
+ *_data_bitmap = *universe - *_data_bitmap;
+ }
// The _null_bitmap remains unchanged.
}
return *this;
@@ -161,10 +181,14 @@ public:
// Operator -=
InvertedIndexResultBitmap& operator-=(const InvertedIndexResultBitmap&
other) {
- if (_data_bitmap && _null_bitmap && other._data_bitmap &&
other._null_bitmap) {
+ if (_data_bitmap && other._data_bitmap) {
*_data_bitmap -= *other._data_bitmap;
- *_data_bitmap -= *other._null_bitmap;
- *_null_bitmap -= *other._null_bitmap;
+ if (other._null_bitmap) {
+ *_data_bitmap -= *other._null_bitmap;
+ }
+ if (_null_bitmap && other._null_bitmap) {
+ *_null_bitmap -= *other._null_bitmap;
+ }
}
return *this;
}
@@ -181,6 +205,12 @@ public:
// Check if both bitmaps are empty
bool is_empty() const { return (_data_bitmap == nullptr && _null_bitmap ==
nullptr); }
+
+private:
+ static const roaring::Roaring& _empty_bitmap() {
+ static const roaring::Roaring empty;
+ return empty;
+ }
};
class InvertedIndexReader : public IndexReader {
diff --git a/be/src/runtime/exec_env_init.cpp b/be/src/runtime/exec_env_init.cpp
index b7a049b587b..4857a9acf43 100644
--- a/be/src/runtime/exec_env_init.cpp
+++ b/be/src/runtime/exec_env_init.cpp
@@ -640,7 +640,7 @@ Status ExecEnv::init_mem_env() {
_inverted_index_query_cache = InvertedIndexQueryCache::create_global_cache(
inverted_index_query_cache_limit,
config::inverted_index_query_cache_shards);
LOG(INFO) << "Inverted index query match cache memory limit: "
- << PrettyPrinter::print(inverted_index_cache_limit, TUnit::BYTES)
+ << PrettyPrinter::print(inverted_index_query_cache_limit,
TUnit::BYTES)
<< ", origin config value: " <<
config::inverted_index_query_cache_limit;
// use memory limit
diff --git a/be/src/vec/exprs/vsearch.cpp b/be/src/vec/exprs/vsearch.cpp
index f2d9894f2a5..5043990172e 100644
--- a/be/src/vec/exprs/vsearch.cpp
+++ b/be/src/vec/exprs/vsearch.cpp
@@ -24,6 +24,7 @@
#include "common/status.h"
#include "glog/logging.h"
#include "olap/rowset/segment_v2/inverted_index_reader.h"
+#include "runtime/runtime_state.h"
#include "vec/columns/column_const.h"
#include "vec/exprs/vexpr_context.h"
#include "vec/exprs/vliteral.h"
@@ -120,6 +121,16 @@ VSearchExpr::VSearchExpr(const TExprNode& node) :
VExpr(node) {
}
}
+Status VSearchExpr::prepare(RuntimeState* state, const RowDescriptor& row_desc,
+ VExprContext* context) {
+ RETURN_IF_ERROR(VExpr::prepare(state, row_desc, context));
+ const auto& query_options = state->query_options();
+ if (query_options.__isset.enable_inverted_index_query_cache) {
+ _enable_cache = query_options.enable_inverted_index_query_cache;
+ }
+ return Status::OK();
+}
+
const std::string& VSearchExpr::expr_name() const {
static const std::string name = "VSearchExpr";
return name;
@@ -164,7 +175,8 @@ Status VSearchExpr::evaluate_inverted_index(VExprContext*
context, uint32_t segm
auto function = std::make_shared<FunctionSearch>();
auto result_bitmap = InvertedIndexResultBitmap();
auto status = function->evaluate_inverted_index_with_search_param(
- _search_param, bundle.field_types, bundle.iterators,
segment_num_rows, result_bitmap);
+ _search_param, bundle.field_types, bundle.iterators,
segment_num_rows, result_bitmap,
+ _enable_cache);
if (!status.ok()) {
LOG(WARNING) << "VSearchExpr: Function evaluation failed: " <<
status.to_string();
diff --git a/be/src/vec/exprs/vsearch.h b/be/src/vec/exprs/vsearch.h
index 9f818e50ee4..a7cfaa84ef0 100644
--- a/be/src/vec/exprs/vsearch.h
+++ b/be/src/vec/exprs/vsearch.h
@@ -41,10 +41,15 @@ public:
bool can_push_down_to_index() const override { return true; }
const TSearchParam& get_search_param() const { return _search_param; }
+ bool enable_cache() const { return _enable_cache; }
+
+ Status prepare(RuntimeState* state, const RowDescriptor& row_desc,
+ VExprContext* context) override;
private:
TSearchParam _search_param;
std::string _original_dsl;
+ bool _enable_cache = true;
};
} // namespace doris::vectorized
diff --git a/be/src/vec/functions/function_search.cpp
b/be/src/vec/functions/function_search.cpp
index 2b5e8d9d305..1aa12b658f3 100644
--- a/be/src/vec/functions/function_search.cpp
+++ b/be/src/vec/functions/function_search.cpp
@@ -49,7 +49,9 @@
#include "olap/rowset/segment_v2/inverted_index/util/string_helper.h"
#include "olap/rowset/segment_v2/inverted_index_iterator.h"
#include "olap/rowset/segment_v2/inverted_index_reader.h"
+#include "olap/rowset/segment_v2/inverted_index_searcher.h"
#include "util/string_util.h"
+#include "util/thrift_util.h"
#include "vec/columns/column_const.h"
#include "vec/core/columns_with_type_and_name.h"
#include "vec/data_types/data_type_string.h"
@@ -57,6 +59,48 @@
namespace doris::vectorized {
+// Build canonical DSL signature for cache key.
+// Serializes the entire TSearchParam via Thrift binary protocol so that
+// every field (DSL, AST root, field bindings, default_operator,
+// minimum_should_match, etc.) is included automatically.
+static std::string build_dsl_signature(const TSearchParam& param) {
+ ThriftSerializer ser(false, 1024);
+ TSearchParam copy = param;
+ std::string sig;
+ auto st = ser.serialize(©, &sig);
+ if (UNLIKELY(!st.ok())) {
+ LOG(WARNING) << "build_dsl_signature: Thrift serialization failed: "
<< st.to_string()
+ << ", caching disabled for this query";
+ return "";
+ }
+ return sig;
+}
+
+// Extract segment path prefix from the first available inverted index
iterator.
+// All fields in the same segment share the same path prefix.
+static std::string extract_segment_prefix(
+ const std::unordered_map<std::string, IndexIterator*>& iterators) {
+ for (const auto& [field_name, iter] : iterators) {
+ auto* inv_iter = dynamic_cast<InvertedIndexIterator*>(iter);
+ if (!inv_iter) continue;
+ // Try fulltext reader first, then string type
+ for (auto type :
+ {InvertedIndexReaderType::FULLTEXT,
InvertedIndexReaderType::STRING_TYPE}) {
+ IndexReaderType reader_type = type;
+ auto reader = inv_iter->get_reader(reader_type);
+ if (!reader) continue;
+ auto inv_reader =
std::dynamic_pointer_cast<InvertedIndexReader>(reader);
+ if (!inv_reader) continue;
+ auto file_reader = inv_reader->get_index_file_reader();
+ if (!file_reader) continue;
+ return file_reader->get_index_path_prefix();
+ }
+ }
+ VLOG_DEBUG << "extract_segment_prefix: no suitable inverted index reader
found across "
+ << iterators.size() << " iterators, caching disabled for this
query";
+ return "";
+}
+
Status FieldReaderResolver::resolve(const std::string& field_name,
InvertedIndexQueryType query_type,
FieldReaderBinding* binding) {
@@ -149,33 +193,58 @@ Status FieldReaderResolver::resolve(const std::string&
field_name,
"index file reader is null for field '{}'", field_name);
}
- RETURN_IF_ERROR(
- index_file_reader->init(config::inverted_index_read_buffer_size,
_context->io_ctx));
-
- auto directory = DORIS_TRY(
- index_file_reader->open(&inverted_reader->get_index_meta(),
_context->io_ctx));
-
- lucene::index::IndexReader* raw_reader = nullptr;
- try {
- raw_reader = lucene::index::IndexReader::open(
- directory.get(), config::inverted_index_read_buffer_size,
false);
- } catch (const CLuceneError& e) {
- return Status::Error<ErrorCode::INVERTED_INDEX_CLUCENE_ERROR>(
- "failed to open IndexReader for field '{}': {}", field_name,
e.what());
+ // Use InvertedIndexSearcherCache to avoid re-opening index files
repeatedly
+ auto index_file_key =
+
index_file_reader->get_index_file_cache_key(&inverted_reader->get_index_meta());
+ InvertedIndexSearcherCache::CacheKey searcher_cache_key(index_file_key);
+ InvertedIndexCacheHandle searcher_cache_handle;
+ bool cache_hit =
InvertedIndexSearcherCache::instance()->lookup(searcher_cache_key,
+
&searcher_cache_handle);
+
+ std::shared_ptr<lucene::index::IndexReader> reader_holder;
+ if (cache_hit) {
+ auto searcher_variant = searcher_cache_handle.get_index_searcher();
+ auto* searcher_ptr =
std::get_if<FulltextIndexSearcherPtr>(&searcher_variant);
+ if (searcher_ptr != nullptr && *searcher_ptr != nullptr) {
+ reader_holder = std::shared_ptr<lucene::index::IndexReader>(
+ (*searcher_ptr)->getReader(),
+ [](lucene::index::IndexReader*) { /* lifetime managed by
searcher cache */ });
+ }
}
- if (raw_reader == nullptr) {
- return Status::Error<ErrorCode::INVERTED_INDEX_CLUCENE_ERROR>(
- "IndexReader is null for field '{}'", field_name);
+ if (!reader_holder) {
+ // Cache miss: open directory, build IndexSearcher, insert into cache
+ RETURN_IF_ERROR(
+
index_file_reader->init(config::inverted_index_read_buffer_size,
_context->io_ctx));
+ auto directory = DORIS_TRY(
+ index_file_reader->open(&inverted_reader->get_index_meta(),
_context->io_ctx));
+
+ auto index_searcher_builder = DORIS_TRY(
+
IndexSearcherBuilder::create_index_searcher_builder(inverted_reader->type()));
+ auto searcher_result =
+
DORIS_TRY(index_searcher_builder->get_index_searcher(directory.get()));
+ auto reader_size = index_searcher_builder->get_reader_size();
+
+ auto* cache_value = new
InvertedIndexSearcherCache::CacheValue(std::move(searcher_result),
+
reader_size, UnixMillis());
+ InvertedIndexSearcherCache::instance()->insert(searcher_cache_key,
cache_value,
+ &searcher_cache_handle);
+
+ auto new_variant = searcher_cache_handle.get_index_searcher();
+ auto* new_ptr = std::get_if<FulltextIndexSearcherPtr>(&new_variant);
+ if (new_ptr != nullptr && *new_ptr != nullptr) {
+ reader_holder = std::shared_ptr<lucene::index::IndexReader>(
+ (*new_ptr)->getReader(),
+ [](lucene::index::IndexReader*) { /* lifetime managed by
searcher cache */ });
+ }
+
+ if (!reader_holder) {
+ return Status::Error<ErrorCode::INVERTED_INDEX_CLUCENE_ERROR>(
+ "failed to build IndexSearcher for field '{}'",
field_name);
+ }
}
- auto reader_holder = std::shared_ptr<lucene::index::IndexReader>(
- raw_reader, [](lucene::index::IndexReader* reader) {
- if (reader != nullptr) {
- reader->close();
- _CLDELETE(reader);
- }
- });
+ _searcher_cache_handles.push_back(std::move(searcher_cache_handle));
FieldReaderBinding resolved;
resolved.logical_field_name = field_name;
@@ -226,7 +295,7 @@ Status
FunctionSearch::evaluate_inverted_index_with_search_param(
const std::unordered_map<std::string,
vectorized::IndexFieldNameAndTypePair>&
data_type_with_names,
std::unordered_map<std::string, IndexIterator*> iterators, uint32_t
num_rows,
- InvertedIndexResultBitmap& bitmap_result) const {
+ InvertedIndexResultBitmap& bitmap_result, bool enable_cache) const {
if (iterators.empty() || data_type_with_names.empty()) {
LOG(INFO) << "No indexed columns or iterators available, returning
empty result, dsl:"
<< search_param.original_dsl;
@@ -235,6 +304,45 @@ Status
FunctionSearch::evaluate_inverted_index_with_search_param(
return Status::OK();
}
+ // DSL result cache: reuse InvertedIndexQueryCache with SEARCH_DSL_QUERY
type
+ auto* dsl_cache = enable_cache ? InvertedIndexQueryCache::instance() :
nullptr;
+ std::string seg_prefix;
+ std::string dsl_sig;
+ InvertedIndexQueryCache::CacheKey dsl_cache_key;
+ bool cache_usable = false;
+ if (dsl_cache) {
+ seg_prefix = extract_segment_prefix(iterators);
+ dsl_sig = build_dsl_signature(search_param);
+ if (!seg_prefix.empty() && !dsl_sig.empty()) {
+ dsl_cache_key = InvertedIndexQueryCache::CacheKey {
+ seg_prefix, "__search_dsl__",
InvertedIndexQueryType::SEARCH_DSL_QUERY,
+ dsl_sig};
+ cache_usable = true;
+ InvertedIndexQueryCacheHandle dsl_cache_handle;
+ if (dsl_cache->lookup(dsl_cache_key, &dsl_cache_handle)) {
+ auto cached_bitmap = dsl_cache_handle.get_bitmap();
+ if (cached_bitmap) {
+ // Also retrieve cached null bitmap for three-valued SQL
logic
+ // (needed by compound operators NOT, OR, AND in
VCompoundPred)
+ auto null_cache_key = InvertedIndexQueryCache::CacheKey {
+ seg_prefix, "__search_dsl__",
InvertedIndexQueryType::SEARCH_DSL_QUERY,
+ dsl_sig + "__null"};
+ InvertedIndexQueryCacheHandle null_cache_handle;
+ std::shared_ptr<roaring::Roaring> null_bitmap;
+ if (dsl_cache->lookup(null_cache_key, &null_cache_handle))
{
+ null_bitmap = null_cache_handle.get_bitmap();
+ }
+ if (!null_bitmap) {
+ null_bitmap = std::make_shared<roaring::Roaring>();
+ }
+ bitmap_result =
+ InvertedIndexResultBitmap(cached_bitmap,
std::move(null_bitmap));
+ return Status::OK();
+ }
+ }
+ }
+ }
+
auto context = std::make_shared<IndexQueryContext>();
context->collection_statistics = std::make_shared<CollectionStatistics>();
context->collection_similarity = std::make_shared<CollectionSimilarity>();
@@ -352,6 +460,21 @@ Status
FunctionSearch::evaluate_inverted_index_with_search_param(
VLOG_TRACE << "search: After mask - result_bitmap="
<< bitmap_result.get_data_bitmap()->cardinality();
+ // Insert post-mask_out_null result into DSL cache for future reuse
+ // Cache both data bitmap and null bitmap so compound operators (NOT, OR,
AND)
+ // can apply correct three-valued SQL logic on cache hit
+ if (dsl_cache && cache_usable) {
+ InvertedIndexQueryCacheHandle insert_handle;
+ dsl_cache->insert(dsl_cache_key, bitmap_result.get_data_bitmap(),
&insert_handle);
+ if (bitmap_result.get_null_bitmap()) {
+ auto null_cache_key = InvertedIndexQueryCache::CacheKey {
+ seg_prefix, "__search_dsl__",
InvertedIndexQueryType::SEARCH_DSL_QUERY,
+ dsl_sig + "__null"};
+ InvertedIndexQueryCacheHandle null_insert_handle;
+ dsl_cache->insert(null_cache_key, bitmap_result.get_null_bitmap(),
&null_insert_handle);
+ }
+ }
+
return Status::OK();
}
@@ -850,37 +973,6 @@ Status FunctionSearch::build_leaf_query(const
TSearchClause& clause,
return Status::OK();
}
-Status FunctionSearch::collect_all_field_nulls(
- const TSearchClause& clause,
- const std::unordered_map<std::string, IndexIterator*>& iterators,
- std::shared_ptr<roaring::Roaring>& null_bitmap) const {
- // Recursively collect NULL bitmaps from all fields referenced in the query
- if (clause.__isset.field_name) {
- const std::string& field_name = clause.field_name;
- auto it = iterators.find(field_name);
- if (it != iterators.end() && it->second) {
- auto has_null_result = it->second->has_null();
- if (has_null_result.has_value() && has_null_result.value()) {
- segment_v2::InvertedIndexQueryCacheHandle
null_bitmap_cache_handle;
-
RETURN_IF_ERROR(it->second->read_null_bitmap(&null_bitmap_cache_handle));
- auto field_null_bitmap = null_bitmap_cache_handle.get_bitmap();
- if (field_null_bitmap) {
- *null_bitmap |= *field_null_bitmap;
- }
- }
- }
- }
-
- // Recurse into child clauses
- if (clause.__isset.children) {
- for (const auto& child_clause : clause.children) {
- RETURN_IF_ERROR(collect_all_field_nulls(child_clause, iterators,
null_bitmap));
- }
- }
-
- return Status::OK();
-}
-
void register_function_search(SimpleFunctionFactory& factory) {
factory.register_function<FunctionSearch>();
}
diff --git a/be/src/vec/functions/function_search.h
b/be/src/vec/functions/function_search.h
index d8b7c08fac6..d86f23605b2 100644
--- a/be/src/vec/functions/function_search.h
+++ b/be/src/vec/functions/function_search.h
@@ -28,6 +28,7 @@
#include "gen_cpp/Exprs_types.h"
#include "olap/rowset/segment_v2/index_query_context.h"
#include
"olap/rowset/segment_v2/inverted_index/query_v2/boolean_query/operator_boolean_query.h"
+#include "olap/rowset/segment_v2/inverted_index_cache.h"
#include "vec/core/block.h"
#include "vec/core/types.h"
#include "vec/data_types/data_type.h"
@@ -121,6 +122,9 @@ private:
std::vector<std::shared_ptr<lucene::index::IndexReader>> _readers;
std::unordered_map<std::string,
std::shared_ptr<lucene::index::IndexReader>> _binding_readers;
std::unordered_map<std::wstring,
std::shared_ptr<lucene::index::IndexReader>> _field_readers;
+ // Keep searcher cache handles alive for the resolver's lifetime.
+ // This pins cached IndexSearcher entries so extracted IndexReaders remain
valid.
+ std::vector<segment_v2::InvertedIndexCacheHandle> _searcher_cache_handles;
};
class FunctionSearch : public IFunction {
@@ -163,7 +167,7 @@ public:
const std::unordered_map<std::string,
vectorized::IndexFieldNameAndTypePair>&
data_type_with_names,
std::unordered_map<std::string, IndexIterator*> iterators,
uint32_t num_rows,
- InvertedIndexResultBitmap& bitmap_result) const;
+ InvertedIndexResultBitmap& bitmap_result, bool enable_cache =
true) const;
// Public methods for testing
enum class ClauseTypeCategory {
@@ -193,10 +197,6 @@ public:
FieldReaderResolver& resolver,
inverted_index::query_v2::QueryPtr* out,
std::string* binding_key, const std::string&
default_operator,
int32_t minimum_should_match) const;
-
- Status collect_all_field_nulls(const TSearchClause& clause,
- const std::unordered_map<std::string,
IndexIterator*>& iterators,
- std::shared_ptr<roaring::Roaring>&
null_bitmap) const;
};
} // namespace doris::vectorized
diff --git
a/be/test/olap/rowset/segment_v2/search_function_query_cache_test.cpp
b/be/test/olap/rowset/segment_v2/search_function_query_cache_test.cpp
new file mode 100644
index 00000000000..fc257f02fb1
--- /dev/null
+++ b/be/test/olap/rowset/segment_v2/search_function_query_cache_test.cpp
@@ -0,0 +1,207 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <gtest/gtest.h>
+
+#include <memory>
+#include <roaring/roaring.hh>
+#include <string>
+
+#include "olap/rowset/segment_v2/inverted_index_cache.h"
+#include "olap/rowset/segment_v2/inverted_index_query_type.h"
+
+namespace doris::segment_v2 {
+
+class SearchDslQueryCacheTest : public testing::Test {
+public:
+ static const int kCacheSize = 4096;
+
+ void SetUp() override { _cache = new InvertedIndexQueryCache(kCacheSize,
1); }
+
+ void TearDown() override { delete _cache; }
+
+ InvertedIndexQueryCache::CacheKey make_key(const std::string& seg_prefix,
+ const std::string& dsl_sig) {
+ return InvertedIndexQueryCache::CacheKey {
+ seg_prefix, "__search_dsl__",
InvertedIndexQueryType::SEARCH_DSL_QUERY, dsl_sig};
+ }
+
+protected:
+ InvertedIndexQueryCache* _cache = nullptr;
+};
+
+TEST_F(SearchDslQueryCacheTest, insert_and_lookup) {
+ auto bm = std::make_shared<roaring::Roaring>();
+ bm->add(1);
+ bm->add(3);
+ bm->add(5);
+
+ auto key = make_key("segment_0001", "thrift_sig_abc");
+
+ InvertedIndexQueryCacheHandle handle;
+ _cache->insert(key, bm, &handle);
+
+ // Lookup should succeed
+ InvertedIndexQueryCacheHandle lookup_handle;
+ EXPECT_TRUE(_cache->lookup(key, &lookup_handle));
+
+ auto cached_bm = lookup_handle.get_bitmap();
+ ASSERT_NE(cached_bm, nullptr);
+ EXPECT_TRUE(cached_bm->contains(1));
+ EXPECT_TRUE(cached_bm->contains(3));
+ EXPECT_TRUE(cached_bm->contains(5));
+ EXPECT_FALSE(cached_bm->contains(2));
+ EXPECT_EQ(cached_bm->cardinality(), 3);
+}
+
+TEST_F(SearchDslQueryCacheTest, lookup_miss) {
+ auto key = make_key("segment_0001", "thrift_sig_abc");
+
+ InvertedIndexQueryCacheHandle handle;
+ EXPECT_FALSE(_cache->lookup(key, &handle));
+}
+
+TEST_F(SearchDslQueryCacheTest, different_keys_independent) {
+ auto bm1 = std::make_shared<roaring::Roaring>();
+ bm1->add(10);
+ auto bm2 = std::make_shared<roaring::Roaring>();
+ bm2->add(20);
+
+ auto key1 = make_key("seg_a", "dsl_1");
+ auto key2 = make_key("seg_a", "dsl_2");
+
+ {
+ InvertedIndexQueryCacheHandle h;
+ _cache->insert(key1, bm1, &h);
+ }
+ {
+ InvertedIndexQueryCacheHandle h;
+ _cache->insert(key2, bm2, &h);
+ }
+
+ // Lookup key1
+ {
+ InvertedIndexQueryCacheHandle h;
+ EXPECT_TRUE(_cache->lookup(key1, &h));
+ auto cached = h.get_bitmap();
+ ASSERT_NE(cached, nullptr);
+ EXPECT_TRUE(cached->contains(10));
+ EXPECT_FALSE(cached->contains(20));
+ }
+
+ // Lookup key2
+ {
+ InvertedIndexQueryCacheHandle h;
+ EXPECT_TRUE(_cache->lookup(key2, &h));
+ auto cached = h.get_bitmap();
+ ASSERT_NE(cached, nullptr);
+ EXPECT_TRUE(cached->contains(20));
+ EXPECT_FALSE(cached->contains(10));
+ }
+}
+
+TEST_F(SearchDslQueryCacheTest, different_segments_independent) {
+ auto bm1 = std::make_shared<roaring::Roaring>();
+ bm1->add(1);
+ auto bm2 = std::make_shared<roaring::Roaring>();
+ bm2->add(2);
+
+ auto key1 = make_key("seg_a", "same_dsl");
+ auto key2 = make_key("seg_b", "same_dsl");
+
+ {
+ InvertedIndexQueryCacheHandle h;
+ _cache->insert(key1, bm1, &h);
+ }
+ {
+ InvertedIndexQueryCacheHandle h;
+ _cache->insert(key2, bm2, &h);
+ }
+
+ {
+ InvertedIndexQueryCacheHandle h;
+ EXPECT_TRUE(_cache->lookup(key1, &h));
+ EXPECT_TRUE(h.get_bitmap()->contains(1));
+ EXPECT_FALSE(h.get_bitmap()->contains(2));
+ }
+ {
+ InvertedIndexQueryCacheHandle h;
+ EXPECT_TRUE(_cache->lookup(key2, &h));
+ EXPECT_TRUE(h.get_bitmap()->contains(2));
+ EXPECT_FALSE(h.get_bitmap()->contains(1));
+ }
+}
+
+TEST_F(SearchDslQueryCacheTest, no_collision_with_regular_query_cache) {
+ // SEARCH_DSL_QUERY key should not collide with a regular EQUAL_QUERY key
+ // even with same index_path and value
+ auto bm_dsl = std::make_shared<roaring::Roaring>();
+ bm_dsl->add(100);
+ auto bm_eq = std::make_shared<roaring::Roaring>();
+ bm_eq->add(200);
+
+ InvertedIndexQueryCache::CacheKey dsl_key {
+ "seg_a", "__search_dsl__",
InvertedIndexQueryType::SEARCH_DSL_QUERY, "some_value"};
+ InvertedIndexQueryCache::CacheKey eq_key {"seg_a", "__search_dsl__",
+
InvertedIndexQueryType::EQUAL_QUERY, "some_value"};
+
+ {
+ InvertedIndexQueryCacheHandle h;
+ _cache->insert(dsl_key, bm_dsl, &h);
+ }
+ {
+ InvertedIndexQueryCacheHandle h;
+ _cache->insert(eq_key, bm_eq, &h);
+ }
+
+ {
+ InvertedIndexQueryCacheHandle h;
+ EXPECT_TRUE(_cache->lookup(dsl_key, &h));
+ EXPECT_TRUE(h.get_bitmap()->contains(100));
+ }
+ {
+ InvertedIndexQueryCacheHandle h;
+ EXPECT_TRUE(_cache->lookup(eq_key, &h));
+ EXPECT_TRUE(h.get_bitmap()->contains(200));
+ }
+}
+
+TEST_F(SearchDslQueryCacheTest, overwrite_same_key) {
+ auto bm1 = std::make_shared<roaring::Roaring>();
+ bm1->add(1);
+ auto bm2 = std::make_shared<roaring::Roaring>();
+ bm2->add(99);
+
+ auto key = make_key("seg", "dsl");
+
+ {
+ InvertedIndexQueryCacheHandle h;
+ _cache->insert(key, bm1, &h);
+ }
+ {
+ InvertedIndexQueryCacheHandle h;
+ _cache->insert(key, bm2, &h);
+ }
+
+ InvertedIndexQueryCacheHandle h;
+ EXPECT_TRUE(_cache->lookup(key, &h));
+ auto cached = h.get_bitmap();
+ ASSERT_NE(cached, nullptr);
+ EXPECT_TRUE(cached->contains(99));
+}
+
+} // namespace doris::segment_v2
diff --git a/be/test/vec/function/function_search_test.cpp
b/be/test/vec/function/function_search_test.cpp
index 4daa48f662a..4e5b7cb2e84 100644
--- a/be/test/vec/function/function_search_test.cpp
+++ b/be/test/vec/function/function_search_test.cpp
@@ -59,40 +59,6 @@ public:
Result<bool> has_null() override { return false; }
};
-class TrackingIndexIterator : public segment_v2::IndexIterator {
-public:
- explicit TrackingIndexIterator(bool has_null) : _has_null(has_null) {}
-
- segment_v2::IndexReaderPtr get_reader(
- segment_v2::IndexReaderType /*reader_type*/) const override {
- return nullptr;
- }
-
- Status read_from_index(const segment_v2::IndexParam& /*param*/) override {
- return Status::OK();
- }
-
- Status read_null_bitmap(segment_v2::InvertedIndexQueryCacheHandle*
/*cache_handle*/) override {
- ++_read_null_bitmap_calls;
- return Status::OK();
- }
-
- Result<bool> has_null() override {
- ++_has_null_checks;
- return _has_null;
- }
-
- int read_null_bitmap_calls() const { return _read_null_bitmap_calls; }
- int has_null_checks() const { return _has_null_checks; }
-
- void set_has_null(bool value) { _has_null = value; }
-
-private:
- bool _has_null = false;
- int _read_null_bitmap_calls = 0;
- int _has_null_checks = 0;
-};
-
TEST_F(FunctionSearchTest, TestGetName) {
EXPECT_EQ("search", function_search->get_name());
}
@@ -1561,40 +1527,6 @@ TEST_F(FunctionSearchTest,
TestEvaluateInvertedIndexWithSearchParamComplexQuery)
}
TEST_F(FunctionSearchTest, TestOrCrossFieldMatchesMatchAnyRows) {
- TSearchClause left_clause;
- left_clause.clause_type = "TERM";
- left_clause.field_name = "title";
- left_clause.value = "foo";
- left_clause.__isset.field_name = true;
- left_clause.__isset.value = true;
-
- TSearchClause right_clause;
- right_clause.clause_type = "TERM";
- right_clause.field_name = "content";
- right_clause.value = "bar";
- right_clause.__isset.field_name = true;
- right_clause.__isset.value = true;
-
- TSearchClause root_clause;
- root_clause.clause_type = "OR";
- root_clause.children = {left_clause, right_clause};
- root_clause.__isset.children = true;
-
- auto left_iterator = std::make_unique<TrackingIndexIterator>(true);
- auto right_iterator = std::make_unique<TrackingIndexIterator>(true);
-
- std::unordered_map<std::string, IndexIterator*> iterators_map = {
- {"title", left_iterator.get()}, {"content", right_iterator.get()}};
-
- auto null_bitmap = std::make_shared<roaring::Roaring>();
- auto status = function_search->collect_all_field_nulls(root_clause,
iterators_map, null_bitmap);
- EXPECT_TRUE(status.ok());
- EXPECT_GE(left_iterator->has_null_checks(), 1);
- EXPECT_GE(right_iterator->has_null_checks(), 1);
- EXPECT_GE(left_iterator->read_null_bitmap_calls(), 1);
- EXPECT_GE(right_iterator->read_null_bitmap_calls(), 1);
- EXPECT_TRUE(null_bitmap->isEmpty());
-
auto data_bitmap = std::make_shared<roaring::Roaring>();
data_bitmap->add(1);
data_bitmap->add(3);
@@ -1622,38 +1554,6 @@ TEST_F(FunctionSearchTest,
TestOrCrossFieldMatchesMatchAnyRows) {
}
TEST_F(FunctionSearchTest, TestOrWithNotSameFieldMatchesMatchAllRows) {
- TSearchClause include_clause;
- include_clause.clause_type = "TERM";
- include_clause.field_name = "title";
- include_clause.value = "foo";
- include_clause.__isset.field_name = true;
- include_clause.__isset.value = true;
-
- TSearchClause exclude_child;
- exclude_child.clause_type = "TERM";
- exclude_child.field_name = "title";
- exclude_child.value = "bar";
- exclude_child.__isset.field_name = true;
- exclude_child.__isset.value = true;
-
- TSearchClause exclude_clause;
- exclude_clause.clause_type = "NOT";
- exclude_clause.children = {exclude_child};
-
- TSearchClause root_clause;
- root_clause.clause_type = "OR";
- root_clause.children = {include_clause, exclude_clause};
- root_clause.__isset.children = true;
-
- auto iterator = std::make_unique<TrackingIndexIterator>(true);
- std::unordered_map<std::string, IndexIterator*> iterators_map = {{"title",
iterator.get()}};
-
- auto null_bitmap = std::make_shared<roaring::Roaring>();
- auto status = function_search->collect_all_field_nulls(root_clause,
iterators_map, null_bitmap);
- EXPECT_TRUE(status.ok());
- EXPECT_GE(iterator->has_null_checks(), 1);
- EXPECT_GE(iterator->read_null_bitmap_calls(), 1);
-
auto data_bitmap = std::make_shared<roaring::Roaring>();
data_bitmap->add(1);
data_bitmap->add(2);
@@ -2201,4 +2101,19 @@ TEST_F(FunctionSearchTest,
TestEvaluateInvertedIndexWithOccurBoolean) {
EXPECT_TRUE(status.is<ErrorCode::INVERTED_INDEX_FILE_NOT_FOUND>());
}
+TEST_F(FunctionSearchTest, TestSearcherCacheHandlesLifetime) {
+ // Verify FieldReaderResolver keeps _searcher_cache_handles alive
+ std::unordered_map<std::string, vectorized::IndexFieldNameAndTypePair>
data_types;
+ std::unordered_map<std::string, IndexIterator*> iterators;
+ auto context = std::make_shared<IndexQueryContext>();
+
+ FieldReaderResolver resolver(data_types, iterators, context);
+
+ // The resolver should have an empty cache handles vector initially
+ // (We can't directly access _searcher_cache_handles, but we can verify
+ // that binding_cache is empty)
+ EXPECT_TRUE(resolver.binding_cache().empty());
+ EXPECT_TRUE(resolver.readers().empty());
+}
+
} // namespace doris::vectorized
diff --git a/regression-test/suites/search/test_search_boundary_cases.groovy
b/regression-test/suites/search/test_search_boundary_cases.groovy
index b7c3a931252..5ab2a9386aa 100644
--- a/regression-test/suites/search/test_search_boundary_cases.groovy
+++ b/regression-test/suites/search/test_search_boundary_cases.groovy
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-suite("test_search_boundary_cases") {
+suite("test_search_boundary_cases", "p0") {
def tableName = "search_boundary_test"
// Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy
testing.
diff --git a/regression-test/suites/search/test_search_cache.groovy
b/regression-test/suites/search/test_search_cache.groovy
new file mode 100644
index 00000000000..39af2184bb7
--- /dev/null
+++ b/regression-test/suites/search/test_search_cache.groovy
@@ -0,0 +1,138 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_search_cache", "p0") {
+ def tableName = "search_cache_test"
+
+ sql "DROP TABLE IF EXISTS ${tableName}"
+ sql """
+ CREATE TABLE ${tableName} (
+ id INT,
+ title VARCHAR(200),
+ content VARCHAR(500),
+ INDEX idx_title(title) USING INVERTED PROPERTIES("parser" =
"english"),
+ INDEX idx_content(content) USING INVERTED PROPERTIES("parser" =
"english")
+ ) ENGINE=OLAP
+ DUPLICATE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ """
+
+ sql """INSERT INTO ${tableName} VALUES
+ (1, 'apple banana cherry', 'red fruit sweet'),
+ (2, 'banana grape mango', 'yellow fruit tropical'),
+ (3, 'cherry plum peach', 'stone fruit summer'),
+ (4, 'apple grape kiwi', 'green fruit fresh'),
+ (5, 'mango pineapple coconut', 'tropical fruit exotic'),
+ (6, 'apple cherry plum', 'mixed fruit salad'),
+ (7, 'banana coconut papaya', 'smoothie blend tropical'),
+ (8, 'grape cherry apple', 'wine fruit tart')
+ """
+ sql """sync"""
+
+ // sync ensures data is flushed. Sleep is a best-effort wait for
+ // background index availability; may need to increase under load.
+ Thread.sleep(2000)
+
+ // Test 1: Cache consistency - same query returns same results with cache
enabled
+ def result1 = sql """
+ SELECT
/*+SET_VAR(enable_common_expr_pushdown=true,enable_inverted_index_query_cache=true)
*/
+ id FROM ${tableName}
+ WHERE search('title:apple')
+ ORDER BY id
+ """
+
+ // Run same query again (should hit cache)
+ def result2 = sql """
+ SELECT
/*+SET_VAR(enable_common_expr_pushdown=true,enable_inverted_index_query_cache=true)
*/
+ id FROM ${tableName}
+ WHERE search('title:apple')
+ ORDER BY id
+ """
+
+ // Results must be identical (cache hit returns same data)
+ assertEquals(result1, result2)
+
+ // Test 2: Cache disabled returns same results as cache enabled
+ def result_no_cache = sql """
+ SELECT
/*+SET_VAR(enable_common_expr_pushdown=true,enable_inverted_index_query_cache=false)
*/
+ id FROM ${tableName}
+ WHERE search('title:apple')
+ ORDER BY id
+ """
+ assertEquals(result1, result_no_cache)
+
+ // Test 3: Multi-field query cache consistency
+ def mf_result1 = sql """
+ SELECT
/*+SET_VAR(enable_common_expr_pushdown=true,enable_inverted_index_query_cache=true)
*/
+ id FROM ${tableName}
+ WHERE search('title:cherry OR content:tropical')
+ ORDER BY id
+ """
+
+ def mf_result2 = sql """
+ SELECT
/*+SET_VAR(enable_common_expr_pushdown=true,enable_inverted_index_query_cache=true)
*/
+ id FROM ${tableName}
+ WHERE search('title:cherry OR content:tropical')
+ ORDER BY id
+ """
+ assertEquals(mf_result1, mf_result2)
+
+ // Test 4: Different queries produce different cache entries
+ def diff_result = sql """
+ SELECT
/*+SET_VAR(enable_common_expr_pushdown=true,enable_inverted_index_query_cache=true)
*/
+ id FROM ${tableName}
+ WHERE search('title:banana')
+ ORDER BY id
+ """
+ // banana result should differ from apple result
+ assertNotEquals(result1, diff_result)
+
+ // Test 5: AND query - cache vs no-cache consistency
+ def and_cached = sql """
+ SELECT
/*+SET_VAR(enable_common_expr_pushdown=true,enable_inverted_index_query_cache=true)
*/
+ id, title FROM ${tableName}
+ WHERE search('title:apple AND title:cherry')
+ ORDER BY id
+ """
+
+ def and_uncached = sql """
+ SELECT
/*+SET_VAR(enable_common_expr_pushdown=true,enable_inverted_index_query_cache=false)
*/
+ id, title FROM ${tableName}
+ WHERE search('title:apple AND title:cherry')
+ ORDER BY id
+ """
+ assertEquals(and_cached, and_uncached)
+
+ // Test 6: Complex boolean query - cache vs no-cache consistency
+ def complex_cached = sql """
+ SELECT
/*+SET_VAR(enable_common_expr_pushdown=true,enable_inverted_index_query_cache=true)
*/
+ id, title FROM ${tableName}
+ WHERE search('(title:apple OR title:banana) AND content:fruit')
+ ORDER BY id
+ """
+
+ def complex_uncached = sql """
+ SELECT
/*+SET_VAR(enable_common_expr_pushdown=true,enable_inverted_index_query_cache=false)
*/
+ id, title FROM ${tableName}
+ WHERE search('(title:apple OR title:banana) AND content:fruit')
+ ORDER BY id
+ """
+ assertEquals(complex_cached, complex_uncached)
+
+ sql "DROP TABLE IF EXISTS ${tableName}"
+}
diff --git
a/regression-test/suites/search/test_search_default_field_operator.groovy
b/regression-test/suites/search/test_search_default_field_operator.groovy
index 89d07a794be..654a9ad2abf 100644
--- a/regression-test/suites/search/test_search_default_field_operator.groovy
+++ b/regression-test/suites/search/test_search_default_field_operator.groovy
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-suite("test_search_default_field_operator") {
+suite("test_search_default_field_operator", "p0") {
def tableName = "search_enhanced_test"
// Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy
testing.
diff --git a/regression-test/suites/search/test_search_dsl_operators.groovy
b/regression-test/suites/search/test_search_dsl_operators.groovy
index 7415dfab10a..08106c65243 100644
--- a/regression-test/suites/search/test_search_dsl_operators.groovy
+++ b/regression-test/suites/search/test_search_dsl_operators.groovy
@@ -35,7 +35,7 @@
* - NOT marks current term as MUST_NOT (-)
* - With minimum_should_match=0 and MUST clauses present, SHOULD clauses are
discarded
*/
-suite("test_search_dsl_operators") {
+suite("test_search_dsl_operators", "p0") {
def tableName = "search_dsl_operators_test"
// Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy
testing.
diff --git a/regression-test/suites/search/test_search_dsl_syntax.groovy
b/regression-test/suites/search/test_search_dsl_syntax.groovy
index e0ec9010926..b52a018fa03 100644
--- a/regression-test/suites/search/test_search_dsl_syntax.groovy
+++ b/regression-test/suites/search/test_search_dsl_syntax.groovy
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-suite("test_search_dsl_syntax") {
+suite("test_search_dsl_syntax", "p0") {
def tableName = "search_dsl_test_table"
sql "DROP TABLE IF EXISTS ${tableName}"
diff --git a/regression-test/suites/search/test_search_escape.groovy
b/regression-test/suites/search/test_search_escape.groovy
index 18e999e2767..e7e09ca846c 100644
--- a/regression-test/suites/search/test_search_escape.groovy
+++ b/regression-test/suites/search/test_search_escape.groovy
@@ -29,7 +29,7 @@
* - Groovy string: \\\\ -> SQL string: \\ -> DSL: \ (escape char)
* - Groovy string: \\\\\\\\ -> SQL string: \\\\ -> DSL: \\ -> literal: \
*/
-suite("test_search_escape") {
+suite("test_search_escape", "p0") {
def tableName = "search_escape_test"
// Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy
testing.
diff --git a/regression-test/suites/search/test_search_exact_basic.groovy
b/regression-test/suites/search/test_search_exact_basic.groovy
index 0c9c368c340..ffad823e451 100644
--- a/regression-test/suites/search/test_search_exact_basic.groovy
+++ b/regression-test/suites/search/test_search_exact_basic.groovy
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-suite("test_search_exact_basic") {
+suite("test_search_exact_basic", "p0") {
def tableName = "exact_basic_test"
// Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy
testing.
diff --git a/regression-test/suites/search/test_search_exact_lowercase.groovy
b/regression-test/suites/search/test_search_exact_lowercase.groovy
index 8389d7e72ed..f4b82d411b6 100644
--- a/regression-test/suites/search/test_search_exact_lowercase.groovy
+++ b/regression-test/suites/search/test_search_exact_lowercase.groovy
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-suite("test_search_exact_lowercase") {
+suite("test_search_exact_lowercase", "p0") {
def tableName = "exact_lowercase_test"
// Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy
testing.
diff --git a/regression-test/suites/search/test_search_exact_match.groovy
b/regression-test/suites/search/test_search_exact_match.groovy
index caf55679375..6f8439b0d22 100644
--- a/regression-test/suites/search/test_search_exact_match.groovy
+++ b/regression-test/suites/search/test_search_exact_match.groovy
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-suite("test_search_exact_match") {
+suite("test_search_exact_match", "p0") {
def tableName = "search_exact_test_table"
// Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy
testing.
diff --git a/regression-test/suites/search/test_search_exact_multi_index.groovy
b/regression-test/suites/search/test_search_exact_multi_index.groovy
index 1230b53aa4d..8c11a560970 100644
--- a/regression-test/suites/search/test_search_exact_multi_index.groovy
+++ b/regression-test/suites/search/test_search_exact_multi_index.groovy
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-suite("test_search_exact_multi_index") {
+suite("test_search_exact_multi_index", "p0") {
def tableName = "exact_multi_index_test"
// Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy
testing.
diff --git a/regression-test/suites/search/test_search_function.groovy
b/regression-test/suites/search/test_search_function.groovy
index bf09ca237f6..61ee8e4b026 100644
--- a/regression-test/suites/search/test_search_function.groovy
+++ b/regression-test/suites/search/test_search_function.groovy
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-suite("test_search_function") {
+suite("test_search_function", "p0") {
def tableName = "search_test_table"
def indexTableName = "search_test_index_table"
diff --git a/regression-test/suites/search/test_search_inverted_index.groovy
b/regression-test/suites/search/test_search_inverted_index.groovy
index 6314a291bb3..d4d83384d76 100644
--- a/regression-test/suites/search/test_search_inverted_index.groovy
+++ b/regression-test/suites/search/test_search_inverted_index.groovy
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-suite("test_search_inverted_index") {
+suite("test_search_inverted_index", "p0") {
def tableWithIndex = "search_index_test_table"
def tableWithoutIndex = "search_no_index_test_table"
diff --git a/regression-test/suites/search/test_search_lucene_mode.groovy
b/regression-test/suites/search/test_search_lucene_mode.groovy
index f971d8a9729..4a2a5c7d94c 100644
--- a/regression-test/suites/search/test_search_lucene_mode.groovy
+++ b/regression-test/suites/search/test_search_lucene_mode.groovy
@@ -30,7 +30,7 @@
* Enable Lucene mode with options parameter (JSON format):
* search(dsl,
'{"default_field":"title","default_operator":"and","mode":"lucene"}')
*/
-suite("test_search_lucene_mode") {
+suite("test_search_lucene_mode", "p0") {
def tableName = "search_lucene_mode_test"
// Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy
testing.
diff --git a/regression-test/suites/search/test_search_mow_support.groovy
b/regression-test/suites/search/test_search_mow_support.groovy
index ce759ad99c9..279cc45b1df 100644
--- a/regression-test/suites/search/test_search_mow_support.groovy
+++ b/regression-test/suites/search/test_search_mow_support.groovy
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-suite("test_search_mow_support") {
+suite("test_search_mow_support", "p0") {
def tableName = "search_mow_support_tbl"
sql "DROP TABLE IF EXISTS ${tableName}"
diff --git a/regression-test/suites/search/test_search_multi_field.groovy
b/regression-test/suites/search/test_search_multi_field.groovy
index ca1c97eef5a..44f9d9afad9 100644
--- a/regression-test/suites/search/test_search_multi_field.groovy
+++ b/regression-test/suites/search/test_search_multi_field.groovy
@@ -30,7 +30,7 @@
*
* Multi-field search can also be combined with Lucene mode for
MUST/SHOULD/MUST_NOT semantics.
*/
-suite("test_search_multi_field") {
+suite("test_search_multi_field", "p0") {
def tableName = "search_multi_field_test"
// Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy
testing.
diff --git a/regression-test/suites/search/test_search_null_regression.groovy
b/regression-test/suites/search/test_search_null_regression.groovy
index 70666341662..c2acfeb51b2 100644
--- a/regression-test/suites/search/test_search_null_regression.groovy
+++ b/regression-test/suites/search/test_search_null_regression.groovy
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-suite("test_search_null_regression") {
+suite("test_search_null_regression", "p0") {
def tableName = "search_null_regression_test"
// Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy
testing.
diff --git a/regression-test/suites/search/test_search_null_semantics.groovy
b/regression-test/suites/search/test_search_null_semantics.groovy
index 1c49e350bf3..1a16adb03aa 100644
--- a/regression-test/suites/search/test_search_null_semantics.groovy
+++ b/regression-test/suites/search/test_search_null_semantics.groovy
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-suite("test_search_null_semantics") {
+suite("test_search_null_semantics", "p0") {
def tableName = "search_null_test"
// Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy
testing.
diff --git a/regression-test/suites/search/test_search_regexp_lowercase.groovy
b/regression-test/suites/search/test_search_regexp_lowercase.groovy
index 7151ac1c10b..81b93d0de7d 100644
--- a/regression-test/suites/search/test_search_regexp_lowercase.groovy
+++ b/regression-test/suites/search/test_search_regexp_lowercase.groovy
@@ -19,7 +19,7 @@
// Regex patterns are NOT lowercased (matching ES query_string behavior).
// Wildcard patterns ARE lowercased (matching ES query_string normalizer
behavior).
-suite("test_search_regexp_lowercase") {
+suite("test_search_regexp_lowercase", "p0") {
def tableName = "search_regexp_lowercase_test"
// Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy
testing.
diff --git
a/regression-test/suites/search/test_search_usage_restrictions.groovy
b/regression-test/suites/search/test_search_usage_restrictions.groovy
index ea31a4eb998..842fa5454ad 100644
--- a/regression-test/suites/search/test_search_usage_restrictions.groovy
+++ b/regression-test/suites/search/test_search_usage_restrictions.groovy
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-suite("test_search_usage_restrictions") {
+suite("test_search_usage_restrictions", "p0") {
def tableName = "search_usage_test_table"
def tableName2 = "search_usage_test_table2"
diff --git
a/regression-test/suites/search/test_search_variant_dual_index_reader.groovy
b/regression-test/suites/search/test_search_variant_dual_index_reader.groovy
index 6e53370381f..2463d126ba6 100644
--- a/regression-test/suites/search/test_search_variant_dual_index_reader.groovy
+++ b/regression-test/suites/search/test_search_variant_dual_index_reader.groovy
@@ -33,7 +33,7 @@
* Before fix: search() returns empty (wrong reader selected)
* After fix: search() returns matching rows (correct FULLTEXT reader
selected)
*/
-suite("test_search_variant_dual_index_reader") {
+suite("test_search_variant_dual_index_reader", "p0") {
def tableName = "test_variant_dual_index_reader"
sql """ set enable_match_without_inverted_index = false """
diff --git
a/regression-test/suites/search/test_search_variant_subcolumn_analyzer.groovy
b/regression-test/suites/search/test_search_variant_subcolumn_analyzer.groovy
index 61c7c34c6e9..69bc0f29157 100644
---
a/regression-test/suites/search/test_search_variant_subcolumn_analyzer.groovy
+++
b/regression-test/suites/search/test_search_variant_subcolumn_analyzer.groovy
@@ -28,7 +28,7 @@
* Fix: FE now looks up the Index for each field in SearchExpression and passes
* the index_properties via TSearchFieldBinding to BE.
*/
-suite("test_search_variant_subcolumn_analyzer") {
+suite("test_search_variant_subcolumn_analyzer", "p0") {
def tableName = "test_variant_subcolumn_analyzer"
sql """ set enable_match_without_inverted_index = false """
diff --git
a/regression-test/suites/search/test_search_vs_match_consistency.groovy
b/regression-test/suites/search/test_search_vs_match_consistency.groovy
index ac4cd1f802b..5b2c7b4b664 100644
--- a/regression-test/suites/search/test_search_vs_match_consistency.groovy
+++ b/regression-test/suites/search/test_search_vs_match_consistency.groovy
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-suite("test_search_vs_match_consistency") {
+suite("test_search_vs_match_consistency", "p0") {
def tableName = "search_match_consistency_test"
// Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy
testing.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]