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 aa564ea61dd [feature](ann-index) Add ann topn small candidate fallback 
session va… (#64555)
aa564ea61dd is described below

commit aa564ea61dd7faaee3a33d7cc3a2ba420c091a77
Author: Qi Chen <[email protected]>
AuthorDate: Wed Jun 17 17:27:50 2026 +0800

    [feature](ann-index) Add ann topn small candidate fallback session va… 
(#64555)
    
    ### What problem does this PR solve?
    
    Issue Number: close #xxx
    
    Related PR: #xxx
    
    Problem Summary:
    
    ### Release note
    
    Cherry-pick #64243
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [ ] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - Behavior changed:
        - [ ] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [ ] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
---
 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                |  17 +-
 be/src/exprs/vectorized_fn_call.h                  |   3 +-
 be/src/exprs/vexpr.cpp                             |   2 +-
 be/src/exprs/vexpr.h                               |   4 +-
 be/src/exprs/vexpr_context.cpp                     |   6 +-
 be/src/exprs/vexpr_context.h                       |   4 +-
 be/src/exprs/virtual_slot_ref.cpp                  |   8 +-
 be/src/exprs/virtual_slot_ref.h                    |   3 +-
 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 |  60 ++++++
 .../storage/index/ann/ann_range_search_test.cpp    |  27 +--
 .../java/org/apache/doris/qe/SessionVariable.java  |  38 ++++
 gensrc/thrift/PaloInternalService.thrift           |   5 +
 .../ann_topn_small_candidate_fallback.groovy       | 231 +++++++++++++++++++++
 23 files changed, 467 insertions(+), 38 deletions(-)

diff --git a/be/src/exec/operator/olap_scan_operator.cpp 
b/be/src/exec/operator/olap_scan_operator.cpp
index 12aabf2d457..da855aff32d 100644
--- a/be/src/exec/operator/olap_scan_operator.cpp
+++ b/be/src/exec/operator/olap_scan_operator.cpp
@@ -374,6 +374,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 5bf32f7b870..cfc16bd65d0 100644
--- a/be/src/exec/operator/olap_scan_operator.h
+++ b/be/src/exec/operator/olap_scan_operator.h
@@ -258,6 +258,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 c2d3a945cca..aa21a657fba 100644
--- a/be/src/exec/scan/olap_scanner.cpp
+++ b/be/src/exec/scan/olap_scanner.cpp
@@ -910,6 +910,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 b8e679936ab..c5e969172a4 100644
--- a/be/src/exec/scan/vector_search_user_params.cpp
+++ b/be/src/exec/scan/vector_search_user_params.cpp
@@ -24,13 +24,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 dfe1bf89880..bef9ff30aaf 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 {
@@ -27,9 +29,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;
 };
 #include "common/compile_check_end.h"
diff --git a/be/src/exprs/vectorized_fn_call.cpp 
b/be/src/exprs/vectorized_fn_call.cpp
index 40aef9a0e4f..542eaf98512 100644
--- a/be/src/exprs/vectorized_fn_call.cpp
+++ b/be/src/exprs/vectorized_fn_call.cpp
@@ -556,7 +556,8 @@ 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,
+        size_t rows_of_segment, roaring::Roaring& row_bitmap,
+        segment_v2::AnnIndexStats& ann_index_stats,
         AnnRangeSearchEvaluationResult& evaluation_result) {
     evaluation_result = {};
     if (range_search_runtime.is_ann_range_search == false) {
@@ -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 f90206c6d81..61034b34d75 100644
--- a/be/src/exprs/vectorized_fn_call.h
+++ b/be/src/exprs/vectorized_fn_call.h
@@ -92,7 +92,8 @@ 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,
+            size_t rows_of_segment, roaring::Roaring& row_bitmap,
+            segment_v2::AnnIndexStats& ann_index_stats,
             AnnRangeSearchEvaluationResult& result) override;
 
     void prepare_ann_range_search(const doris::VectorSearchUserParams& params,
diff --git a/be/src/exprs/vexpr.cpp b/be/src/exprs/vexpr.cpp
index 5c80aecede0..d26d209cb55 100644
--- a/be/src/exprs/vexpr.cpp
+++ b/be/src/exprs/vexpr.cpp
@@ -1017,7 +1017,7 @@ 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,
+        size_t rows_of_segment, roaring::Roaring& row_bitmap, AnnIndexStats& 
ann_index_stats,
         AnnRangeSearchEvaluationResult& result) {
     result = {};
     return Status::OK();
diff --git a/be/src/exprs/vexpr.h b/be/src/exprs/vexpr.h
index 196ffd1b082..9ab63b6e31c 100644
--- a/be/src/exprs/vexpr.h
+++ b/be/src/exprs/vexpr.h
@@ -352,8 +352,8 @@ 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,
-            AnnRangeSearchEvaluationResult& result);
+            size_t rows_of_segment, roaring::Roaring& row_bitmap,
+            segment_v2::AnnIndexStats& ann_index_stats, 
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 29e0b25cf49..084d466a61d 100644
--- a/be/src/exprs/vexpr_context.cpp
+++ b/be/src/exprs/vexpr_context.cpp
@@ -433,8 +433,8 @@ 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* ann_range_search_executed) {
+        size_t rows_of_segment, roaring::Roaring& row_bitmap,
+        segment_v2::AnnIndexStats& ann_index_stats, bool* 
ann_range_search_executed) {
     if (ann_range_search_executed != nullptr) {
         *ann_range_search_executed = false;
     }
@@ -445,7 +445,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, evaluation_result));
+            rows_of_segment, row_bitmap, ann_index_stats, 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 ccd385d3a93..b07027ef162 100644
--- a/be/src/exprs/vexpr_context.h
+++ b/be/src/exprs/vexpr_context.h
@@ -395,8 +395,8 @@ 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* ann_range_search_executed);
+            size_t rows_of_segment, roaring::Roaring& row_bitmap,
+            segment_v2::AnnIndexStats& ann_index_stats, 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 66448b39af1..01f8f708cab 100644
--- a/be/src/exprs/virtual_slot_ref.cpp
+++ b/be/src/exprs/virtual_slot_ref.cpp
@@ -234,11 +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,
-        AnnRangeSearchEvaluationResult& result) {
+        size_t rows_of_segment, roaring::Roaring& row_bitmap,
+        segment_v2::AnnIndexStats& ann_index_stats, 
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, result);
+            range_search_runtime, cid_to_index_iterators, idx_to_cid, 
column_iterators,
+            rows_of_segment, row_bitmap, ann_index_stats, result);
 }
 #include "common/compile_check_end.h"
 } // namespace doris
