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 35b4fc18f0c [fix](be) Prevent score leakage across Boolean search 
branches (#69053)
35b4fc18f0c is described below

commit 35b4fc18f0cbd13a8316605c1024d62cad0fc34a
Author: Jack <[email protected]>
AuthorDate: Fri Oct 16 20:53:37 2026 +0800

    [fix](be) Prevent score leakage across Boolean search branches (#69053)
    
    ### What problem does this PR solve?
    
    Issue Number: N/A
    
    Related PR: #68758
    
    Problem Summary: Inverted-index leaves publish scores before an
    enclosing Boolean predicate knows which rows match. A row returned
    through one OR branch can receive scores from another branch whose AND
    condition failed, or from a negated term.
    
    Collect scores separately for each compound predicate and merge only
    scores for rows in its final positive bitmap. NOT discards its scores. A
    scored compound with incomplete index evaluation returns an explicit
    error. Defer restores the parent collector on every exit.
    
    This branch merges master and preserves its unified MATCH/SEARCH
    execution. The score collector and its includes now use
    `storage/index/inverted/query/scoring`; the obsolete reader-to-scorer
    hand-off documentation was removed. Existing Boolean evaluation changes
    and non-null ReadSchema test setup are retained.
    
    ### Elasticsearch comparison
    
    Before this master synchronization, an Elasticsearch 8.11.3 experiment
    used the regression suite's ten rows, one primary shard, the English
    analyzer, and default BM25. Equivalent `query_string` and `bool` +
    `match` queries returned the same scores. Eleven Doris query variants
    were compared by row id with the captured ES results, including
    independently measured leaves and branches, failed AND, reversed AND,
    and NOT.
    
    - An AND branch contributes only when the entire branch matches.
    - OR adds scores from its matching positive branches.
    - NOT contributes zero; rows returned only through the negative branch
    have zero score.
    - All eleven variants have the same matching ids and scores within an
    absolute tolerance of `1e-5` on this fixture.
    
    These are the [ES Boolean scoring
    
rules](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-bool-query).
    The result establishes equivalence for the tested Boolean trees and
    corpus. It does not establish universal cross-engine BM25 numeric
    compatibility. Doris also retains an explicit unsupported error for
    scored compounds that need row-level fallback after incomplete index
    evaluation.
---
 be/src/exprs/vcompound_pred.h                      |  33 ++-
 .../query/scoring/collection_similarity.cpp        |   9 +
 .../inverted/query/scoring/collection_similarity.h |  11 +
 be/test/exprs/vcompound_pred_score_test.cpp        | 226 +++++++++++++++++++++
 .../search/test_search_score_boolean_branches.out  |  60 ++++++
 .../test_search_score_boolean_branches.groovy      | 105 ++++++++++
 6 files changed, 442 insertions(+), 2 deletions(-)

diff --git a/be/src/exprs/vcompound_pred.h b/be/src/exprs/vcompound_pred.h
index fb7532e9590..793d912d3e3 100644
--- a/be/src/exprs/vcompound_pred.h
+++ b/be/src/exprs/vcompound_pred.h
@@ -20,6 +20,7 @@
 
 #include <algorithm>
 #include <cstdint>
+#include <memory>
 
 #include "common/logging.h"
 #include "common/status.h"
@@ -29,7 +30,9 @@
 #include "exprs/vectorized_fn_call.h"
 #include "exprs/vexpr_context.h"
 #include "exprs/vexpr_fwd.h"
+#include "storage/index/inverted/query/scoring/collection_similarity.h"
 #include "storage/index/zone_map/zonemap_eval_context.h"
+#include "util/defer_op.h"
 #include "util/simd/bits.h"
 
 namespace doris {
@@ -270,9 +273,24 @@ public:
         }
     }
 
+    // 
NOLINTNEXTLINE(readability-function-size,readability-function-cognitive-complexity):
 Keep each operator's bitmap and score updates together.
     Status evaluate_inverted_index(VExprContext* context, uint32_t 
segment_num_rows) override {
         segment_v2::InvertedIndexResultBitmap res;
         bool all_pass = true;
+        auto index_context = context->get_index_context();
+        auto query_context = index_context->get_index_query_context();
+        auto parent_similarity =
+                query_context != nullptr ? 
query_context->collection_similarity : nullptr;
+        CollectionSimilarityPtr branch_similarity;
+        if (parent_similarity != nullptr) {
+            branch_similarity = std::make_shared<CollectionSimilarity>();
+            query_context->collection_similarity = branch_similarity;
+        }
+        Defer restore_similarity {[&]() {
+            if (branch_similarity != nullptr) {
+                query_context->collection_similarity = parent_similarity;
+            }
+        }};
 
         switch (_op) {
         case TExprOpcode::COMPOUND_OR: {
@@ -296,7 +314,7 @@ public:
                     } else {
                         res |= *index_result;
                     }
-                    if (inverted_index_context->get_score_runtime() == 
nullptr) {
+                    if (branch_similarity == nullptr) {
                         if (res.get_data_bitmap()->cardinality() == 
segment_num_rows) {
                             break; // Early exit if result is full
                         }
@@ -362,8 +380,19 @@ public:
                     "Compound operator must be AND, OR, or NOT to execute with 
inverted index.");
         }
 
+        if (branch_similarity != nullptr && _op != TExprOpcode::COMPOUND_NOT) {
+            auto scores = branch_similarity->release_scores();
+            if (!scores.empty() && (!all_pass || res.is_empty())) {
+                return Status::Error<ErrorCode::INVERTED_INDEX_NOT_SUPPORTED>(
+                        "Cannot score a compound predicate with incomplete 
index evaluation");
+            }
+            if (all_pass && !res.is_empty()) {
+                DORIS_CHECK(res.get_data_bitmap() != nullptr);
+                parent_similarity->merge_scores(scores, 
*res.get_data_bitmap());
+            }
+        }
         if (all_pass && !res.is_empty()) {
-            context->get_index_context()->set_index_result_for_expr(this, res);
+            index_context->set_index_result_for_expr(this, res);
         }
         return Status::OK();
     }
diff --git 
a/be/src/storage/index/inverted/query/scoring/collection_similarity.cpp 
b/be/src/storage/index/inverted/query/scoring/collection_similarity.cpp
index 2386d7c17a0..6b65721bf28 100644
--- a/be/src/storage/index/inverted/query/scoring/collection_similarity.cpp
+++ b/be/src/storage/index/inverted/query/scoring/collection_similarity.cpp
@@ -26,6 +26,15 @@ void CollectionSimilarity::collect(segment_v2::rowid_t 
row_id, float score) {
     _bm25_scores[row_id] += score;
 }
 
+void CollectionSimilarity::merge_scores(const ScoreMap& scores,
+                                        const roaring::Roaring& matched_rows) {
+    for (const auto& [row_id, score] : scores) {
+        if (matched_rows.contains(row_id)) {
+            collect(row_id, score);
+        }
+    }
+}
+
 void CollectionSimilarity::get_bm25_scores(roaring::Roaring* row_bitmap,
                                            IColumn::MutablePtr& scores,
                                            
std::unique_ptr<std::vector<uint64_t>>& row_ids,
diff --git 
a/be/src/storage/index/inverted/query/scoring/collection_similarity.h 
b/be/src/storage/index/inverted/query/scoring/collection_similarity.h
index f3ed818be4f..068b2c4884d 100644
--- a/be/src/storage/index/inverted/query/scoring/collection_similarity.h
+++ b/be/src/storage/index/inverted/query/scoring/collection_similarity.h
@@ -49,6 +49,17 @@ public:
 
     void collect(segment_v2::rowid_t row_id, float score);
 
+    void merge_scores(const ScoreMap& scores, const roaring::Roaring& 
matched_rows);
+
+    // Moves a Boolean branch's scores to its parent without copying the 
per-row map.
+    ScoreMap release_scores() {
+        ScoreMap released = std::move(_bm25_scores);
+        // A moved-from flat_hash_map is valid but unspecified, not guaranteed 
empty, so make the
+        // "leaves this instance empty" half of the contract true rather than 
merely likely.
+        _bm25_scores.clear();
+        return released;
+    }
+
     void get_bm25_scores(roaring::Roaring* row_bitmap, IColumn::MutablePtr& 
scores,
                          std::unique_ptr<std::vector<uint64_t>>& row_ids,
                          const ScoreRangeFilterPtr& filter = nullptr) const;
diff --git a/be/test/exprs/vcompound_pred_score_test.cpp 
b/be/test/exprs/vcompound_pred_score_test.cpp
new file mode 100644
index 00000000000..87f3b0bac54
--- /dev/null
+++ b/be/test/exprs/vcompound_pred_score_test.cpp
@@ -0,0 +1,226 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <gtest/gtest.h>
+
+#include <cstdint>
+#include <initializer_list>
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "core/data_type/primitive_type.h"
+#include "exprs/score_runtime.h"
+#include "exprs/vcompound_pred.h"
+#include "exprs/vexpr_context.h"
+#include "storage/index/index_iterator.h"
+#include "storage/index/index_query_context.h"
+#include "storage/index/inverted/query/scoring/collection_similarity.h"
+#include "storage/schema.h"
+
+namespace doris {
+namespace {
+
+class ScoredBitmapExpr final : public VExpr {
+public:
+    ScoredBitmapExpr(std::initializer_list<uint32_t> rows, float score) : 
_score(score) {
+        set_node_type(TExprNodeType::MATCH_PRED);
+        for (uint32_t row : rows) {
+            _rows.add(row);
+        }
+    }
+
+    const std::string& expr_name() const override {
+        static const std::string name = "ScoredBitmapExpr";
+        return name;
+    }
+
+    Status execute(VExprContext*, Block*, int*) const override { return 
Status::OK(); }
+
+    Status execute_column_impl(VExprContext*, const Block*, const Selector*, 
size_t,
+                               ColumnPtr&) const override {
+        return Status::OK();
+    }
+
+    Status evaluate_inverted_index(VExprContext* context, uint32_t) override {
+        auto index_context = context->get_index_context();
+        auto query_context = index_context->get_index_query_context();
+        for (uint32_t row : _rows) {
+            query_context->collection_similarity->collect(row, _score);
+        }
+        index_context->set_index_result_for_expr(
+                this,
+                
segment_v2::InvertedIndexResultBitmap(std::make_shared<roaring::Roaring>(_rows),
+                                                      
std::make_shared<roaring::Roaring>()));
+        return Status::OK();
+    }
+
+private:
+    roaring::Roaring _rows;
+    float _score;
+};
+
+class UnindexedExpr final : public VExpr {
+public:
+    const std::string& expr_name() const override {
+        static const std::string name = "UnindexedExpr";
+        return name;
+    }
+
+    Status execute(VExprContext*, Block*, int*) const override { return 
Status::OK(); }
+
+    Status execute_column_impl(VExprContext*, const Block*, const Selector*, 
size_t,
+                               ColumnPtr&) const override {
+        return Status::OK();
+    }
+
+    Status evaluate_inverted_index(VExprContext*, uint32_t) override { return 
Status::OK(); }
+};
+
+std::shared_ptr<VCompoundPred> make_compound(TExprOpcode::type op, VExprSPtr 
left,
+                                             VExprSPtr right) {
+    TExprNode node;
+    node.__set_type(create_type_desc(PrimitiveType::TYPE_BOOLEAN));
+    node.__set_node_type(TExprNodeType::COMPOUND_PRED);
+    node.__set_opcode(op);
+    node.__set_num_children(2);
+    node.__set_is_nullable(false);
+    auto expr = VCompoundPred::create_shared(node);
+    expr->add_child(left);
+    expr->add_child(right);
+    return expr;
+}
+
+std::shared_ptr<VCompoundPred> make_not(VExprSPtr child) {
+    TExprNode node;
+    node.__set_type(create_type_desc(PrimitiveType::TYPE_BOOLEAN));
+    node.__set_node_type(TExprNodeType::COMPOUND_PRED);
+    node.__set_opcode(TExprOpcode::COMPOUND_NOT);
+    node.__set_num_children(1);
+    node.__set_is_nullable(false);
+    auto expr = VCompoundPred::create_shared(node);
+    expr->add_child(child);
+    return expr;
+}
+
+TEST(VCompoundPredScoreTest, FailedAndBranchDoesNotContributeToOrScore) {
+    auto first = make_compound(
+            TExprOpcode::COMPOUND_AND,
+            std::make_shared<ScoredBitmapExpr>(std::initializer_list<uint32_t> 
{1, 2, 4}, 1.0F),
+            std::make_shared<ScoredBitmapExpr>(std::initializer_list<uint32_t> 
{1, 2, 5}, 2.0F));
+    auto second = make_compound(
+            TExprOpcode::COMPOUND_AND,
+            std::make_shared<ScoredBitmapExpr>(std::initializer_list<uint32_t> 
{1, 2, 3, 4, 5},
+                                               3.0F),
+            std::make_shared<ScoredBitmapExpr>(std::initializer_list<uint32_t> 
{1, 3, 4, 5}, 4.0F));
+    auto root = make_compound(TExprOpcode::COMPOUND_OR, first, second);
+
+    std::vector<std::unique_ptr<segment_v2::IndexIterator>> iterators;
+    std::vector<IndexFieldNameAndTypePair> storage_types;
+    std::unordered_map<ColumnId, std::unordered_map<const VExpr*, bool>> 
status_map;
+    segment_v2::ColumnIteratorOptions iterator_options;
+    auto score_runtime = std::make_shared<ScoreRuntime>(nullptr, false, 0);
+    auto index_context = std::make_shared<IndexExecContext>(
+            iterators, storage_types, status_map, score_runtime, nullptr, 
iterator_options,
+            std::make_shared<ReadSchema>(std::vector<TabletColumnPtr> {}));
+    auto query_context = std::make_shared<segment_v2::IndexQueryContext>();
+    query_context->collection_similarity = 
std::make_shared<CollectionSimilarity>();
+    auto root_similarity = query_context->collection_similarity;
+    index_context->set_index_query_context(query_context);
+    auto expr_context = VExprContext::create_shared(root);
+    expr_context->set_index_context(index_context);
+
+    ASSERT_TRUE(root->evaluate_inverted_index(expr_context.get(), 6).ok());
+    const auto* result = index_context->get_index_result_for_expr(root.get());
+    ASSERT_NE(result, nullptr);
+    EXPECT_EQ(result->get_data_bitmap()->cardinality(), 5);
+    EXPECT_EQ(query_context->collection_similarity, root_similarity);
+
+    auto scores = root_similarity->release_scores();
+    ASSERT_EQ(scores.size(), 5);
+    EXPECT_FLOAT_EQ(scores.at(1), 10.0F);
+    EXPECT_FLOAT_EQ(scores.at(2), 3.0F);
+    EXPECT_FLOAT_EQ(scores.at(3), 7.0F);
+    EXPECT_FLOAT_EQ(scores.at(4), 7.0F);
+    EXPECT_FLOAT_EQ(scores.at(5), 7.0F);
+}
+
+TEST(VCompoundPredScoreTest, NotBranchDoesNotContributeToOrScore) {
+    auto negative = make_not(
+            std::make_shared<ScoredBitmapExpr>(std::initializer_list<uint32_t> 
{1, 2}, 1.0F));
+    auto positive =
+            std::make_shared<ScoredBitmapExpr>(std::initializer_list<uint32_t> 
{1, 3}, 5.0F);
+    auto root = make_compound(TExprOpcode::COMPOUND_OR, negative, positive);
+
+    std::vector<std::unique_ptr<segment_v2::IndexIterator>> iterators;
+    std::vector<IndexFieldNameAndTypePair> storage_types;
+    std::unordered_map<ColumnId, std::unordered_map<const VExpr*, bool>> 
status_map;
+    segment_v2::ColumnIteratorOptions iterator_options;
+    auto score_runtime = std::make_shared<ScoreRuntime>(nullptr, false, 0);
+    auto index_context = std::make_shared<IndexExecContext>(
+            iterators, storage_types, status_map, score_runtime, nullptr, 
iterator_options,
+            std::make_shared<ReadSchema>(std::vector<TabletColumnPtr> {}));
+    auto query_context = std::make_shared<segment_v2::IndexQueryContext>();
+    query_context->collection_similarity = 
std::make_shared<CollectionSimilarity>();
+    index_context->set_index_query_context(query_context);
+    auto expr_context = VExprContext::create_shared(root);
+    expr_context->set_index_context(index_context);
+
+    ASSERT_TRUE(root->evaluate_inverted_index(expr_context.get(), 4).ok());
+    const auto* result = index_context->get_index_result_for_expr(root.get());
+    ASSERT_NE(result, nullptr);
+    EXPECT_EQ(result->get_data_bitmap()->cardinality(), 3);
+    EXPECT_TRUE(result->get_data_bitmap()->contains(0));
+    EXPECT_TRUE(result->get_data_bitmap()->contains(1));
+    EXPECT_TRUE(result->get_data_bitmap()->contains(3));
+
+    auto scores = query_context->collection_similarity->release_scores();
+    ASSERT_EQ(scores.size(), 2);
+    EXPECT_FLOAT_EQ(scores.at(1), 5.0F);
+    EXPECT_FLOAT_EQ(scores.at(3), 5.0F);
+}
+
+TEST(VCompoundPredScoreTest, 
IncompleteScoredBranchReturnsErrorWithoutPublishingScores) {
+    auto root = make_compound(
+            TExprOpcode::COMPOUND_AND,
+            std::make_shared<ScoredBitmapExpr>(std::initializer_list<uint32_t> 
{1}, 1.0F),
+            std::make_shared<UnindexedExpr>());
+
+    std::vector<std::unique_ptr<segment_v2::IndexIterator>> iterators;
+    std::vector<IndexFieldNameAndTypePair> storage_types;
+    std::unordered_map<ColumnId, std::unordered_map<const VExpr*, bool>> 
status_map;
+    segment_v2::ColumnIteratorOptions iterator_options;
+    auto score_runtime = std::make_shared<ScoreRuntime>(nullptr, false, 0);
+    auto index_context = std::make_shared<IndexExecContext>(
+            iterators, storage_types, status_map, score_runtime, nullptr, 
iterator_options,
+            std::make_shared<ReadSchema>(std::vector<TabletColumnPtr> {}));
+    auto query_context = std::make_shared<segment_v2::IndexQueryContext>();
+    query_context->collection_similarity = 
std::make_shared<CollectionSimilarity>();
+    auto parent_similarity = query_context->collection_similarity;
+    index_context->set_index_query_context(query_context);
+    auto expr_context = VExprContext::create_shared(root);
+    expr_context->set_index_context(index_context);
+
+    auto status = root->evaluate_inverted_index(expr_context.get(), 2);
+    EXPECT_EQ(status.code(), ErrorCode::INVERTED_INDEX_NOT_SUPPORTED);
+    EXPECT_EQ(query_context->collection_similarity, parent_similarity);
+    EXPECT_TRUE(parent_similarity->release_scores().empty());
+}
+
+} // namespace
+} // namespace doris
diff --git a/regression-test/data/search/test_search_score_boolean_branches.out 
b/regression-test/data/search/test_search_score_boolean_branches.out
new file mode 100644
index 00000000000..ba8d7a888cb
--- /dev/null
+++ b/regression-test/data/search/test_search_score_boolean_branches.out
@@ -0,0 +1,60 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !search --
+1      1.3714
+10     1.2833
+2      0.8781
+4      0.4932
+5      0.4932
+6      1.5112
+7      0.4932
+9      1.5553
+
+-- !match --
+1      1.3714
+10     1.2833
+2      0.8781
+4      0.4932
+5      0.4932
+6      1.5112
+7      0.4932
+9      1.5553
+
+-- !match_warm --
+1      1.3714
+10     1.2833
+2      0.8781
+4      0.4932
+5      0.4932
+6      1.5112
+7      0.4932
+9      1.5553
+
+-- !failed_and --
+1      0.4932
+10     0.4052
+4      0.4932
+5      0.4932
+6      0.4932
+7      0.4932
+9      0.4932
+
+-- !failed_and_reversed --
+1      0.4932
+10     0.4052
+4      0.4932
+5      0.4932
+6      0.4932
+7      0.4932
+9      0.4932
+
+-- !not_branch --
+1      0.4932
+10     0.4052
+3      0
+4      0.4932
+5      0.4932
+6      0.4932
+7      0.4932
+8      0
+9      0.4932
+
diff --git 
a/regression-test/suites/search/test_search_score_boolean_branches.groovy 
b/regression-test/suites/search/test_search_score_boolean_branches.groovy
new file mode 100644
index 00000000000..87bfb8f4e2c
--- /dev/null
+++ b/regression-test/suites/search/test_search_score_boolean_branches.groovy
@@ -0,0 +1,105 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_search_score_boolean_branches", "p0") {
+    sql "DROP TABLE IF EXISTS test_search_score_multi_column_mixed"
+    sql """
+        CREATE TABLE test_search_score_multi_column_mixed (
+            id INT NOT NULL,
+            title STRING NULL,
+            content STRING NULL,
+            tags STRING NULL,
+            INDEX idx_title(title) USING INVERTED PROPERTIES("parser" = 
"english"),
+            INDEX idx_content(content) USING INVERTED PROPERTIES("parser" = 
"english"),
+            INDEX idx_tags(tags) USING INVERTED PROPERTIES("parser" = 
"english")
+        ) ENGINE=OLAP
+        DUPLICATE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES ("replication_allocation" = "tag.location.default:1")
+    """
+    sql """
+        INSERT INTO test_search_score_multi_column_mixed VALUES
+            (1, 'apple banana', 'search engine', 'database analytics'),
+            (2, 'apple guide', 'search basics', 'database'),
+            (3, 'banana manual', 'engine tuning', 'analytics'),
+            (4, 'apple banana', 'storage engine', 'database analytics'),
+            (5, 'pear guide', 'search engine', 'database analytics'),
+            (6, 'apple', 'engine search', 'analytics database'),
+            (7, 'banana', 'search', 'database analytics'),
+            (8, 'grape', 'storage', 'archive'),
+            (9, 'apple banana apple', 'search engine search', 'database 
analytics'),
+            (10, 'apple docs', 'search engine', 'database analytics archive')
+    """
+
+    order_qt_search """
+        SELECT id, ROUND(score(), 4) AS bm25_score
+        FROM test_search_score_multi_column_mixed
+        WHERE search('(title:apple AND content:search) OR (tags:database AND 
tags:analytics)',
+                     
'{"default_operator":"AND","mode":"lucene","minimum_should_match":0}')
+        ORDER BY score() DESC LIMIT 100
+    """
+
+    order_qt_match """
+        SELECT id, ROUND(score(), 4) AS bm25_score
+        FROM test_search_score_multi_column_mixed
+        WHERE (title MATCH_ANY 'apple' AND content MATCH_ANY 'search')
+           OR tags MATCH_ALL 'database analytics'
+        ORDER BY score() DESC LIMIT 100
+    """
+
+    order_qt_match_warm """
+        SELECT id, ROUND(score(), 4) AS bm25_score
+        FROM test_search_score_multi_column_mixed
+        WHERE (title MATCH_ANY 'apple' AND content MATCH_ANY 'search')
+           OR tags MATCH_ALL 'database analytics'
+        ORDER BY score() DESC LIMIT 100
+    """
+
+    order_qt_failed_and """
+        SELECT id, ROUND(score(), 4) AS bm25_score
+        FROM test_search_score_multi_column_mixed
+        WHERE (title MATCH_ANY 'apple' AND content MATCH_ANY 'missing_token')
+           OR tags MATCH_ALL 'database analytics'
+        ORDER BY score() DESC LIMIT 100
+    """
+
+    order_qt_failed_and_reversed """
+        SELECT id, ROUND(score(), 4) AS bm25_score
+        FROM test_search_score_multi_column_mixed
+        WHERE (content MATCH_ANY 'missing_token' AND title MATCH_ANY 'apple')
+           OR tags MATCH_ALL 'database analytics'
+        ORDER BY score() DESC LIMIT 100
+    """
+
+    order_qt_not_branch """
+        SELECT id, ROUND(score(), 4) AS bm25_score
+        FROM test_search_score_multi_column_mixed
+        WHERE NOT (title MATCH_ANY 'apple') OR tags MATCH_ALL 'database 
analytics'
+        ORDER BY score() DESC LIMIT 100
+    """
+
+    test {
+        sql """
+            SELECT id, score() AS bm25_score
+            FROM test_search_score_multi_column_mixed
+            WHERE (title MATCH_ANY 'apple' AND id > 0)
+               OR tags MATCH_ALL 'database analytics'
+            ORDER BY bm25_score DESC LIMIT 100
+        """
+        exception "Cannot score a compound predicate with incomplete index 
evaluation"
+    }
+}


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

Reply via email to