This is an automated email from the ASF dual-hosted git repository.

yiguolei 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 69d0094f62e [improvement](scan) Align scanner split to segments for 
MATCH (#67915)
69d0094f62e is described below

commit 69d0094f62efa47cd877b12a914d9d4e0dc4d07f
Author: Chenyang Sun <[email protected]>
AuthorDate: Wed Oct 28 10:47:01 2026 +0800

    [improvement](scan) Align scanner split to segments for MATCH (#67915)
    
    ### What problem does this PR solve?
    An inverted index query is run over the whole segment and only then
    intersected with the scanner's
    row range, so its cost is priced per segment while
    `ParallelScannerBuilder` splits by rows. A
    segment shared by two scanners gets its posting lists walked twice.
    
    An ann topn has the same property and already asks for one scanner per
    segment. This PR reuses that
    path for MATCH, so both of these now build one scanner per segment:
    
    ```sql
    SELECT k1 FROM t ORDER BY l2_distance_approximate(embedding, [1.0, 2.0]) 
LIMIT 2;  -- already did
    SELECT k1 FROM t WHERE msg MATCH_PHRASE 'error timeout';                    
      -- new
    
    Cloud mode only. Queries without MATCH or ann topn are unchanged.
    
    Related PR: #xxx
    
    Problem Summary:
    
    ### Release note
    
    None
    
    ### 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.
            - [x] 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 -->
    
    ---------
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 be/src/exec/operator/olap_scan_operator.cpp        | 39 +++++++++++++++++-----
 be/src/exec/operator/olap_scan_operator.h          |  3 ++
 .../analyzer/test_custom_analyzer1.groovy          |  8 ++---
 .../analyzer/test_ik_analyzer.groovy               | 16 ++++-----
 .../suites/inverted_index_p0/test_stopwords.groovy | 16 ++++-----
 .../suites/search/test_search_dsl_syntax.groovy    |  6 ++--
 6 files changed, 56 insertions(+), 32 deletions(-)

diff --git a/be/src/exec/operator/olap_scan_operator.cpp 
b/be/src/exec/operator/olap_scan_operator.cpp
index a61fa641819..aecb90e02a8 100644
--- a/be/src/exec/operator/olap_scan_operator.cpp
+++ b/be/src/exec/operator/olap_scan_operator.cpp
@@ -477,6 +477,17 @@ static bool contains_expr_node_type(const VExprSPtr& expr, 
TExprNodeType::type n
     });
 }
 
+// Find MATCH recursively; ones nested in AND / OR / NOT count too.
+static bool is_match_expr(const VExprSPtr& expr) {
+    DORIS_CHECK(expr != nullptr);
+    if (expr->node_type() == TExprNodeType::MATCH_PRED ||
+        expr->node_type() == TExprNodeType::SEARCH_EXPR || 
expr->can_push_down_to_index()) {
+        return true;
+    }
+    return std::ranges::any_of(expr->children(),
+                               [](const auto& child) { return 
is_match_expr(child); });
+}
+
 static Status validate_residual_scan_conjuncts(RuntimeState* state,
                                                TPushAggOp::type 
push_down_agg_type,
                                                const VExprContextSPtrs& 
conjuncts) {
@@ -683,6 +694,24 @@ void OlapScanLocalState::_prune_rowsets_by_tso(const 
TPaloScanRange& scan_range,
     COUNTER_UPDATE(_segments_pruned_by_tso_counter, pruned_segments);
 }
 
+// Give each segment a scanner of its own for queries like:
+//   SELECT k1 FROM t ORDER BY l2_distance_approximate(embedding, [1.0, 2.0]) 
LIMIT 2
+//   SELECT k1 FROM t WHERE msg MATCH_PHRASE 'error timeout'
+bool OlapScanLocalState::_use_scan_parallelism_by_per_segment() {
+    if (!state()->query_options().__isset.optimize_index_scan_parallelism ||
+        !state()->query_options().optimize_index_scan_parallelism) {
+        return false;
+    }
+    // TODO: Use optimize_index_scan_parallelism for ann range search in the 
future.
+    // Currently, ann topn is enough
+    if (_ann_topn_runtime != nullptr) {
+        return true;
+    }
+    return config::is_cloud_mode() &&
+           std::ranges::any_of(_common_expr_ctxs_push_down,
+                               [](const auto& ctx) { return 
is_match_expr(ctx->root()); });
+}
+
 Status OlapScanLocalState::_init_scanners(std::list<ScannerSPtr>* scanners) {
     if (_scan_ranges.empty()) {
         _eos = true;
@@ -835,15 +864,7 @@ Status 
OlapScanLocalState::_init_scanners(std::list<ScannerSPtr>* scanners) {
         scanner_builder.set_min_rows_per_scanner(min_rows_per_scanner);
         // BM25 statistics must not depend on how a tablet is split into 
scanners.
         scanner_builder.set_share_tablet_collection_statistics(_score_runtime 
!= nullptr);
-        // If the session variable is set, force one scanner per segment.
-        if (state()->query_options().__isset.optimize_index_scan_parallelism &&
-            state()->query_options().optimize_index_scan_parallelism) {
-            // TODO: Use optimize_index_scan_parallelism for ann range search 
in the future.
-            // Currently, ann topn is enough
-            if (_ann_topn_runtime != nullptr) {
-                scanner_builder.set_scan_parallelism_by_per_segment(true);
-            }
-        }
+        
scanner_builder.set_scan_parallelism_by_per_segment(_use_scan_parallelism_by_per_segment());
 
         RETURN_IF_ERROR(scanner_builder.build_scanners(*scanners));
         for (auto& scanner : *scanners) {
diff --git a/be/src/exec/operator/olap_scan_operator.h 
b/be/src/exec/operator/olap_scan_operator.h
index 0c593e66f7b..0c3c0099c18 100644
--- a/be/src/exec/operator/olap_scan_operator.h
+++ b/be/src/exec/operator/olap_scan_operator.h
@@ -137,6 +137,9 @@ private:
 
     void _prune_rowsets_by_tso(const TPaloScanRange& scan_range, 
TabletReadSource& read_source);
 
+    // Whether each segment should be scanned by a scanner of its own.
+    bool _use_scan_parallelism_by_per_segment();
+
     Status _build_key_ranges_and_filters();
 
     bool _is_tablet_pruned_by_runtime_filter(int64_t partition_id, int32_t 
bucket_seq,
diff --git 
a/regression-test/suites/inverted_index_p0/analyzer/test_custom_analyzer1.groovy
 
b/regression-test/suites/inverted_index_p0/analyzer/test_custom_analyzer1.groovy
index 1f40f3c42d4..7dbbb9f25bb 100644
--- 
a/regression-test/suites/inverted_index_p0/analyzer/test_custom_analyzer1.groovy
+++ 
b/regression-test/suites/inverted_index_p0/analyzer/test_custom_analyzer1.groovy
@@ -120,11 +120,11 @@ suite("test_custom_analyzer1", "p0") {
         sql "sync"
         sql """ set enable_segment_limit_pushdown = true; """
 
-        qt_sql """ select * from test_custom_analyzer1 where ch match 'with'; 
"""
-        qt_sql """ select * from test_custom_analyzer1 where ch match 'the'; 
"""
+        order_qt_sql """ select * from test_custom_analyzer1 where ch match 
'with'; """
+        order_qt_sql """ select * from test_custom_analyzer1 where ch match 
'the'; """
 
-        qt_sql """ select * from test_custom_analyzer2 where ch match 'hm'; """
-        qt_sql """ select * from test_custom_analyzer2 where ch match 'bg'; """
+        order_qt_sql """ select * from test_custom_analyzer2 where ch match 
'hm'; """
+        order_qt_sql """ select * from test_custom_analyzer2 where ch match 
'bg'; """
     } finally {
     }
 }
diff --git 
a/regression-test/suites/inverted_index_p0/analyzer/test_ik_analyzer.groovy 
b/regression-test/suites/inverted_index_p0/analyzer/test_ik_analyzer.groovy
index e806a339fb0..250d8da7b87 100644
--- a/regression-test/suites/inverted_index_p0/analyzer/test_ik_analyzer.groovy
+++ b/regression-test/suites/inverted_index_p0/analyzer/test_ik_analyzer.groovy
@@ -70,17 +70,17 @@ suite("test_ik_analyzer", "p0") {
 
         // Testing ik smart mode
         println "Testing ik smart mode:"
-        qt_sql """ select * from ${tableNameSmart} where content match_phrase 
'北京'; """
-        qt_sql """ select * from ${tableNameSmart} where content match_phrase 
'计算机科学'; """
-        qt_sql """ select * from ${tableNameSmart} where content match_phrase 
'数据库管理系统'; """
-        qt_sql """ select * from ${tableNameSmart} where content match_phrase 
'中华人民共和国'; """
+        order_qt_sql """ select * from ${tableNameSmart} where content 
match_phrase '北京'; """
+        order_qt_sql """ select * from ${tableNameSmart} where content 
match_phrase '计算机科学'; """
+        order_qt_sql """ select * from ${tableNameSmart} where content 
match_phrase '数据库管理系统'; """
+        order_qt_sql """ select * from ${tableNameSmart} where content 
match_phrase '中华人民共和国'; """
 
         // Testing ik max_word mode
         println "Testing ik max_word mode:"
-        qt_sql """ select * from ${tableNameMaxWord} where content 
match_phrase '北京'; """
-        qt_sql """ select * from ${tableNameMaxWord} where content 
match_phrase '计算机科学'; """
-        qt_sql """ select * from ${tableNameMaxWord} where content 
match_phrase '数据库管理系统'; """
-        qt_sql """ select * from ${tableNameMaxWord} where content 
match_phrase '中华人民共和国'; """
+        order_qt_sql """ select * from ${tableNameMaxWord} where content 
match_phrase '北京'; """
+        order_qt_sql """ select * from ${tableNameMaxWord} where content 
match_phrase '计算机科学'; """
+        order_qt_sql """ select * from ${tableNameMaxWord} where content 
match_phrase '数据库管理系统'; """
+        order_qt_sql """ select * from ${tableNameMaxWord} where content 
match_phrase '中华人民共和国'; """
 
     } finally {
         sql "DROP TABLE IF EXISTS ${tableNameSmart}"
diff --git a/regression-test/suites/inverted_index_p0/test_stopwords.groovy 
b/regression-test/suites/inverted_index_p0/test_stopwords.groovy
index 31e30cf6e48..2ff7dd3f556 100644
--- a/regression-test/suites/inverted_index_p0/test_stopwords.groovy
+++ b/regression-test/suites/inverted_index_p0/test_stopwords.groovy
@@ -44,16 +44,16 @@ suite("test_stopwords", "p0"){
         sql "sync"
         sql """ set enable_segment_limit_pushdown = true """
 
-        qt_sql """ select * from ${indexTbName} where b match 'a'; """
-        qt_sql """ select * from ${indexTbName} where b match 'are'; """
-        qt_sql """ select * from ${indexTbName} where b match 'to'; """
+        order_qt_sql """ select * from ${indexTbName} where b match 'a'; """
+        order_qt_sql """ select * from ${indexTbName} where b match 'are'; """
+        order_qt_sql """ select * from ${indexTbName} where b match 'to'; """
 
-        qt_sql """ select * from ${indexTbName} where c match 'a'; """
-        qt_sql """ select * from ${indexTbName} where c match 'are'; """
-        qt_sql """ select * from ${indexTbName} where c match 'to'; """
+        order_qt_sql """ select * from ${indexTbName} where c match 'a'; """
+        order_qt_sql """ select * from ${indexTbName} where c match 'are'; """
+        order_qt_sql """ select * from ${indexTbName} where c match 'to'; """
 
-        qt_sql """ select * from ${indexTbName} where b match_phrase 'like a 
box'; """
-        qt_sql """ select * from ${indexTbName} where c match_phrase 'like a 
box'; """
+        order_qt_sql """ select * from ${indexTbName} where b match_phrase 
'like a box'; """
+        order_qt_sql """ select * from ${indexTbName} where c match_phrase 
'like a box'; """
 
     } finally {
         //try_sql("DROP TABLE IF EXISTS ${testTable}")
diff --git a/regression-test/suites/search/test_search_dsl_syntax.groovy 
b/regression-test/suites/search/test_search_dsl_syntax.groovy
index 5a4d9d4729a..7b1293ab876 100644
--- a/regression-test/suites/search/test_search_dsl_syntax.groovy
+++ b/regression-test/suites/search/test_search_dsl_syntax.groovy
@@ -92,16 +92,16 @@ suite("test_search_dsl_syntax", "p0") {
     qt_sql "SELECT /*+SET_VAR(enable_segment_limit_pushdown=true) */ id, title 
FROM ${tableName} WHERE search('title:Machine') ORDER BY id"
 
     // DSL Syntax Test 2: Phrase queries with quotes
-    qt_sql "SELECT /*+SET_VAR(enable_segment_limit_pushdown=true) */ id, title 
FROM ${tableName} WHERE search('title:\"Machine Learning\"')"
+    qt_sql "SELECT /*+SET_VAR(enable_segment_limit_pushdown=true) */ id, title 
FROM ${tableName} WHERE search('title:\"Machine Learning\"') ORDER BY id"
 
     // DSL Syntax Test 3: Wildcard queries
     qt_sql "SELECT /*+SET_VAR(enable_segment_limit_pushdown=true) */ id, title 
FROM ${tableName} WHERE search('title:*Learning') ORDER BY id"
 
     // DSL Syntax Test 4: Prefix queries
-    qt_sql "SELECT /*+SET_VAR(enable_segment_limit_pushdown=true) */ id, title 
FROM ${tableName} WHERE search('title:Data*')"
+    qt_sql "SELECT /*+SET_VAR(enable_segment_limit_pushdown=true) */ id, title 
FROM ${tableName} WHERE search('title:Data*') ORDER BY id"
 
     // DSL Syntax Test 5: Regular expression queries
-    qt_sql "SELECT /*+SET_VAR(enable_segment_limit_pushdown=true) */ id, title 
FROM ${tableName} WHERE search('title:/.*[Dd]ata.*/')"
+    qt_sql "SELECT /*+SET_VAR(enable_segment_limit_pushdown=true) */ id, title 
FROM ${tableName} WHERE search('title:/.*[Dd]ata.*/') ORDER BY id"
 
     // DSL Syntax Test 6: Boolean AND operator
     qt_sql "SELECT /*+SET_VAR(enable_segment_limit_pushdown=true) */ id, title 
FROM ${tableName} WHERE search('category:Technology AND tags:AI') ORDER BY id"


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to