diff --git a/be/src/exprs/virtual_slot_ref.h b/be/src/exprs/virtual_slot_ref.h
index 382a3b45e34..8fbf2567719 100644
--- a/be/src/exprs/virtual_slot_ref.h
+++ b/be/src/exprs/virtual_slot_ref.h
@@ -107,7 +107,8 @@ 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,
+            size_t rows_of_segment, roaring::Roaring& row_bitmap,
+            segment_v2::AnnIndexStats& ann_index_stats,
             AnnRangeSearchEvaluationResult& result) override;
 
 #ifdef BE_TEST
diff --git a/be/src/runtime/runtime_state.h b/be/src/runtime/runtime_state.h
index b8a85d097f9..13e4b7b6355 100644
--- a/be/src/runtime/runtime_state.h
+++ b/be/src/runtime/runtime_state.h
@@ -845,6 +845,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 95d8d11dd83..9ad0f9d30c5 100644
--- a/be/src/storage/index/ann/ann_search_params.h
+++ b/be/src/storage/index/ann/ann_search_params.h
@@ -61,7 +61,9 @@ struct AnnIndexStats {
               ivf_on_disk_search_cnt(TUnit::UNIT, 0),
               ivf_on_disk_cache_hit_cnt(TUnit::UNIT, 0),
               ivf_on_disk_cache_miss_cnt(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()),
@@ -76,7 +78,9 @@ struct AnnIndexStats {
               ivf_on_disk_search_cnt(TUnit::UNIT, 
other.ivf_on_disk_search_cnt.value()),
               ivf_on_disk_cache_hit_cnt(TUnit::UNIT, 
other.ivf_on_disk_cache_hit_cnt.value()),
               ivf_on_disk_cache_miss_cnt(TUnit::UNIT, 
other.ivf_on_disk_cache_miss_cnt.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) {
@@ -92,6 +96,8 @@ struct AnnIndexStats {
             
ivf_on_disk_cache_hit_cnt.set(other.ivf_on_disk_cache_hit_cnt.value());
             
ivf_on_disk_cache_miss_cnt.set(other.ivf_on_disk_cache_miss_cnt.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;
     }
@@ -109,6 +115,8 @@ struct AnnIndexStats {
     RuntimeProfile::Counter ivf_on_disk_cache_hit_cnt;   // IVF_ON_DISK cache 
hit count
     RuntimeProfile::Counter ivf_on_disk_cache_miss_cnt;  // IVF_ON_DISK cache 
miss count
     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 63e04cc30b6..05167f76317 100644
--- a/be/src/storage/index/ann/ann_topn_runtime.h
+++ b/be/src/storage/index/ann/ann_topn_runtime.h
@@ -151,6 +151,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 8219acf28b8..4c9a4fd8352 100644
--- a/be/src/storage/olap_common.h
+++ b/be/src/storage/olap_common.h
@@ -400,6 +400,10 @@ struct OlapReaderStatistics {
     int64_t ann_range_engine_convert_ns = 0; // time spent on FAISS-side 
conversions (Range)
     int64_t rows_ann_index_range_filtered = 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 d5e9b68437e..8c9ccb7424d 100644
--- a/be/src/storage/segment/segment_iterator.cpp
+++ b/be/src/storage/segment/segment_iterator.cpp
@@ -933,15 +933,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;
@@ -1238,7 +1242,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,
                 &ann_range_search_executed));
         if (ann_range_search_executed) {
             _opts.stats->ann_index_range_search_cnt++;
@@ -1256,6 +1260,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;
     }
 
     return Status::OK();
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..e4ab446c928 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,11 +22,13 @@
 #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"
 #include "storage/index/ann/faiss_ann_index.h"
 #include "storage/index/ann/vector_search_utils.h"
+#include "testutil/mock/mock_query_context.h"
 
 using namespace doris::vector_search_utils;
 
@@ -38,24 +40,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 +131,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 +168,35 @@ 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);
+
+    auto query_ctx = MockQueryContext::create();
+    TQueryGlobals query_globals;
+    RuntimeState state(TUniqueId(), 0, query_options, query_globals, nullptr, 
query_ctx.get());
+    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 400e822695c..1beab6a9559 100644
--- a/be/test/storage/index/ann/ann_range_search_test.cpp
+++ b/be/test/storage/index/ann/ann_range_search_test.cpp
@@ -188,7 +188,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, 
&ann_range_search_executed)
+                                                    row_bitmap.cardinality(), 
row_bitmap, stats,
+                                                    &ann_range_search_executed)
                         .ok());
     EXPECT_TRUE(ann_range_search_executed);
 
