This is an automated email from the ASF dual-hosted git repository.
airborne12 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 a958942a3d9 [feature](ann-index) Add ann topn small candidate fallback
session var. (#64243)
a958942a3d9 is described below
commit a958942a3d9b15b6a15d86e5511d2ebefc26ad7e
Author: Qi Chen <[email protected]>
AuthorDate: Tue Jun 16 13:34:03 2026 +0800
[feature](ann-index) Add ann topn small candidate fallback session var.
(#64243)
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary:
When ANN TopN or ANN range search already has a small candidate set
after previous filtering, continuing to load and search the ANN index
can be unnecessary and may cost more than exact vector
evaluation.
This PR adds session variables to control small-candidate fallback for
ANN search:
- `ann_index_candidate_rows_threshold`: absolute candidate row
threshold, `0` disables it
- `ann_index_candidate_rows_percent_threshold`: candidate ratio
threshold against segment rows
For ANN TopN, the previous fixed `30%` fallback rule is replaced by the
shared threshold helper. For ANN range search, the same helper is
applied before ANN index loading/searching. When fallback is
triggered, Doris skips ANN index search and continues with exact vector
distance evaluation.
This PR also adds profile counters for observability:
- `AnnIndexTopNFallbackBySmallCandidateCnt`
- `AnnIndexTopNFallbackSmallCandidateRows`
- `AnnIndexRangeFallbackBySmallCandidateCnt`
- `AnnIndexRangeFallbackSmallCandidateRows`
### Release note
ANN TopN and ANN range search can now skip ANN index search when
candidate rows are below configurable small-candidate thresholds.
---
be/src/exec/operator/olap_scan_operator.cpp | 8 +
be/src/exec/operator/olap_scan_operator.h | 4 +
be/src/exec/scan/olap_scanner.cpp | 8 +
be/src/exec/scan/vector_search_user_params.cpp | 24 ++-
be/src/exec/scan/vector_search_user_params.h | 7 +
be/src/exprs/vectorized_fn_call.cpp | 19 +-
be/src/exprs/vectorized_fn_call.h | 5 +-
be/src/exprs/vexpr.cpp | 4 +-
be/src/exprs/vexpr.h | 5 +-
be/src/exprs/vexpr_context.cpp | 7 +-
be/src/exprs/vexpr_context.h | 5 +-
be/src/exprs/virtual_slot_ref.cpp | 9 +-
be/src/exprs/virtual_slot_ref.h | 5 +-
be/src/runtime/runtime_state.h | 8 +
be/src/storage/index/ann/ann_search_params.h | 12 +-
be/src/storage/index/ann/ann_topn_runtime.h | 2 +
be/src/storage/olap_common.h | 4 +
be/src/storage/segment/segment_iterator.cpp | 20 +-
.../storage/index/ann/ann_index_edge_case_test.cpp | 57 +++++
.../storage/index/ann/ann_range_search_test.cpp | 35 ++--
.../java/org/apache/doris/qe/SessionVariable.java | 38 ++++
gensrc/thrift/PaloInternalService.thrift | 5 +
.../ann_topn_small_candidate_fallback.groovy | 231 +++++++++++++++++++++
23 files changed, 475 insertions(+), 47 deletions(-)
diff --git a/be/src/exec/operator/olap_scan_operator.cpp
b/be/src/exec/operator/olap_scan_operator.cpp
index 92dfbaaeae0..4d401a78100 100644
--- a/be/src/exec/operator/olap_scan_operator.cpp
+++ b/be/src/exec/operator/olap_scan_operator.cpp
@@ -395,6 +395,14 @@ Status OlapScanLocalState::_init_profile() {
"AnnIndexRangeResultPostProcessCosts");
_ann_fallback_brute_force_cnt =
ADD_COUNTER(_segment_profile, "AnnIndexFallbackBruteForceCnt",
TUnit::UNIT);
+ _ann_topn_fallback_by_small_candidate_cnt =
+ ADD_COUNTER(_segment_profile,
"AnnIndexTopNFallbackBySmallCandidateCnt", TUnit::UNIT);
+ _ann_topn_fallback_small_candidate_rows =
+ ADD_COUNTER(_segment_profile,
"AnnIndexTopNFallbackSmallCandidateRows", TUnit::UNIT);
+ _ann_range_fallback_by_small_candidate_cnt =
+ ADD_COUNTER(_segment_profile,
"AnnIndexRangeFallbackBySmallCandidateCnt", TUnit::UNIT);
+ _ann_range_fallback_small_candidate_rows =
+ ADD_COUNTER(_segment_profile,
"AnnIndexRangeFallbackSmallCandidateRows", TUnit::UNIT);
_variant_scan_sparse_column_timer = ADD_TIMER(_segment_profile,
"VariantScanSparseColumnTimer");
_variant_scan_sparse_column_bytes =
ADD_COUNTER(_segment_profile, "VariantScanSparseColumnBytes",
TUnit::BYTES);
diff --git a/be/src/exec/operator/olap_scan_operator.h
b/be/src/exec/operator/olap_scan_operator.h
index d22aac75052..344dd604db1 100644
--- a/be/src/exec/operator/olap_scan_operator.h
+++ b/be/src/exec/operator/olap_scan_operator.h
@@ -264,6 +264,10 @@ private:
RuntimeProfile::Counter* _ann_range_result_convert_costs = nullptr;
RuntimeProfile::Counter* _ann_fallback_brute_force_cnt = nullptr;
+ RuntimeProfile::Counter* _ann_topn_fallback_by_small_candidate_cnt =
nullptr;
+ RuntimeProfile::Counter* _ann_topn_fallback_small_candidate_rows = nullptr;
+ RuntimeProfile::Counter* _ann_range_fallback_by_small_candidate_cnt =
nullptr;
+ RuntimeProfile::Counter* _ann_range_fallback_small_candidate_rows =
nullptr;
RuntimeProfile::Counter* _output_index_result_column_timer = nullptr;
diff --git a/be/src/exec/scan/olap_scanner.cpp
b/be/src/exec/scan/olap_scanner.cpp
index c90b8897a86..8da4483ea3a 100644
--- a/be/src/exec/scan/olap_scanner.cpp
+++ b/be/src/exec/scan/olap_scanner.cpp
@@ -1109,6 +1109,14 @@ void OlapScanner::_collect_profile_before_close() {
stats.ann_index_topn_result_process_ns);
COUNTER_UPDATE(local_state->_ann_fallback_brute_force_cnt,
stats.ann_fall_back_brute_force_cnt);
+ COUNTER_UPDATE(local_state->_ann_topn_fallback_by_small_candidate_cnt,
+ stats.ann_topn_fallback_by_small_candidate_cnt);
+ COUNTER_UPDATE(local_state->_ann_topn_fallback_small_candidate_rows,
+ stats.ann_topn_fallback_small_candidate_rows);
+ COUNTER_UPDATE(local_state->_ann_range_fallback_by_small_candidate_cnt,
+ stats.ann_range_fallback_by_small_candidate_cnt);
+ COUNTER_UPDATE(local_state->_ann_range_fallback_small_candidate_rows,
+ stats.ann_range_fallback_small_candidate_rows);
// Overhead counter removed; precise instrumentation is reported via
engine_prepare above.
}
diff --git a/be/src/exec/scan/vector_search_user_params.cpp
b/be/src/exec/scan/vector_search_user_params.cpp
index 6eeefc1ef50..84146fb008b 100644
--- a/be/src/exec/scan/vector_search_user_params.cpp
+++ b/be/src/exec/scan/vector_search_user_params.cpp
@@ -23,13 +23,31 @@ namespace doris {
bool VectorSearchUserParams::operator==(const VectorSearchUserParams& other)
const {
return hnsw_ef_search == other.hnsw_ef_search &&
hnsw_check_relative_distance == other.hnsw_check_relative_distance
&&
- hnsw_bounded_queue == other.hnsw_bounded_queue && ivf_nprobe ==
other.ivf_nprobe;
+ hnsw_bounded_queue == other.hnsw_bounded_queue && ivf_nprobe ==
other.ivf_nprobe &&
+ ann_index_candidate_rows_threshold ==
other.ann_index_candidate_rows_threshold &&
+ ann_index_candidate_rows_percent_threshold ==
+ other.ann_index_candidate_rows_percent_threshold;
+}
+
+bool VectorSearchUserParams::should_fallback_ann_index_by_small_candidate(
+ size_t candidate_rows, size_t rows_of_segment) const {
+ bool reach_absolute_threshold =
+ ann_index_candidate_rows_threshold > 0 &&
+ candidate_rows <
static_cast<size_t>(ann_index_candidate_rows_threshold);
+ bool reach_percent_threshold = ann_index_candidate_rows_percent_threshold
> 0 &&
+ static_cast<double>(candidate_rows) <
+
static_cast<double>(rows_of_segment) *
+
ann_index_candidate_rows_percent_threshold;
+ return reach_absolute_threshold || reach_percent_threshold;
}
std::string VectorSearchUserParams::to_string() const {
return fmt::format(
"hnsw_ef_search: {}, hnsw_check_relative_distance: {}, "
- "hnsw_bounded_queue: {}, ivf_nprobe: {}",
- hnsw_ef_search, hnsw_check_relative_distance, hnsw_bounded_queue,
ivf_nprobe);
+ "hnsw_bounded_queue: {}, ivf_nprobe: {}, "
+ "ann_index_candidate_rows_threshold: {}, "
+ "ann_index_candidate_rows_percent_threshold: {}",
+ hnsw_ef_search, hnsw_check_relative_distance, hnsw_bounded_queue,
ivf_nprobe,
+ ann_index_candidate_rows_threshold,
ann_index_candidate_rows_percent_threshold);
}
} // namespace doris
diff --git a/be/src/exec/scan/vector_search_user_params.h
b/be/src/exec/scan/vector_search_user_params.h
index e97033ec41c..3bfa8d6e8d6 100644
--- a/be/src/exec/scan/vector_search_user_params.h
+++ b/be/src/exec/scan/vector_search_user_params.h
@@ -17,6 +17,8 @@
#pragma once
+#include <cstddef>
+#include <cstdint>
#include <string>
namespace doris {
@@ -26,9 +28,14 @@ struct VectorSearchUserParams {
bool hnsw_check_relative_distance = true;
bool hnsw_bounded_queue = true;
int ivf_nprobe = 32;
+ int64_t ann_index_candidate_rows_threshold = 0;
+ double ann_index_candidate_rows_percent_threshold = 0.3;
bool operator==(const VectorSearchUserParams& other) const;
+ bool should_fallback_ann_index_by_small_candidate(size_t candidate_rows,
+ size_t rows_of_segment)
const;
+
std::string to_string() const;
};
} // namespace doris
diff --git a/be/src/exprs/vectorized_fn_call.cpp
b/be/src/exprs/vectorized_fn_call.cpp
index d6858c612d0..b5ac9cf0c76 100644
--- a/be/src/exprs/vectorized_fn_call.cpp
+++ b/be/src/exprs/vectorized_fn_call.cpp
@@ -556,8 +556,9 @@ Status VectorizedFnCall::evaluate_ann_range_search(
const std::vector<std::unique_ptr<segment_v2::IndexIterator>>&
cid_to_index_iterators,
const std::vector<ColumnId>& idx_to_cid,
const std::vector<std::unique_ptr<segment_v2::ColumnIterator>>&
column_iterators,
- roaring::Roaring& row_bitmap, segment_v2::AnnIndexStats&
ann_index_stats,
- bool enable_result_cache, AnnRangeSearchEvaluationResult&
evaluation_result) {
+ size_t rows_of_segment, roaring::Roaring& row_bitmap,
+ segment_v2::AnnIndexStats& ann_index_stats, bool enable_result_cache,
+ AnnRangeSearchEvaluationResult& evaluation_result) {
evaluation_result = {};
if (range_search_runtime.is_ann_range_search == false) {
return Status::OK();
@@ -611,6 +612,20 @@ Status VectorizedFnCall::evaluate_ann_range_search(
range_search_runtime.dim, index_dim);
}
+ const auto& user_params = range_search_runtime.user_params;
+ if (user_params.should_fallback_ann_index_by_small_candidate(origin_num,
rows_of_segment)) {
+ VLOG_DEBUG << fmt::format(
+ "Ann range search input rows {} reach small candidate
threshold, "
+ "rows_of_segment: {}, absolute_threshold: {},
percent_threshold: {}, "
+ "will not use ann index to filter",
+ origin_num, rows_of_segment,
user_params.ann_index_candidate_rows_threshold,
+ user_params.ann_index_candidate_rows_percent_threshold);
+ ann_index_stats.fall_back_brute_force_cnt += 1;
+ ann_index_stats.range_fallback_by_small_candidate_cnt += 1;
+ ann_index_stats.range_fallback_small_candidate_rows += origin_num;
+ return Status::OK();
+ }
+
auto stats = std::make_unique<segment_v2::AnnIndexStats>();
// Track load index timing
{
diff --git a/be/src/exprs/vectorized_fn_call.h
b/be/src/exprs/vectorized_fn_call.h
index 4d8a1011169..ff7b8174e8a 100644
--- a/be/src/exprs/vectorized_fn_call.h
+++ b/be/src/exprs/vectorized_fn_call.h
@@ -91,8 +91,9 @@ public:
const std::vector<std::unique_ptr<segment_v2::IndexIterator>>&
cid_to_index_iterators,
const std::vector<ColumnId>& idx_to_cid,
const std::vector<std::unique_ptr<segment_v2::ColumnIterator>>&
column_iterators,
- roaring::Roaring& row_bitmap, segment_v2::AnnIndexStats&
ann_index_stats,
- bool enable_result_cache, AnnRangeSearchEvaluationResult& result)
override;
+ size_t rows_of_segment, roaring::Roaring& row_bitmap,
+ segment_v2::AnnIndexStats& ann_index_stats, bool
enable_result_cache,
+ AnnRangeSearchEvaluationResult& result) override;
void prepare_ann_range_search(const doris::VectorSearchUserParams& params,
segment_v2::AnnRangeSearchRuntime& runtime,
diff --git a/be/src/exprs/vexpr.cpp b/be/src/exprs/vexpr.cpp
index 65dcd4eb9de..7f6b75ce6dd 100644
--- a/be/src/exprs/vexpr.cpp
+++ b/be/src/exprs/vexpr.cpp
@@ -1042,8 +1042,8 @@ Status VExpr::evaluate_ann_range_search(
const std::vector<std::unique_ptr<segment_v2::IndexIterator>>&
index_iterators,
const std::vector<ColumnId>& idx_to_cid,
const std::vector<std::unique_ptr<segment_v2::ColumnIterator>>&
column_iterators,
- roaring::Roaring& row_bitmap, AnnIndexStats& ann_index_stats, bool
enable_result_cache,
- AnnRangeSearchEvaluationResult& result) {
+ size_t rows_of_segment, roaring::Roaring& row_bitmap, AnnIndexStats&
ann_index_stats,
+ bool enable_result_cache, AnnRangeSearchEvaluationResult& result) {
result = {};
return Status::OK();
}
diff --git a/be/src/exprs/vexpr.h b/be/src/exprs/vexpr.h
index 46ac59ed093..58bb14abe47 100644
--- a/be/src/exprs/vexpr.h
+++ b/be/src/exprs/vexpr.h
@@ -348,8 +348,9 @@ public:
const std::vector<std::unique_ptr<segment_v2::IndexIterator>>&
cid_to_index_iterators,
const std::vector<ColumnId>& idx_to_cid,
const std::vector<std::unique_ptr<segment_v2::ColumnIterator>>&
column_iterators,
- roaring::Roaring& row_bitmap, segment_v2::AnnIndexStats&
ann_index_stats,
- bool enable_result_cache, AnnRangeSearchEvaluationResult& result);
+ size_t rows_of_segment, roaring::Roaring& row_bitmap,
+ segment_v2::AnnIndexStats& ann_index_stats, bool
enable_result_cache,
+ AnnRangeSearchEvaluationResult& result);
// Prepare the runtime for ANN range search.
// AnnRangeSearchRuntime is used to store the runtime information of ann
range search.
diff --git a/be/src/exprs/vexpr_context.cpp b/be/src/exprs/vexpr_context.cpp
index 1bdbff89ae0..b9abce25588 100644
--- a/be/src/exprs/vexpr_context.cpp
+++ b/be/src/exprs/vexpr_context.cpp
@@ -440,8 +440,9 @@ Status VExprContext::evaluate_ann_range_search(
const std::vector<std::unique_ptr<segment_v2::ColumnIterator>>&
column_iterators,
const std::unordered_map<VExprContext*, std::unordered_map<ColumnId,
VExpr*>>&
common_expr_to_slotref_map,
- roaring::Roaring& row_bitmap, segment_v2::AnnIndexStats&
ann_index_stats,
- bool enable_result_cache, bool* ann_range_search_executed) {
+ size_t rows_of_segment, roaring::Roaring& row_bitmap,
+ segment_v2::AnnIndexStats& ann_index_stats, bool enable_result_cache,
+ bool* ann_range_search_executed) {
if (ann_range_search_executed != nullptr) {
*ann_range_search_executed = false;
}
@@ -452,7 +453,7 @@ Status VExprContext::evaluate_ann_range_search(
AnnRangeSearchEvaluationResult evaluation_result;
RETURN_IF_ERROR(_root->evaluate_ann_range_search(
_ann_range_search_runtime, cid_to_index_iterators, idx_to_cid,
column_iterators,
- row_bitmap, ann_index_stats, enable_result_cache,
evaluation_result));
+ rows_of_segment, row_bitmap, ann_index_stats, enable_result_cache,
evaluation_result));
if (!evaluation_result.executed) {
return Status::OK();
diff --git a/be/src/exprs/vexpr_context.h b/be/src/exprs/vexpr_context.h
index 84900c02463..72398c71d05 100644
--- a/be/src/exprs/vexpr_context.h
+++ b/be/src/exprs/vexpr_context.h
@@ -388,8 +388,9 @@ public:
const std::vector<std::unique_ptr<segment_v2::ColumnIterator>>&
column_iterators,
const std::unordered_map<VExprContext*,
std::unordered_map<ColumnId, VExpr*>>&
common_expr_to_slotref_map,
- roaring::Roaring& row_bitmap, segment_v2::AnnIndexStats&
ann_index_stats,
- bool enable_result_cache, bool* ann_range_search_executed);
+ size_t rows_of_segment, roaring::Roaring& row_bitmap,
+ segment_v2::AnnIndexStats& ann_index_stats, bool
enable_result_cache,
+ bool* ann_range_search_executed);
uint64_t get_digest(uint64_t seed) const;
diff --git a/be/src/exprs/virtual_slot_ref.cpp
b/be/src/exprs/virtual_slot_ref.cpp
index 5367df1f31d..f898269efba 100644
--- a/be/src/exprs/virtual_slot_ref.cpp
+++ b/be/src/exprs/virtual_slot_ref.cpp
@@ -234,10 +234,11 @@ Status VirtualSlotRef::evaluate_ann_range_search(
const std::vector<std::unique_ptr<segment_v2::IndexIterator>>&
cid_to_index_iterators,
const std::vector<ColumnId>& idx_to_cid,
const std::vector<std::unique_ptr<segment_v2::ColumnIterator>>&
column_iterators,
- roaring::Roaring& row_bitmap, segment_v2::AnnIndexStats&
ann_index_stats,
- bool enable_result_cache, AnnRangeSearchEvaluationResult& result) {
+ size_t rows_of_segment, roaring::Roaring& row_bitmap,
+ segment_v2::AnnIndexStats& ann_index_stats, bool enable_result_cache,
+ AnnRangeSearchEvaluationResult& result) {
return _virtual_column_expr->evaluate_ann_range_search(
- range_search_runtime, cid_to_index_iterators, idx_to_cid,
column_iterators, row_bitmap,
- ann_index_stats, enable_result_cache, result);
+ range_search_runtime, cid_to_index_iterators, idx_to_cid,
column_iterators,
+ rows_of_segment, row_bitmap, ann_index_stats, enable_result_cache,
result);
}
} // namespace doris
diff --git a/be/src/exprs/virtual_slot_ref.h b/be/src/exprs/virtual_slot_ref.h
index 8f1e14b4b86..ade88a4cd69 100644
--- a/be/src/exprs/virtual_slot_ref.h
+++ b/be/src/exprs/virtual_slot_ref.h
@@ -107,8 +107,9 @@ public:
const std::vector<std::unique_ptr<segment_v2::IndexIterator>>&
cid_to_index_iterators,
const std::vector<ColumnId>& idx_to_cid,
const std::vector<std::unique_ptr<segment_v2::ColumnIterator>>&
column_iterators,
- roaring::Roaring& row_bitmap, segment_v2::AnnIndexStats&
ann_index_stats,
- bool enable_result_cache, AnnRangeSearchEvaluationResult& result)
override;
+ size_t rows_of_segment, roaring::Roaring& row_bitmap,
+ segment_v2::AnnIndexStats& ann_index_stats, bool
enable_result_cache,
+ AnnRangeSearchEvaluationResult& result) override;
#ifdef BE_TEST
// Test-only setter methods for unit testing
diff --git a/be/src/runtime/runtime_state.h b/be/src/runtime/runtime_state.h
index ee1ab4cf882..a01a57d3f7c 100644
--- a/be/src/runtime/runtime_state.h
+++ b/be/src/runtime/runtime_state.h
@@ -842,6 +842,14 @@ public:
params.hnsw_check_relative_distance =
_query_options.hnsw_check_relative_distance;
params.hnsw_bounded_queue = _query_options.hnsw_bounded_queue;
params.ivf_nprobe = _query_options.ivf_nprobe;
+ params.ann_index_candidate_rows_threshold =
+ _query_options.__isset.ann_index_candidate_rows_threshold
+ ? _query_options.ann_index_candidate_rows_threshold
+ : 0;
+ params.ann_index_candidate_rows_percent_threshold =
+
_query_options.__isset.ann_index_candidate_rows_percent_threshold
+ ?
_query_options.ann_index_candidate_rows_percent_threshold
+ : 0.3;
return params;
}
diff --git a/be/src/storage/index/ann/ann_search_params.h
b/be/src/storage/index/ann/ann_search_params.h
index 568657eab4f..c651639bf11 100644
--- a/be/src/storage/index/ann/ann_search_params.h
+++ b/be/src/storage/index/ann/ann_search_params.h
@@ -64,7 +64,9 @@ struct AnnIndexStats {
ivf_on_disk_cache_miss_cnt(TUnit::UNIT, 0),
topn_cache_hits(TUnit::UNIT, 0),
range_cache_hits(TUnit::UNIT, 0),
- fall_back_brute_force_cnt(0) {}
+ fall_back_brute_force_cnt(0),
+ range_fallback_by_small_candidate_cnt(0),
+ range_fallback_small_candidate_rows(0) {}
AnnIndexStats(const AnnIndexStats& other)
: search_costs_ns(TUnit::TIME_NS, other.search_costs_ns.value()),
@@ -81,7 +83,9 @@ struct AnnIndexStats {
ivf_on_disk_cache_miss_cnt(TUnit::UNIT,
other.ivf_on_disk_cache_miss_cnt.value()),
topn_cache_hits(TUnit::UNIT, other.topn_cache_hits.value()),
range_cache_hits(TUnit::UNIT, other.range_cache_hits.value()),
- fall_back_brute_force_cnt(other.fall_back_brute_force_cnt) {}
+ fall_back_brute_force_cnt(other.fall_back_brute_force_cnt),
+
range_fallback_by_small_candidate_cnt(other.range_fallback_by_small_candidate_cnt),
+
range_fallback_small_candidate_rows(other.range_fallback_small_candidate_rows)
{}
AnnIndexStats& operator=(const AnnIndexStats& other) {
if (this != &other) {
@@ -99,6 +103,8 @@ struct AnnIndexStats {
topn_cache_hits.set(other.topn_cache_hits.value());
range_cache_hits.set(other.range_cache_hits.value());
fall_back_brute_force_cnt = other.fall_back_brute_force_cnt;
+ range_fallback_by_small_candidate_cnt =
other.range_fallback_by_small_candidate_cnt;
+ range_fallback_small_candidate_rows =
other.range_fallback_small_candidate_rows;
}
return *this;
}
@@ -118,6 +124,8 @@ struct AnnIndexStats {
RuntimeProfile::Counter topn_cache_hits; // number of cache hits in ANN
TopN result cache
RuntimeProfile::Counter range_cache_hits;
int64_t fall_back_brute_force_cnt; // fallback count when ANN range search
is bypassed
+ int64_t range_fallback_by_small_candidate_cnt;
+ int64_t range_fallback_small_candidate_rows;
};
struct AnnTopNParam {
diff --git a/be/src/storage/index/ann/ann_topn_runtime.h
b/be/src/storage/index/ann/ann_topn_runtime.h
index 49f48fa7388..07bfc21df53 100644
--- a/be/src/storage/index/ann/ann_topn_runtime.h
+++ b/be/src/storage/index/ann/ann_topn_runtime.h
@@ -154,6 +154,8 @@ public:
*/
bool is_asc() const { return _asc; }
+ const doris::VectorSearchUserParams& user_params() const { return
_user_params; }
+
private:
// Core configuration
const bool _asc; ///< Sort order for results
diff --git a/be/src/storage/olap_common.h b/be/src/storage/olap_common.h
index 445d40230e8..2a8893a1b0e 100644
--- a/be/src/storage/olap_common.h
+++ b/be/src/storage/olap_common.h
@@ -416,6 +416,10 @@ struct OlapReaderStatistics {
int64_t rows_ann_index_range_filtered = 0;
int64_t ann_index_range_cache_hits = 0;
int64_t ann_fall_back_brute_force_cnt = 0;
+ int64_t ann_topn_fallback_by_small_candidate_cnt = 0;
+ int64_t ann_topn_fallback_small_candidate_rows = 0;
+ int64_t ann_range_fallback_by_small_candidate_cnt = 0;
+ int64_t ann_range_fallback_small_candidate_rows = 0;
int64_t output_index_result_column_timer = 0;
// number of segment filtered by column stat when creating seg iterator
diff --git a/be/src/storage/segment/segment_iterator.cpp
b/be/src/storage/segment/segment_iterator.cpp
index 19a02ddcbc7..e865828c52b 100644
--- a/be/src/storage/segment/segment_iterator.cpp
+++ b/be/src/storage/segment/segment_iterator.cpp
@@ -1050,15 +1050,19 @@ Status SegmentIterator::_apply_ann_topn_predicate() {
size_t pre_size = _row_bitmap.cardinality();
size_t rows_of_segment = _segment->num_rows();
- if (static_cast<double>(pre_size) < static_cast<double>(rows_of_segment) *
0.3) {
+ const auto& user_params = _ann_topn_runtime->user_params();
+ if (user_params.should_fallback_ann_index_by_small_candidate(pre_size,
rows_of_segment)) {
VLOG_DEBUG << fmt::format(
- "Ann topn predicate input rows {} < 30% of segment rows {},
will not use ann index "
- "to "
- "filter",
- pre_size, rows_of_segment);
+ "Ann topn predicate input rows {} reach small candidate
threshold, "
+ "rows_of_segment: {}, absolute_threshold: {},
percent_threshold: {}, "
+ "will not use ann index to filter",
+ pre_size, rows_of_segment,
user_params.ann_index_candidate_rows_threshold,
+ user_params.ann_index_candidate_rows_percent_threshold);
// Disable index-only scan on ann indexed column.
_need_read_data_indices[src_cid] = true;
_opts.stats->ann_fall_back_brute_force_cnt += 1;
+ _opts.stats->ann_topn_fallback_by_small_candidate_cnt += 1;
+ _opts.stats->ann_topn_fallback_small_candidate_rows += pre_size;
return Status::OK();
}
IColumn::MutablePtr result_column;
@@ -1370,7 +1374,7 @@ Status SegmentIterator::_apply_index_expr() {
bool ann_range_search_executed = false;
RETURN_IF_ERROR(expr_ctx->evaluate_ann_range_search(
_index_iterators, _schema->column_ids(), _column_iterators,
- _common_expr_to_slotref_map, _row_bitmap, ann_index_stats,
+ _common_expr_to_slotref_map, num_rows(), _row_bitmap,
ann_index_stats,
enable_ann_index_result_cache, &ann_range_search_executed));
if (ann_range_search_executed) {
_opts.stats->ann_index_range_search_cnt++;
@@ -1388,6 +1392,10 @@ Status SegmentIterator::_apply_index_expr() {
_opts.stats->ann_range_engine_convert_ns +=
ann_index_stats.engine_convert_ns.value();
_opts.stats->ann_range_pre_process_ns +=
ann_index_stats.engine_prepare_ns.value();
_opts.stats->ann_fall_back_brute_force_cnt +=
ann_index_stats.fall_back_brute_force_cnt;
+ _opts.stats->ann_range_fallback_by_small_candidate_cnt +=
+ ann_index_stats.range_fallback_by_small_candidate_cnt;
+ _opts.stats->ann_range_fallback_small_candidate_rows +=
+ ann_index_stats.range_fallback_small_candidate_rows;
_opts.stats->ann_index_range_cache_hits +=
ann_index_stats.range_cache_hits.value();
}
diff --git a/be/test/storage/index/ann/ann_index_edge_case_test.cpp
b/be/test/storage/index/ann/ann_index_edge_case_test.cpp
index aa6d7637dd4..a42085f02c3 100644
--- a/be/test/storage/index/ann/ann_index_edge_case_test.cpp
+++ b/be/test/storage/index/ann/ann_index_edge_case_test.cpp
@@ -22,6 +22,7 @@
#include <string>
#include <vector>
+#include "runtime/runtime_state.h"
#include "storage/index/ann/ann_index_iterator.h"
#include "storage/index/ann/ann_index_reader.h"
#include "storage/index/ann/ann_index_writer.h"
@@ -38,24 +39,34 @@ TEST_F(VectorSearchTest, TestAnnIndexStatsInitialization) {
// Test initial values
EXPECT_EQ(stats.search_costs_ns.value(), 0);
EXPECT_EQ(stats.load_index_costs_ns.value(), 0);
+ EXPECT_EQ(stats.range_fallback_by_small_candidate_cnt, 0);
+ EXPECT_EQ(stats.range_fallback_small_candidate_rows, 0);
// Test setting values
stats.search_costs_ns.set(1000L);
stats.load_index_costs_ns.set(2000L);
+ stats.range_fallback_by_small_candidate_cnt = 1;
+ stats.range_fallback_small_candidate_rows = 3;
EXPECT_EQ(stats.search_costs_ns.value(), 1000);
EXPECT_EQ(stats.load_index_costs_ns.value(), 2000);
+ EXPECT_EQ(stats.range_fallback_by_small_candidate_cnt, 1);
+ EXPECT_EQ(stats.range_fallback_small_candidate_rows, 3);
}
TEST_F(VectorSearchTest, TestAnnIndexStatsCopyConstructor) {
doris::segment_v2::AnnIndexStats original;
original.search_costs_ns.set(1500L);
original.load_index_costs_ns.set(2500L);
+ original.range_fallback_by_small_candidate_cnt = 1;
+ original.range_fallback_small_candidate_rows = 3;
doris::segment_v2::AnnIndexStats copied(original);
EXPECT_EQ(copied.search_costs_ns.value(), 1500);
EXPECT_EQ(copied.load_index_costs_ns.value(), 2500);
+ EXPECT_EQ(copied.range_fallback_by_small_candidate_cnt, 1);
+ EXPECT_EQ(copied.range_fallback_small_candidate_rows, 3);
}
TEST_F(VectorSearchTest, TestAnnRangeSearchParamsToString) {
@@ -119,6 +130,25 @@ TEST_F(VectorSearchTest,
TestVectorSearchUserParamsDefaultValues) {
EXPECT_EQ(params.hnsw_ef_search, 32);
EXPECT_EQ(params.hnsw_check_relative_distance, true);
EXPECT_EQ(params.hnsw_bounded_queue, true);
+ EXPECT_EQ(params.ivf_nprobe, 32);
+ EXPECT_EQ(params.ann_index_candidate_rows_threshold, 0);
+ EXPECT_EQ(params.ann_index_candidate_rows_percent_threshold, 0.3);
+}
+
+TEST_F(VectorSearchTest, TestVectorSearchUserParamsSmallCandidateFallback) {
+ doris::VectorSearchUserParams params;
+ params.ann_index_candidate_rows_percent_threshold = 0;
+
+ EXPECT_FALSE(params.should_fallback_ann_index_by_small_candidate(3, 10));
+
+ params.ann_index_candidate_rows_threshold = 4;
+ EXPECT_TRUE(params.should_fallback_ann_index_by_small_candidate(3, 10));
+ EXPECT_FALSE(params.should_fallback_ann_index_by_small_candidate(4, 10));
+
+ params.ann_index_candidate_rows_threshold = 0;
+ params.ann_index_candidate_rows_percent_threshold = 0.3;
+ EXPECT_TRUE(params.should_fallback_ann_index_by_small_candidate(2, 10));
+ EXPECT_FALSE(params.should_fallback_ann_index_by_small_candidate(3, 10));
}
TEST_F(VectorSearchTest, TestVectorSearchUserParamsEquality) {
@@ -137,6 +167,33 @@ TEST_F(VectorSearchTest,
TestVectorSearchUserParamsEquality) {
// Test inequality
params2.hnsw_ef_search = 50;
EXPECT_NE(params1, params2);
+
+ params2.hnsw_ef_search = 100;
+ params2.ann_index_candidate_rows_threshold = 10;
+ EXPECT_NE(params1, params2);
+
+ params2.ann_index_candidate_rows_threshold = 0;
+ params2.ann_index_candidate_rows_percent_threshold = 0.1;
+ EXPECT_NE(params1, params2);
+}
+
+TEST_F(VectorSearchTest, TestRuntimeStateVectorSearchUserParams) {
+ TQueryOptions query_options;
+ query_options.__set_hnsw_ef_search(64);
+ query_options.__set_hnsw_check_relative_distance(false);
+ query_options.__set_hnsw_bounded_queue(false);
+ query_options.__set_ivf_nprobe(8);
+ query_options.__set_ann_index_candidate_rows_threshold(100);
+ query_options.__set_ann_index_candidate_rows_percent_threshold(0.2);
+
+ RuntimeState state(query_options, TQueryGlobals());
+ auto params = state.get_vector_search_params();
+ EXPECT_EQ(params.hnsw_ef_search, 64);
+ EXPECT_EQ(params.hnsw_check_relative_distance, false);
+ EXPECT_EQ(params.hnsw_bounded_queue, false);
+ EXPECT_EQ(params.ivf_nprobe, 8);
+ EXPECT_EQ(params.ann_index_candidate_rows_threshold, 100);
+ EXPECT_EQ(params.ann_index_candidate_rows_percent_threshold, 0.2);
}
TEST_F(VectorSearchTest, TestIndexSearchResultInitialization) {
diff --git a/be/test/storage/index/ann/ann_range_search_test.cpp
b/be/test/storage/index/ann/ann_range_search_test.cpp
index a3395f0fc2d..0ba7b122df6 100644
--- a/be/test/storage/index/ann/ann_range_search_test.cpp
+++ b/be/test/storage/index/ann/ann_range_search_test.cpp
@@ -189,8 +189,8 @@ TEST_F(VectorSearchTest, TestEvaluateAnnRangeSearch) {
ASSERT_TRUE(range_search_ctx
->evaluate_ann_range_search(cid_to_index_iterators,
idx_to_cid,
column_iterators,
common_expr_to_slotref_map,
- row_bitmap, stats, false,
- &ann_range_search_executed)
+ row_bitmap.cardinality(),
row_bitmap, stats,
+ false,
&ann_range_search_executed)
.ok());
EXPECT_TRUE(ann_range_search_executed);
@@ -293,8 +293,8 @@ TEST_F(VectorSearchTest, TestEvaluateAnnRangeSearch2) {
ASSERT_TRUE(range_search_ctx
->evaluate_ann_range_search(cid_to_index_iterators,
idx_to_cid,
column_iterators,
common_expr_to_slotref_map,
- row_bitmap, stats, false,
- &ann_range_search_executed)
+ row_bitmap.cardinality(),
row_bitmap, stats,
+ false,
&ann_range_search_executed)
.ok());
EXPECT_TRUE(ann_range_search_executed);
@@ -375,10 +375,10 @@ TEST_F(VectorSearchTest,
TestEvaluateAnnRangeSearchStateDoesNotLeakAcrossClones)
common_expr_to_slotref_map;
bool ann_range_search_executed = false;
ASSERT_TRUE(segment_with_ann_ctx
- ->evaluate_ann_range_search(ann_index_iterators,
idx_to_cid,
- ann_column_iterators,
-
common_expr_to_slotref_map, ann_row_bitmap,
- ann_stats, false,
&ann_range_search_executed)
+ ->evaluate_ann_range_search(
+ ann_index_iterators, idx_to_cid,
ann_column_iterators,
+ common_expr_to_slotref_map,
ann_row_bitmap.cardinality(),
+ ann_row_bitmap, ann_stats, false,
&ann_range_search_executed)
.ok());
EXPECT_TRUE(ann_range_search_executed);
const auto* ann_result =
segment_with_ann_ctx->get_index_context()->get_index_result_for_expr(
@@ -398,12 +398,13 @@ TEST_F(VectorSearchTest,
TestEvaluateAnnRangeSearchStateDoesNotLeakAcrossClones)
roaring::Roaring no_ann_row_bitmap;
segment_v2::AnnIndexStats no_ann_stats;
bool no_ann_range_search_executed = true;
- ASSERT_TRUE(segment_without_ann_ctx
- ->evaluate_ann_range_search(
- no_ann_index_iterators, idx_to_cid,
no_ann_column_iterators,
- common_expr_to_slotref_map, no_ann_row_bitmap,
no_ann_stats, false,
- &no_ann_range_search_executed)
- .ok());
+ ASSERT_TRUE(
+ segment_without_ann_ctx
+ ->evaluate_ann_range_search(no_ann_index_iterators,
idx_to_cid,
+ no_ann_column_iterators,
common_expr_to_slotref_map,
+
no_ann_row_bitmap.cardinality(), no_ann_row_bitmap,
+ no_ann_stats, false,
&no_ann_range_search_executed)
+ .ok());
EXPECT_FALSE(no_ann_range_search_executed);
EXPECT_FALSE(segment_without_ann_ctx->get_index_context()->has_index_result_for_expr(
segment_without_ann_ctx->root().get()));
@@ -480,8 +481,8 @@ TEST_F(VectorSearchTest,
TestEvaluateAnnRangeSearchUsesSourceColumnIndexForSlotM
ASSERT_TRUE(range_search_ctx
->evaluate_ann_range_search(cid_to_index_iterators,
idx_to_cid,
column_iterators,
common_expr_to_slotref_map,
- row_bitmap, stats, false,
- &ann_range_search_executed)
+ row_bitmap.cardinality(),
row_bitmap, stats,
+ false,
&ann_range_search_executed)
.ok());
EXPECT_TRUE(ann_range_search_executed);
EXPECT_TRUE(common_expr_index_status[5][range_search_ctx->root().get()]);
@@ -872,7 +873,7 @@ TEST_F(VectorSearchTest,
TestEvaluateAnnRangeSearch_DimensionMismatch) {
auto st = range_search_ctx->evaluate_ann_range_search(
cid_to_index_iterators, idx_to_cid, column_iterators,
common_expr_to_slotref_map,
- row_bitmap, stats, false, nullptr);
+ row_bitmap.cardinality(), row_bitmap, stats, false, nullptr);
EXPECT_FALSE(st.ok());
EXPECT_TRUE(st.is<doris::ErrorCode::INVALID_ARGUMENT>());
}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
b/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
index e096b3772eb..c9a17bc7527 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
@@ -965,6 +965,10 @@ public class SessionVariable implements Serializable,
Writable {
public static final String HNSW_CHECK_RELATIVE_DISTANCE =
"hnsw_check_relative_distance";
public static final String HNSW_BOUNDED_QUEUE = "hnsw_bounded_queue";
public static final String IVF_NPROBE = "ivf_nprobe";
+ public static final String ANN_INDEX_CANDIDATE_ROWS_THRESHOLD =
+ "ann_index_candidate_rows_threshold";
+ public static final String ANN_INDEX_CANDIDATE_ROWS_PERCENT_THRESHOLD =
+ "ann_index_candidate_rows_percent_threshold";
public static final String DEFAULT_VARIANT_MAX_SUBCOLUMNS_COUNT =
"default_variant_max_subcolumns_count";
@@ -3531,6 +3535,38 @@ public class SessionVariable implements Serializable,
Writable {
"IVF index nprobe parameter, controls the number of
clusters to search"})
public int ivfNprobe = 32;
+ @VarAttrDef.VarAttr(name = ANN_INDEX_CANDIDATE_ROWS_THRESHOLD, needForward
= true,
+ checker = "checkAnnIndexCandidateRowsThreshold",
+ description = {"Skip ANN index when candidate rows before ANN
search are less "
+ + "than this threshold. 0 disables the absolute row
threshold",
+ "Skip ANN index when candidate rows before ANN search are
less "
+ + "than this threshold. 0 disables the absolute
row threshold"})
+ public long annIndexCandidateRowsThreshold = 0;
+
+ @VarAttrDef.VarAttr(name = ANN_INDEX_CANDIDATE_ROWS_PERCENT_THRESHOLD,
needForward = true,
+ checker = "checkAnnIndexCandidateRowsPercentThreshold",
+ description = {"Skip ANN index when candidate row ratio before ANN
search is less "
+ + "than this threshold",
+ "Skip ANN index when candidate row ratio before ANN search
is less "
+ + "than this threshold"})
+ public double annIndexCandidateRowsPercentThreshold = 0.3;
+
+ public void checkAnnIndexCandidateRowsThreshold(String value) {
+ long threshold = Long.parseLong(value);
+ if (threshold < 0) {
+ throw new InvalidParameterException(
+ ANN_INDEX_CANDIDATE_ROWS_THRESHOLD + " should be greater
than or equal to 0");
+ }
+ }
+
+ public void checkAnnIndexCandidateRowsPercentThreshold(String value) {
+ double threshold = Double.parseDouble(value);
+ if (Double.isNaN(threshold) || Double.isInfinite(threshold) ||
threshold < 0 || threshold > 1) {
+ throw new InvalidParameterException(
+ ANN_INDEX_CANDIDATE_ROWS_PERCENT_THRESHOLD + " should be
between 0 and 1");
+ }
+ }
+
@VarAttrDef.VarAttr(
name = DEFAULT_VARIANT_MAX_SUBCOLUMNS_COUNT,
needForward = true,
@@ -5566,6 +5602,8 @@ public class SessionVariable implements Serializable,
Writable {
tResult.setHnswCheckRelativeDistance(hnswCheckRelativeDistance);
tResult.setHnswBoundedQueue(hnswBoundedQueue);
tResult.setIvfNprobe(ivfNprobe);
+
tResult.setAnnIndexCandidateRowsThreshold(annIndexCandidateRowsThreshold);
+
tResult.setAnnIndexCandidateRowsPercentThreshold(annIndexCandidateRowsPercentThreshold);
tResult.setMergeReadSliceSize(mergeReadSliceSizeBytes);
tResult.setEnableExtendedRegex(enableExtendedRegex);
if (fileCacheQueryLimitPercent > 0) {
diff --git a/gensrc/thrift/PaloInternalService.thrift
b/gensrc/thrift/PaloInternalService.thrift
index 3271cb01e88..aee6e567f13 100644
--- a/gensrc/thrift/PaloInternalService.thrift
+++ b/gensrc/thrift/PaloInternalService.thrift
@@ -493,6 +493,11 @@ struct TQueryOptions {
219: optional bool enable_segment_limit_pushdown = true
220: optional bool enable_ann_index_result_cache = true
+ // ANN search falls back to exact vector distance evaluation when candidate
rows
+ // before ANN search are less than this value. 0 disables the absolute
threshold.
+ 221: optional i64 ann_index_candidate_rows_threshold = 0
+ // Candidate row ratio threshold against segment rows. Existing default is
0.3.
+ 222: optional double ann_index_candidate_rows_percent_threshold = 0.3
// For cloud, to control if the content would be written into file cache
// In write path, to control if the content would be written into file cache.
// In read path, read from file cache or remote storage when execute query.
diff --git
a/regression-test/suites/ann_index_p0/ann_topn_small_candidate_fallback.groovy
b/regression-test/suites/ann_index_p0/ann_topn_small_candidate_fallback.groovy
new file mode 100644
index 00000000000..b4f0e29085b
--- /dev/null
+++
b/regression-test/suites/ann_index_p0/ann_topn_small_candidate_fallback.groovy
@@ -0,0 +1,231 @@
+// 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 groovy.json.JsonSlurper
+
+def getProfileList = {
+ def dst = "http://" + context.config.feHttpAddress
+ def conn = new URL(dst + "/rest/v1/query_profile").openConnection()
+ conn.setRequestMethod("GET")
+ def encoding =
Base64.getEncoder().encodeToString((context.config.feHttpUser + ":" +
+ (context.config.feHttpPassword == null ? "" :
context.config.feHttpPassword))
+ .getBytes("UTF-8"))
+ conn.setRequestProperty("Authorization", "Basic ${encoding}")
+ return conn.getInputStream().getText()
+}
+
+def getProfile = { id ->
+ def dst = "http://" + context.config.feHttpAddress
+ def conn = new URL(dst +
"/api/profile/text/?query_id=$id").openConnection()
+ conn.setRequestMethod("GET")
+ def encoding =
Base64.getEncoder().encodeToString((context.config.feHttpUser + ":" +
+ (context.config.feHttpPassword == null ? "" :
context.config.feHttpPassword))
+ .getBytes("UTF-8"))
+ conn.setRequestProperty("Authorization", "Basic ${encoding}")
+ return conn.getInputStream().getText()
+}
+
+def extractCounterValue = { String profileText, String counterName ->
+ for (def line : profileText.split("\n")) {
+ if (line.contains(counterName + ":")) {
+ def m = (line =~
/${java.util.regex.Pattern.quote(counterName)}:\s*([0-9]+(?:\.[0-9]+)?)/)
+ if (m.find()) {
+ return m.group(1)
+ }
+ }
+ }
+ return null
+}
+
+suite("ann_topn_small_candidate_fallback", "nonConcurrent") {
+ def getProfileWithToken = { token ->
+ String profileId = ""
+ int attempts = 0
+ while (attempts < 10 && (profileId == null || profileId == "")) {
+ List profileData = new
JsonSlurper().parseText(getProfileList()).data.rows
+ for (def profileItem in profileData) {
+ if (profileItem["Sql Statement"].toString().contains(token)) {
+ profileId = profileItem["Profile ID"].toString()
+ break
+ }
+ }
+ if (profileId == null || profileId == "") {
+ Thread.sleep(300)
+ }
+ attempts++
+ }
+ assertTrue(profileId != null && profileId != "")
+ Thread.sleep(800)
+ return getProfile(profileId).toString()
+ }
+
+ sql "unset variable all;"
+ sql "set enable_common_expr_pushdown=true;"
+ sql "set experimental_enable_virtual_slot_for_cse=true;"
+ sql "set enable_no_need_read_data_opt=true;"
+ sql "set enable_profile=true;"
+ sql "set profile_level=2;"
+ sql "set parallel_pipeline_task_num=1;"
+ sql "set enable_sql_cache=false;"
+ sql "set enable_condition_cache=false;"
+ sql "set ann_index_candidate_rows_percent_threshold=0;"
+
+ sql "drop table if exists ann_topn_small_candidate_fallback"
+ sql """
+ create table ann_topn_small_candidate_fallback (
+ id int not null,
+ embedding array<float> not null,
+ comment string not null,
+ index idx_comment(`comment`) using inverted properties("parser" =
"english"),
+ index ann_embedding(`embedding`) using ann properties(
+ "index_type"="hnsw",
+ "metric_type"="l2_distance",
+ "dim"="3"
+ )
+ ) duplicate key(id)
+ distributed by hash(id) buckets 1
+ properties("replication_num"="1");
+ """
+
+ sql """
+ insert into ann_topn_small_candidate_fallback values
+ (1, [0.0, 0.0, 0.0], 'small candidate'),
+ (2, [0.2, 0.0, 0.0], 'small candidate'),
+ (3, [0.4, 0.0, 0.0], 'small candidate'),
+ (4, [10.0, 0.0, 0.0], 'large candidate'),
+ (5, [11.0, 0.0, 0.0], 'large candidate'),
+ (6, [12.0, 0.0, 0.0], 'large candidate'),
+ (7, [13.0, 0.0, 0.0], 'large candidate'),
+ (8, [14.0, 0.0, 0.0], 'large candidate'),
+ (9, [15.0, 0.0, 0.0], 'large candidate'),
+ (10, [16.0, 0.0, 0.0], 'large candidate');
+ """
+
+ def defaultRows = sql """
+ select id
+ from ann_topn_small_candidate_fallback
+ where comment match_any 'small'
+ order by l2_distance_approximate(embedding, [0.0, 0.0, 0.0])
+ limit 2;
+ """
+ assertEquals([1, 2], defaultRows.collect { it[0] })
+
+ sql "set ann_index_candidate_rows_threshold=4;"
+ def fallbackRows = sql """
+ select id
+ from ann_topn_small_candidate_fallback
+ where comment match_any 'small'
+ order by l2_distance_approximate(embedding, [0.0, 0.0, 0.0])
+ limit 2;
+ """
+ assertEquals([1, 2], fallbackRows.collect { it[0] })
+
+ def tokenFallback = UUID.randomUUID().toString()
+ sql """
+ select id, "${tokenFallback}"
+ from ann_topn_small_candidate_fallback
+ where comment match_any 'small'
+ order by l2_distance_approximate(embedding, [0.0, 0.0, 0.0])
+ limit 2;
+ """
+ def fallbackProfile = getProfileWithToken(tokenFallback)
+ def smallCandidateFallbackCnt =
+ extractCounterValue(fallbackProfile,
"AnnIndexTopNFallbackBySmallCandidateCnt")
+ def smallCandidateRows =
+ extractCounterValue(fallbackProfile,
"AnnIndexTopNFallbackSmallCandidateRows")
+ logger.info("small candidate fallback count=${smallCandidateFallbackCnt},
rows=${smallCandidateRows}")
+ assertEquals("1", smallCandidateFallbackCnt)
+ assertEquals("3", smallCandidateRows)
+
+ sql "set ann_index_candidate_rows_threshold=11;"
+ def tokenRangeFallback = UUID.randomUUID().toString()
+ sql """
+ select id, "${tokenRangeFallback}"
+ from ann_topn_small_candidate_fallback
+ where l2_distance_approximate(embedding, [0.0, 0.0, 0.0]) < 1.0
+ order by id;
+ """
+ def rangeFallbackProfile = getProfileWithToken(tokenRangeFallback)
+ def rangeSmallCandidateFallbackCnt =
+ extractCounterValue(rangeFallbackProfile,
"AnnIndexRangeFallbackBySmallCandidateCnt")
+ def rangeSmallCandidateRows =
+ extractCounterValue(rangeFallbackProfile,
"AnnIndexRangeFallbackSmallCandidateRows")
+ logger.info("range small candidate fallback
count=${rangeSmallCandidateFallbackCnt}, " +
+ "rows=${rangeSmallCandidateRows}")
+ assertEquals("1", rangeSmallCandidateFallbackCnt)
+ assertEquals("10", rangeSmallCandidateRows)
+
+ try {
+ GetDebugPoint().enableDebugPointForAllBEs(
+ "segment_iterator._read_columns_by_index", [column_name:
"embedding"])
+
+ sql "set ann_index_candidate_rows_threshold=4;"
+ test {
+ sql """
+ select id
+ from ann_topn_small_candidate_fallback
+ where comment match_any 'small'
+ order by l2_distance_approximate(embedding, [0.0, 0.0, 0.0])
+ limit 2;
+ """
+ exception "does not need to read data"
+ }
+
+ sql "set ann_index_candidate_rows_threshold=11;"
+ test {
+ sql """
+ select id
+ from ann_topn_small_candidate_fallback
+ where l2_distance_approximate(embedding, [0.0, 0.0, 0.0]) < 1.0
+ order by id;
+ """
+ exception "does not need to read data"
+ }
+
+ sql "set ann_index_candidate_rows_threshold=0;"
+ sql """
+ select id
+ from ann_topn_small_candidate_fallback
+ where comment match_any 'small'
+ order by l2_distance_approximate(embedding, [0.0, 0.0, 0.0])
+ limit 2;
+ """
+ sql """
+ select id
+ from ann_topn_small_candidate_fallback
+ where l2_distance_approximate(embedding, [0.0, 0.0, 0.0]) < 1.0
+ order by id;
+ """
+ } finally {
+
GetDebugPoint().disableDebugPointForAllBEs("segment_iterator._read_columns_by_index")
+ }
+
+ test {
+ sql "set ann_index_candidate_rows_threshold=-1;"
+ exception "ann_index_candidate_rows_threshold should be greater than
or equal to 0"
+ }
+
+ test {
+ sql "set ann_index_candidate_rows_percent_threshold=1.1;"
+ exception "ann_index_candidate_rows_percent_threshold should be
between 0 and 1"
+ }
+
+ test {
+ sql "set ann_index_candidate_rows_percent_threshold=NaN;"
+ exception "ann_index_candidate_rows_percent_threshold should be
between 0 and 1"
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]