This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new 24f3eca6e81 branch-4.1: [fix](profile) populate inverted index metrics
for SEARCH() query path #62121 (#62180)
24f3eca6e81 is described below
commit 24f3eca6e8125bcb52c1d3988dcf3522e66c1180
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Apr 8 10:48:42 2026 +0800
branch-4.1: [fix](profile) populate inverted index metrics for SEARCH()
query path #62121 (#62180)
Cherry-picked from #62121
Co-authored-by: Jack <[email protected]>
---
be/src/exec/operator/olap_scan_operator.cpp | 40 +--
be/src/exprs/function/function_search.cpp | 113 ++++++--
.../inverted/inverted_index_compound_reader.cpp | 2 +-
.../inverted_index_compound_reader_test.cpp | 57 ++++
.../search/test_search_inverted_index_profile.out | 22 ++
.../test_search_inverted_index_profile.groovy | 302 +++++++++++++++++++++
6 files changed, 499 insertions(+), 37 deletions(-)
diff --git a/be/src/exec/operator/olap_scan_operator.cpp
b/be/src/exec/operator/olap_scan_operator.cpp
index 19e20dc73c0..a80ce982cda 100644
--- a/be/src/exec/operator/olap_scan_operator.cpp
+++ b/be/src/exec/operator/olap_scan_operator.cpp
@@ -238,33 +238,37 @@ Status OlapScanLocalState::_init_profile() {
_statistics_collect_timer = ADD_TIMER(_scanner_profile,
"StatisticsCollectTime");
_inverted_index_filter_counter =
- ADD_COUNTER(_segment_profile, "RowsInvertedIndexFiltered",
TUnit::UNIT);
- _inverted_index_filter_timer = ADD_TIMER(_segment_profile,
"InvertedIndexFilterTime");
+ ADD_COUNTER_WITH_LEVEL(_segment_profile,
"RowsInvertedIndexFiltered", TUnit::UNIT, 1);
+ _inverted_index_filter_timer =
+ ADD_TIMER_WITH_LEVEL(_segment_profile, "InvertedIndexFilterTime",
1);
_inverted_index_query_cache_hit_counter =
- ADD_COUNTER(_segment_profile, "InvertedIndexQueryCacheHit",
TUnit::UNIT);
+ ADD_COUNTER_WITH_LEVEL(_segment_profile,
"InvertedIndexQueryCacheHit", TUnit::UNIT, 1);
_inverted_index_query_cache_miss_counter =
- ADD_COUNTER(_segment_profile, "InvertedIndexQueryCacheMiss",
TUnit::UNIT);
- _inverted_index_query_timer = ADD_TIMER(_segment_profile,
"InvertedIndexQueryTime");
+ ADD_COUNTER_WITH_LEVEL(_segment_profile,
"InvertedIndexQueryCacheMiss", TUnit::UNIT, 1);
+ _inverted_index_query_timer =
+ ADD_TIMER_WITH_LEVEL(_segment_profile, "InvertedIndexQueryTime",
1);
_inverted_index_query_null_bitmap_timer =
- ADD_TIMER(_segment_profile, "InvertedIndexQueryNullBitmapTime");
+ ADD_TIMER_WITH_LEVEL(_segment_profile,
"InvertedIndexQueryNullBitmapTime", 1);
_inverted_index_query_bitmap_copy_timer =
- ADD_TIMER(_segment_profile, "InvertedIndexQueryBitmapCopyTime");
+ ADD_TIMER_WITH_LEVEL(_segment_profile,
"InvertedIndexQueryBitmapCopyTime", 1);
_inverted_index_searcher_open_timer =
- ADD_TIMER(_segment_profile, "InvertedIndexSearcherOpenTime");
+ ADD_TIMER_WITH_LEVEL(_segment_profile,
"InvertedIndexSearcherOpenTime", 1);
_inverted_index_searcher_search_timer =
- ADD_TIMER(_segment_profile, "InvertedIndexSearcherSearchTime");
+ ADD_TIMER_WITH_LEVEL(_segment_profile,
"InvertedIndexSearcherSearchTime", 1);
_inverted_index_searcher_search_init_timer =
- ADD_TIMER(_segment_profile, "InvertedIndexSearcherSearchInitTime");
+ ADD_TIMER_WITH_LEVEL(_segment_profile,
"InvertedIndexSearcherSearchInitTime", 1);
_inverted_index_searcher_search_exec_timer =
- ADD_TIMER(_segment_profile, "InvertedIndexSearcherSearchExecTime");
- _inverted_index_searcher_cache_hit_counter =
- ADD_COUNTER(_segment_profile, "InvertedIndexSearcherCacheHit",
TUnit::UNIT);
- _inverted_index_searcher_cache_miss_counter =
- ADD_COUNTER(_segment_profile, "InvertedIndexSearcherCacheMiss",
TUnit::UNIT);
+ ADD_TIMER_WITH_LEVEL(_segment_profile,
"InvertedIndexSearcherSearchExecTime", 1);
+ _inverted_index_searcher_cache_hit_counter = ADD_COUNTER_WITH_LEVEL(
+ _segment_profile, "InvertedIndexSearcherCacheHit", TUnit::UNIT, 1);
+ _inverted_index_searcher_cache_miss_counter = ADD_COUNTER_WITH_LEVEL(
+ _segment_profile, "InvertedIndexSearcherCacheMiss", TUnit::UNIT,
1);
_inverted_index_downgrade_count_counter =
- ADD_COUNTER(_segment_profile, "InvertedIndexDowngradeCount",
TUnit::UNIT);
- _inverted_index_analyzer_timer = ADD_TIMER(_segment_profile,
"InvertedIndexAnalyzerTime");
- _inverted_index_lookup_timer = ADD_TIMER(_segment_profile,
"InvertedIndexLookupTimer");
+ ADD_COUNTER_WITH_LEVEL(_segment_profile,
"InvertedIndexDowngradeCount", TUnit::UNIT, 1);
+ _inverted_index_analyzer_timer =
+ ADD_TIMER_WITH_LEVEL(_segment_profile,
"InvertedIndexAnalyzerTime", 1);
+ _inverted_index_lookup_timer =
+ ADD_TIMER_WITH_LEVEL(_segment_profile, "InvertedIndexLookupTimer",
1);
_output_index_result_column_timer = ADD_TIMER(_segment_profile,
"OutputIndexResultColumnTime");
_filtered_segment_counter = ADD_COUNTER(_segment_profile,
"NumSegmentFiltered", TUnit::UNIT);
diff --git a/be/src/exprs/function/function_search.cpp
b/be/src/exprs/function/function_search.cpp
index de09e85c561..6c96da46c5b 100644
--- a/be/src/exprs/function/function_search.cpp
+++ b/be/src/exprs/function/function_search.cpp
@@ -37,9 +37,11 @@
#include "core/data_type/data_type_string.h"
#include "exprs/function/simple_function_factory.h"
#include "exprs/vexpr_context.h"
+#include "runtime/runtime_profile.h"
#include "storage/index/index_file_reader.h"
#include "storage/index/index_query_context.h"
#include "storage/index/inverted/analyzer/analyzer.h"
+#include "storage/index/inverted/inverted_index_compound_reader.h"
#include "storage/index/inverted/inverted_index_iterator.h"
#include "storage/index/inverted/inverted_index_parser.h"
#include "storage/index/inverted/inverted_index_reader.h"
@@ -62,6 +64,7 @@
#include "storage/segment/variant/nested_group_provider.h"
#include "storage/segment/variant/variant_column_reader.h"
#include "storage/types.h"
+#include "util/debug_points.h"
#include "util/string_util.h"
#include "util/thrift_util.h"
@@ -265,16 +268,31 @@ Status FieldReaderResolver::resolve(const std::string&
field_name,
"index file reader is null for field '{}'", field_name);
}
- // Use InvertedIndexSearcherCache to avoid re-opening index files
repeatedly
+ // Use InvertedIndexSearcherCache to avoid re-opening index files
repeatedly,
+ // respecting the enable_inverted_index_searcher_cache session variable.
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);
+
+ bool searcher_cache_enabled =
+ _context->runtime_state != nullptr &&
+
_context->runtime_state->query_options().enable_inverted_index_searcher_cache;
+
+ bool cache_hit = false;
+ if (searcher_cache_enabled) {
+ int64_t lookup_dummy = 0;
+ SCOPED_RAW_TIMER(_context->stats ?
&_context->stats->inverted_index_lookup_timer
+ : &lookup_dummy);
+ cache_hit =
InvertedIndexSearcherCache::instance()->lookup(searcher_cache_key,
+
&searcher_cache_handle);
+ }
std::shared_ptr<lucene::index::IndexReader> reader_holder;
if (cache_hit) {
+ if (_context->stats) {
+ _context->stats->inverted_index_searcher_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) {
@@ -285,7 +303,13 @@ Status FieldReaderResolver::resolve(const std::string&
field_name,
}
if (!reader_holder) {
+ if (_context->stats) {
+ _context->stats->inverted_index_searcher_cache_miss++;
+ }
// Cache miss: open directory, build IndexSearcher, insert into cache
+ int64_t dummy_timer = 0;
+ SCOPED_RAW_TIMER(_context->stats ?
&_context->stats->inverted_index_searcher_open_timer
+ : &dummy_timer);
RETURN_IF_ERROR(
index_file_reader->init(config::inverted_index_read_buffer_size,
_context->io_ctx));
auto directory = DORIS_TRY(
@@ -297,6 +321,26 @@ Status FieldReaderResolver::resolve(const std::string&
field_name,
DORIS_TRY(index_searcher_builder->get_index_searcher(directory.get()));
auto reader_size = index_searcher_builder->get_reader_size();
+ // Initialization reads are done. Clear io_ctx on the main stream so
the
+ // cached searcher does not carry a stale reference. Subsequent
query-phase
+ // reads receive the caller's io_ctx through the CLucene API parameters
+ // (termDocs/termPositions/terms) — the same pattern used by the MATCH
path
+ // in InvertedIndexReader::create_index_searcher().
+ auto* stream =
static_cast<DorisCompoundReader*>(directory.get())->getDorisIndexInput();
+ DBUG_EXECUTE_IF(
+ "FieldReaderResolver.resolve.io_ctx", ({
+ const auto* cur_io_ctx = (const
io::IOContext*)stream->getIoContext();
+ if (cur_io_ctx->file_cache_stats) {
+ if (cur_io_ctx->file_cache_stats !=
&_context->stats->file_cache_stats) {
+ LOG(FATAL) << "search: io_ctx file_cache_stats
mismatch: "
+ << cur_io_ctx->file_cache_stats << " vs
"
+ << &_context->stats->file_cache_stats;
+ }
+ }
+ }));
+ stream->setIoContext(nullptr);
+ stream->setIndexFile(false);
+
auto* cache_value = new
InvertedIndexSearcherCache::CacheValue(std::move(searcher_result),
reader_size, UnixMillis());
InvertedIndexSearcherCache::instance()->insert(searcher_cache_key,
cache_value,
@@ -395,6 +439,13 @@ Status
FunctionSearch::evaluate_inverted_index_with_search_param(
return Status::OK();
}
+ // Track overall query time (equivalent to inverted_index_query_timer in
MATCH path).
+ // Must be declared before the DSL cache lookup so that cache-hit fast
paths are
+ // also covered by the timer.
+ int64_t query_timer_dummy = 0;
+ OlapReaderStatistics* outer_stats = index_query_context ?
index_query_context->stats : nullptr;
+ SCOPED_RAW_TIMER(outer_stats ? &outer_stats->inverted_index_query_timer :
&query_timer_dummy);
+
// DSL result cache: reuse InvertedIndexQueryCache with SEARCH_DSL_QUERY
type
auto* dsl_cache = enable_cache ? InvertedIndexQueryCache::instance() :
nullptr;
std::string seg_prefix;
@@ -410,9 +461,19 @@ Status
FunctionSearch::evaluate_inverted_index_with_search_param(
dsl_sig};
cache_usable = true;
InvertedIndexQueryCacheHandle dsl_cache_handle;
- if (dsl_cache->lookup(dsl_cache_key, &dsl_cache_handle)) {
+ bool dsl_hit = false;
+ {
+ int64_t lookup_dummy = 0;
+ SCOPED_RAW_TIMER(outer_stats ?
&outer_stats->inverted_index_lookup_timer
+ : &lookup_dummy);
+ dsl_hit = dsl_cache->lookup(dsl_cache_key, &dsl_cache_handle);
+ }
+ if (dsl_hit) {
auto cached_bitmap = dsl_cache_handle.get_bitmap();
if (cached_bitmap) {
+ if (outer_stats) {
+ outer_stats->inverted_index_query_cache_hit++;
+ }
// 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 {
@@ -431,6 +492,9 @@ Status
FunctionSearch::evaluate_inverted_index_with_search_param(
return Status::OK();
}
}
+ if (outer_stats) {
+ outer_stats->inverted_index_query_cache_miss++;
+ }
}
}
@@ -542,11 +606,19 @@ Status
FunctionSearch::evaluate_inverted_index_with_search_param(
minimum_should_match = search_param.minimum_should_match;
}
+ auto* stats = context->stats;
+ int64_t dummy_timer = 0;
+ SCOPED_RAW_TIMER(stats ? &stats->inverted_index_searcher_search_timer :
&dummy_timer);
+
query_v2::QueryPtr root_query;
std::string root_binding_key;
- RETURN_IF_ERROR(build_query_recursive(search_param.root, context,
resolver, &root_query,
- &root_binding_key, default_operator,
- minimum_should_match));
+ {
+ int64_t init_dummy = 0;
+ SCOPED_RAW_TIMER(stats ?
&stats->inverted_index_searcher_search_init_timer : &init_dummy);
+ RETURN_IF_ERROR(build_query_recursive(search_param.root, context,
resolver, &root_query,
+ &root_binding_key,
default_operator,
+ minimum_should_match));
+ }
if (root_query == nullptr) {
LOG(INFO) << "search: Query tree resolved to empty query, dsl:"
<< search_param.original_dsl;
@@ -577,17 +649,22 @@ Status
FunctionSearch::evaluate_inverted_index_with_search_param(
}
std::shared_ptr<roaring::Roaring> roaring =
std::make_shared<roaring::Roaring>();
- if (enable_scoring && !is_asc && top_k > 0) {
- bool use_wand = index_query_context->runtime_state != nullptr &&
- index_query_context->runtime_state->query_options()
- .enable_inverted_index_wand_query;
- query_v2::collect_multi_segment_top_k(weight, exec_ctx,
root_binding_key, top_k, roaring,
-
index_query_context->collection_similarity, use_wand);
- } else {
- query_v2::collect_multi_segment_doc_set(
- weight, exec_ctx, root_binding_key, roaring,
- index_query_context ?
index_query_context->collection_similarity : nullptr,
- enable_scoring);
+ {
+ int64_t exec_dummy = 0;
+ SCOPED_RAW_TIMER(stats ?
&stats->inverted_index_searcher_search_exec_timer : &exec_dummy);
+ if (enable_scoring && !is_asc && top_k > 0) {
+ bool use_wand = index_query_context->runtime_state != nullptr &&
+ index_query_context->runtime_state->query_options()
+ .enable_inverted_index_wand_query;
+ query_v2::collect_multi_segment_top_k(
+ weight, exec_ctx, root_binding_key, top_k, roaring,
+ index_query_context->collection_similarity, use_wand);
+ } else {
+ query_v2::collect_multi_segment_doc_set(
+ weight, exec_ctx, root_binding_key, roaring,
+ index_query_context ?
index_query_context->collection_similarity : nullptr,
+ enable_scoring);
+ }
}
VLOG_DEBUG << "search: Query completed, matched " <<
roaring->cardinality() << " documents";
diff --git a/be/src/storage/index/inverted/inverted_index_compound_reader.cpp
b/be/src/storage/index/inverted/inverted_index_compound_reader.cpp
index b6d379c4133..6768aced7de 100644
--- a/be/src/storage/index/inverted/inverted_index_compound_reader.cpp
+++ b/be/src/storage/index/inverted/inverted_index_compound_reader.cpp
@@ -412,4 +412,4 @@ void DorisCompoundReader::initialize(const io::IOContext*
io_ctx) {
}
} // namespace doris::segment_v2
-#include "common/compile_check_end.h"
\ No newline at end of file
+#include "common/compile_check_end.h"
diff --git a/be/test/storage/segment/inverted_index_compound_reader_test.cpp
b/be/test/storage/segment/inverted_index_compound_reader_test.cpp
index 2fd921b2c9e..eaef09a6ba2 100644
--- a/be/test/storage/segment/inverted_index_compound_reader_test.cpp
+++ b/be/test/storage/segment/inverted_index_compound_reader_test.cpp
@@ -35,6 +35,7 @@
#include "gtest/gtest_pred_impl.h"
#include "io/fs/local_file_system.h"
+#include "io/io_common.h"
#include "runtime/exec_env.h"
#include "storage/index/index_file_reader.h"
#include "storage/index/index_file_writer.h"
@@ -999,4 +1000,60 @@ TEST_F(DorisCompoundReaderTest,
OpenInputFromRAMDirectory) {
reader.close();
}
+// Verify that after initialize() + setIoContext(nullptr) (the cache-safe
+// transition), the main stream's io_ctx is cleared and sub-streams created
+// via openInput() do NOT carry stale io_ctx pointers. This is the pattern
+// used by both MATCH and SEARCH paths before inserting an IndexSearcher
+// into the global InvertedIndexSearcherCache.
+TEST_F(DorisCompoundReaderTest, InitializeThenClearIoCtxForCacheSafety) {
+ std::string index_path = kTestDir + "/test_io_ctx_lifecycle.idx";
+ std::vector<std::string> file_names = {"segments.gen", "posting.dat"};
+ std::vector<int64_t> lengths = {30, 60};
+
+ CL_NS(store)::IndexInput* index_input =
+ create_mock_index_input(index_path, file_names, lengths);
+
+ DorisCompoundReader reader(index_input, 4096, nullptr);
+
+ // --- Phase 1: initialization (simulates index open with a query's
io_ctx) ---
+ io::IOContext io_ctx;
+ io_ctx.reader_type = ReaderType::READER_QUERY;
+ reader.initialize(&io_ctx);
+
+ auto* stream = reader.getDorisIndexInput();
+ auto* ctx_after_init = (const io::IOContext*)stream->getIoContext();
+ // Main stream should carry the io_ctx during initialization reads
+ EXPECT_EQ(ctx_after_init->reader_type, ReaderType::READER_QUERY);
+
+ // --- Phase 2: cache-safe transition (mirrors create_index_searcher) ---
+ stream->setIoContext(nullptr);
+ stream->setIndexFile(false);
+
+ auto* ctx_after_clear = (const io::IOContext*)stream->getIoContext();
+ // Main stream io_ctx should be reset to defaults
+ EXPECT_EQ(ctx_after_clear->reader_type, ReaderType::UNKNOWN);
+ EXPECT_EQ(ctx_after_clear->file_cache_stats, nullptr);
+
+ // --- Phase 3: verify sub-streams don't inherit stale io_ctx ---
+ CLuceneError err;
+ lucene::store::IndexInput* sub_input = nullptr;
+ EXPECT_TRUE(reader.openInput("posting.dat", sub_input, err, 4096));
+ EXPECT_NE(sub_input, nullptr);
+
+ // CSIndexInput._io_ctx should be nullptr (not set by openInput on master)
+ // This means readInternal() will skip setIoContext on the base stream,
+ // allowing query-phase reads to use their own io_ctx through the
+ // CLucene API parameter chain (termDocs/termPositions/terms).
+ // We verify this indirectly: cloning should also have nullptr _io_ctx.
+ lucene::store::IndexInput* cloned = sub_input->clone();
+ EXPECT_NE(cloned, nullptr);
+
+ sub_input->close();
+ cloned->close();
+ _CLLDELETE(sub_input);
+ _CLLDELETE(cloned);
+
+ reader.close();
+}
+
} // namespace doris::segment_v2
\ No newline at end of file
diff --git a/regression-test/data/search/test_search_inverted_index_profile.out
b/regression-test/data/search/test_search_inverted_index_profile.out
new file mode 100644
index 00000000000..bf3abe806cb
--- /dev/null
+++ b/regression-test/data/search/test_search_inverted_index_profile.out
@@ -0,0 +1,22 @@
+-- This file is automatically generated. You may need to update it when you get
+-- temporary file
'regression-test/data/search/test_search_inverted_index_profile.out'.
+-- !io_ctx_miss --
+2
+5
+7
+
+-- !io_ctx_hit --
+2
+5
+7
+
+-- !io_ctx_multi --
+1
+2
+3
+4
+5
+6
+7
+8
+
diff --git
a/regression-test/suites/search/test_search_inverted_index_profile.groovy
b/regression-test/suites/search/test_search_inverted_index_profile.groovy
new file mode 100644
index 00000000000..6e0b4c92a5e
--- /dev/null
+++ b/regression-test/suites/search/test_search_inverted_index_profile.groovy
@@ -0,0 +1,302 @@
+// 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.
+
+import java.util.regex.Pattern
+
+suite("test_search_inverted_index_profile", "nonConcurrent") {
+ def tableName = "test_search_ii_profile"
+
+ sql "DROP TABLE IF EXISTS ${tableName}"
+ sql """
+ CREATE TABLE ${tableName} (
+ id INT,
+ title VARCHAR(200),
+ content TEXT,
+ 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",
+ "disable_auto_compaction" = "true"
+ )
+ """
+
+ sql """INSERT INTO ${tableName} VALUES
+ (1, 'apple banana cherry', 'red fruit sweet delicious'),
+ (2, 'banana grape mango', 'yellow fruit tropical summer'),
+ (3, 'cherry plum peach', 'stone fruit summer garden'),
+ (4, 'apple grape kiwi', 'green fruit fresh morning'),
+ (5, 'mango pineapple coconut', 'tropical fruit exotic island'),
+ (6, 'apple cherry plum', 'mixed fruit salad party'),
+ (7, 'banana coconut papaya', 'smoothie blend tropical drink'),
+ (8, 'grape cherry apple', 'wine fruit tart autumn')
+ """
+ sql "sync"
+
+ // Helper: extract a numeric counter value from profile string
+ def extractCounter = { String profileStr, String counterName ->
+ // Profile HTML uses for spacing
+ def pattern = Pattern.compile("${counterName}:(?: |\\s)*(\\d+)")
+ def matcher = pattern.matcher(profileStr)
+ if (matcher.find()) {
+ return Long.parseLong(matcher.group(1))
+ }
+ return -1L
+ }
+
+ //
=========================================================================
+ // Test 1: SEARCH() profile metrics are populated on cache miss
+ //
+ // Disable both caches so every query triggers a full index open + search.
+ // Verify that InvertedIndexQueryTime, InvertedIndexSearcherOpenTime,
+ // InvertedIndexSearcherSearchTime, and cache miss counter are all > 0.
+ //
=========================================================================
+ sql """ set enable_profile = true """
+ sql """ set profile_level = 2 """
+ sql """ set enable_sql_cache = false """
+ sql """ set enable_inverted_index_searcher_cache = false """
+ sql """ set enable_inverted_index_query_cache = false """
+ sql """ set enable_common_expr_pushdown = true """
+ sql """ set enable_common_expr_pushdown_for_inverted_index = true """
+
+ def queryId1 = "search_profile_miss_${System.currentTimeMillis()}"
+ try {
+ profile("${queryId1}") {
+ run {
+ sql """/* ${queryId1} */ SELECT id FROM ${tableName}
+ WHERE search('title:apple') ORDER BY id"""
+ }
+ check { profileString, exception ->
+ if (exception != null) throw exception
+ log.info("=== Cache-miss profile ===")
+
+ def queryTime = extractCounter(profileString,
"InvertedIndexQueryTime")
+ def openTime = extractCounter(profileString,
"InvertedIndexSearcherOpenTime")
+ def searchTime = extractCounter(profileString,
"InvertedIndexSearcherSearchTime")
+ def searchInitTime = extractCounter(profileString,
"InvertedIndexSearcherSearchInitTime")
+ def searchExecTime = extractCounter(profileString,
"InvertedIndexSearcherSearchExecTime")
+ def cacheMiss = extractCounter(profileString,
"InvertedIndexSearcherCacheMiss")
+ def cacheHit = extractCounter(profileString,
"InvertedIndexSearcherCacheHit")
+
+ log.info("InvertedIndexQueryTime: {}", queryTime)
+ log.info("InvertedIndexSearcherOpenTime: {}", openTime)
+ log.info("InvertedIndexSearcherSearchTime: {}", searchTime)
+ log.info("InvertedIndexSearcherSearchInitTime: {}",
searchInitTime)
+ log.info("InvertedIndexSearcherSearchExecTime: {}",
searchExecTime)
+ log.info("InvertedIndexSearcherCacheMiss: {}", cacheMiss)
+ log.info("InvertedIndexSearcherCacheHit: {}", cacheHit)
+
+ assertTrue(queryTime > 0,
+ "InvertedIndexQueryTime should be > 0 for SEARCH(), got
${queryTime}")
+ assertTrue(searchTime > 0,
+ "InvertedIndexSearcherSearchTime should be > 0, got
${searchTime}")
+ assertTrue(cacheMiss > 0,
+ "InvertedIndexSearcherCacheMiss should be > 0 (cache
disabled), got ${cacheMiss}")
+ }
+ }
+ } catch (IllegalStateException e) {
+ if (e.message?.contains("HttpCliAction failed")) {
+ log.warn("Profile HTTP request failed, skipping: {}", e.message)
+ } else {
+ throw e
+ }
+ }
+
+ //
=========================================================================
+ // Test 2: SEARCH() cache hit — re-enable searcher cache, run twice.
+ // First run is a cache miss (populates cache), second run is a cache hit.
+ // Verify the second profile shows cache hit > 0 and open time == 0.
+ //
=========================================================================
+ sql """ set enable_inverted_index_searcher_cache = true """
+ sql """ set enable_inverted_index_query_cache = false """
+
+ // First run: populate searcher cache
+ sql """SELECT /*+SET_VAR(enable_common_expr_pushdown=true) */
+ id FROM ${tableName} WHERE search('title:cherry') ORDER BY id"""
+
+ // Second run: should hit searcher cache
+ def queryId2 = "search_profile_hit_${System.currentTimeMillis()}"
+ try {
+ profile("${queryId2}") {
+ run {
+ sql """/* ${queryId2} */ SELECT id FROM ${tableName}
+ WHERE search('title:cherry') ORDER BY id"""
+ }
+ check { profileString, exception ->
+ if (exception != null) throw exception
+ log.info("=== Cache-hit profile ===")
+
+ def cacheHit = extractCounter(profileString,
"InvertedIndexSearcherCacheHit")
+ def cacheMiss = extractCounter(profileString,
"InvertedIndexSearcherCacheMiss")
+ def openTime = extractCounter(profileString,
"InvertedIndexSearcherOpenTime")
+ def searchTime = extractCounter(profileString,
"InvertedIndexSearcherSearchTime")
+
+ log.info("InvertedIndexSearcherCacheHit: {}", cacheHit)
+ log.info("InvertedIndexSearcherCacheMiss: {}", cacheMiss)
+ log.info("InvertedIndexSearcherOpenTime: {}", openTime)
+ log.info("InvertedIndexSearcherSearchTime: {}", searchTime)
+
+ assertTrue(cacheHit > 0,
+ "InvertedIndexSearcherCacheHit should be > 0 on second
run, got ${cacheHit}")
+ assertTrue(searchTime > 0,
+ "InvertedIndexSearcherSearchTime should still be > 0 on
cache hit, got ${searchTime}")
+ }
+ }
+ } catch (IllegalStateException e) {
+ if (e.message?.contains("HttpCliAction failed")) {
+ log.warn("Profile HTTP request failed, skipping: {}", e.message)
+ } else {
+ throw e
+ }
+ }
+
+ //
=========================================================================
+ // Test 3: io_ctx safety — debug point verifies no stale io_ctx on the
+ // main stream after IndexSearcher creation (cache miss path).
+ // Then run a second query that hits the cache and confirm correct results.
+ // If the old dangling-pointer bug existed, ASAN would catch UAF here.
+ //
=========================================================================
+ sql """ set enable_inverted_index_searcher_cache = true """
+ sql """ set enable_inverted_index_query_cache = false """
+
+ try {
+
GetDebugPoint().enableDebugPointForAllBEs("FieldReaderResolver.resolve.io_ctx")
+
+ // First query: cache miss, debug point validates io_ctx consistency
+ qt_io_ctx_miss """ SELECT /*+SET_VAR(enable_common_expr_pushdown=true)
*/
+ id FROM ${tableName} WHERE search('content:tropical') ORDER BY id
"""
+
+ // Second query: cache hit, reuses the cached searcher
+ // If io_ctx was stale, this would crash under ASAN
+ qt_io_ctx_hit """ SELECT /*+SET_VAR(enable_common_expr_pushdown=true)
*/
+ id FROM ${tableName} WHERE search('content:tropical') ORDER BY id
"""
+
+ // Third query: different DSL but same field — exercises resolver cache
+ qt_io_ctx_multi """ SELECT
/*+SET_VAR(enable_common_expr_pushdown=true) */
+ id FROM ${tableName} WHERE search('content:tropical OR
content:fruit') ORDER BY id """
+ } finally {
+
GetDebugPoint().disableDebugPointForAllBEs("FieldReaderResolver.resolve.io_ctx")
+ }
+
+ //
=========================================================================
+ // Test 4: DSL query-cache hit — enable query cache, run same DSL twice.
+ // First run populates the DSL cache (miss), second run hits it.
+ // Verify InvertedIndexQueryCacheHit > 0, InvertedIndexQueryTime > 0,
+ // and InvertedIndexLookupTime > 0 on the cache-hit path.
+ //
=========================================================================
+ sql """ set enable_inverted_index_searcher_cache = true """
+ sql """ set enable_inverted_index_query_cache = true """
+
+ // First run: populate DSL cache
+ sql """SELECT /*+SET_VAR(enable_common_expr_pushdown=true) */
+ id FROM ${tableName} WHERE search('title:banana') ORDER BY id"""
+
+ // Second run: should hit DSL cache
+ def queryId4 = "search_profile_dsl_hit_${System.currentTimeMillis()}"
+ try {
+ profile("${queryId4}") {
+ run {
+ sql """/* ${queryId4} */ SELECT id FROM ${tableName}
+ WHERE search('title:banana') ORDER BY id"""
+ }
+ check { profileString, exception ->
+ if (exception != null) throw exception
+ log.info("=== DSL cache-hit profile ===")
+
+ def queryCacheHit = extractCounter(profileString,
"InvertedIndexQueryCacheHit")
+ def queryTime = extractCounter(profileString,
"InvertedIndexQueryTime")
+ def lookupTime = extractCounter(profileString,
"InvertedIndexLookupTime")
+
+ log.info("InvertedIndexQueryCacheHit: {}", queryCacheHit)
+ log.info("InvertedIndexQueryTime: {}", queryTime)
+ log.info("InvertedIndexLookupTime: {}", lookupTime)
+
+ assertTrue(queryCacheHit > 0,
+ "InvertedIndexQueryCacheHit should be > 0 on DSL cache
hit, got ${queryCacheHit}")
+ assertTrue(queryTime > 0,
+ "InvertedIndexQueryTime should be > 0 even on DSL cache
hit, got ${queryTime}")
+ }
+ }
+ } catch (IllegalStateException e) {
+ if (e.message?.contains("HttpCliAction failed")) {
+ log.warn("Profile HTTP request failed, skipping: {}", e.message)
+ } else {
+ throw e
+ }
+ }
+
+ //
=========================================================================
+ // Test 5: searcher cache disabled — run same DSL twice with searcher cache
+ // off (but query cache also off so DSL cache cannot mask the problem).
+ // Both runs must show cache miss and zero cache hits, proving the switch
+ // is actually respected and a second run does not silently hit the cache.
+ //
=========================================================================
+ sql """ set enable_inverted_index_searcher_cache = false """
+ sql """ set enable_inverted_index_query_cache = false """
+
+ // First run: cache miss (searcher cache disabled, nothing to hit)
+ sql """SELECT /*+SET_VAR(enable_common_expr_pushdown=true) */
+ id FROM ${tableName} WHERE search('title:grape') ORDER BY id"""
+
+ // Second run: should STILL be a cache miss because searcher cache is
disabled
+ def queryId5 = "search_profile_no_cache_${System.currentTimeMillis()}"
+ try {
+ profile("${queryId5}") {
+ run {
+ sql """/* ${queryId5} */ SELECT id FROM ${tableName}
+ WHERE search('title:grape') ORDER BY id"""
+ }
+ check { profileString, exception ->
+ if (exception != null) throw exception
+ log.info("=== Searcher cache disabled (2nd run) profile ===")
+
+ def cacheHit = extractCounter(profileString,
"InvertedIndexSearcherCacheHit")
+ def cacheMiss = extractCounter(profileString,
"InvertedIndexSearcherCacheMiss")
+
+ log.info("InvertedIndexSearcherCacheHit: {}", cacheHit)
+ log.info("InvertedIndexSearcherCacheMiss: {}", cacheMiss)
+
+ assertTrue(cacheHit == 0,
+ "InvertedIndexSearcherCacheHit should be 0 when cache
disabled, got ${cacheHit}")
+ assertTrue(cacheMiss > 0,
+ "InvertedIndexSearcherCacheMiss should be > 0 when cache
disabled, got ${cacheMiss}")
+ }
+ }
+ } catch (IllegalStateException e) {
+ if (e.message?.contains("HttpCliAction failed")) {
+ log.warn("Profile HTTP request failed, skipping: {}", e.message)
+ } else {
+ throw e
+ }
+ }
+
+ //
=========================================================================
+ // Test 6: SEARCH vs MATCH consistency — same data, same predicate logic,
+ // both should return identical results.
+ //
=========================================================================
+ def search_result = sql """
+ SELECT /*+SET_VAR(enable_common_expr_pushdown=true) */
+ id FROM ${tableName} WHERE search('title:apple') ORDER BY id
+ """
+ def match_result = sql """
+ SELECT id FROM ${tableName} WHERE title MATCH_ANY 'apple' ORDER BY id
+ """
+ assertEquals(search_result, match_result,
+ "SEARCH('title:apple') and MATCH_ANY 'apple' should return identical
rows")
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]