@@ -291,7 +292,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, 
&ann_range_search_executed)
+                                                    row_bitmap.cardinality(), 
row_bitmap, stats,
+                                                    &ann_range_search_executed)
                         .ok());
     EXPECT_TRUE(ann_range_search_executed);
 
@@ -372,10 +374,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, 
&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, 
&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(
@@ -396,10 +398,10 @@ TEST_F(VectorSearchTest, 
TestEvaluateAnnRangeSearchStateDoesNotLeakAcrossClones)
     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, 
&no_ann_range_search_executed)
+                        ->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, 
&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(
@@ -477,7 +479,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, 
&ann_range_search_executed)
+                                                    row_bitmap.cardinality(), 
row_bitmap, stats,
+                                                    &ann_range_search_executed)
                         .ok());
     EXPECT_TRUE(ann_range_search_executed);
     EXPECT_TRUE(common_expr_index_status[5][range_search_ctx->root().get()]);
@@ -868,7 +871,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, nullptr);
+            row_bitmap.cardinality(), row_bitmap, stats, 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 dfae86f84f3..9242527f155 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
@@ -959,6 +959,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";
 
@@ -3459,6 +3463,38 @@ public class SessionVariable implements Serializable, 
Writable {
                     "IVF index nprobe parameter, controls the number of 
clusters to search"})
     public int ivfNprobe = 32;
 
+    @VariableMgr.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;
+
+    @VariableMgr.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");
+        }
+    }
+
     @VariableMgr.VarAttr(
             name = DEFAULT_VARIANT_MAX_SUBCOLUMNS_COUNT,
             needForward = true,
@@ -5477,6 +5513,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);
 
diff --git a/gensrc/thrift/PaloInternalService.thrift 
b/gensrc/thrift/PaloInternalService.thrift
index db2835d81ed..71b318f6f0c 100644
--- a/gensrc/thrift/PaloInternalService.thrift
+++ b/gensrc/thrift/PaloInternalService.thrift
@@ -491,6 +491,11 @@ struct TQueryOptions {
   // Default 8MB. Sent by FE session variable preferred_block_size_bytes.
   218: optional i64 preferred_block_size_bytes = 8388608
 
+  // 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.
+  219: optional i64 ann_index_candidate_rows_threshold = 0
+  // Candidate row ratio threshold against segment rows. Existing default is 
0.3.
+  220: 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]

Reply via email to