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

yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new b2eb3c90469 branch-4.2: [fix](be) Score regexp and wildcard patterns 
with a constant like Elasticsearch #69815 (#69922)
b2eb3c90469 is described below

commit b2eb3c90469d4352670c7573b96f81a96b092e1c
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Nov 2 08:51:30 2026 +0800

    branch-4.2: [fix](be) Score regexp and wildcard patterns with a constant 
like Elasticsearch #69815 (#69922)
    
    Cherry-picked from #69815
    
    Co-authored-by: Jack <[email protected]>
---
 be/src/storage/index/index_reader_helper.cpp       |  10 ++
 .../query/engine/expand_query/expand_query.h       |   5 +-
 .../query/engine/expand_query/expand_weight.cpp    |  69 +-----------
 .../query/engine/expand_query/expand_weight.h      |   9 +-
 .../index/inverted/reader/predicate_collector.cpp  |   5 +
 .../exprs/function/search/function_search_test.cpp |  23 ++--
 .../inverted/query/engine/prefix_query_test.cpp    |  23 ----
 .../inverted/query/engine/regexp_query_test.cpp    |  48 ++------
 .../inverted/query/engine/wildcard_query_test.cpp  |  54 ++-------
 .../inverted/reader/collection_statistics_test.cpp |  46 ++++++++
 .../storage/segment/index_reader_helper_test.cpp   |  26 +++++
 .../search/test_search_score_pattern_constant.out  | 124 +++++++++++++++++++++
 .../test_search_score_pattern_constant.groovy      |  80 +++++++++++++
 13 files changed, 325 insertions(+), 197 deletions(-)

diff --git a/be/src/storage/index/index_reader_helper.cpp 
b/be/src/storage/index/index_reader_helper.cpp
index 51ed6baa91d..efd92d9b2d3 100644
--- a/be/src/storage/index/index_reader_helper.cpp
+++ b/be/src/storage/index/index_reader_helper.cpp
@@ -35,6 +35,12 @@ bool can_score_match(const TabletIndex* index_meta) {
 
 bool IndexReaderHelper::is_need_similarity_score(InvertedIndexQueryType 
query_type,
                                                  const TabletIndex* 
index_meta) {
+    // A regexp scores a constant 1 per matching row, as an Elasticsearch 
regexp query does in
+    // query context. The constant reads no statistics, norms or positions, so 
every index can
+    // score it: body MATCH 'apple' AND body MATCH_REGEXP 'ap.*' adds 1 to the 
apple score.
+    if (query_type == InvertedIndexQueryType::MATCH_REGEXP_QUERY) {
+        return true;
+    }
     return (query_type == InvertedIndexQueryType::MATCH_ANY_QUERY ||
             query_type == InvertedIndexQueryType::MATCH_ALL_QUERY ||
             query_type == InvertedIndexQueryType::MATCH_PHRASE_QUERY ||
@@ -44,6 +50,10 @@ bool 
IndexReaderHelper::is_need_similarity_score(InvertedIndexQueryType query_ty
 
 bool IndexReaderHelper::is_need_similarity_score(TExprOpcode::type query_type,
                                                  const TabletIndex* 
index_meta) {
+    // See the InvertedIndexQueryType overload: a regexp scores a constant on 
every index.
+    if (query_type == TExprOpcode::MATCH_REGEXP) {
+        return true;
+    }
     return (query_type == TExprOpcode::MATCH_ANY || query_type == 
TExprOpcode::MATCH_ALL ||
             query_type == TExprOpcode::MATCH_PHRASE ||
             query_type == TExprOpcode::MATCH_PHRASE_PREFIX) &&
diff --git 
a/be/src/storage/index/inverted/query/engine/expand_query/expand_query.h 
b/be/src/storage/index/inverted/query/engine/expand_query/expand_query.h
index 155acf16c73..45816618fc9 100644
--- a/be/src/storage/index/inverted/query/engine/expand_query/expand_query.h
+++ b/be/src/storage/index/inverted/query/engine/expand_query/expand_query.h
@@ -38,8 +38,9 @@ public:
               _pattern(std::move(pattern)) {}
     ~ExpandQuery() override = default;
 
-    WeightPtr weight(bool enable_scoring) override {
-        return std::make_shared<ExpandWeight>(_context, _field, _kind, 
_pattern, enable_scoring);
+    // Scores are constant, so scoring changes nothing.
+    WeightPtr weight(bool /*enable_scoring*/) override {
+        return std::make_shared<ExpandWeight>(_context, _field, _kind, 
_pattern);
     }
 
 private:
diff --git 
a/be/src/storage/index/inverted/query/engine/expand_query/expand_weight.cpp 
b/be/src/storage/index/inverted/query/engine/expand_query/expand_weight.cpp
index 85fd10a51d0..3c55f171ec4 100644
--- a/be/src/storage/index/inverted/query/engine/expand_query/expand_weight.cpp
+++ b/be/src/storage/index/inverted/query/engine/expand_query/expand_weight.cpp
@@ -17,7 +17,6 @@
 
 #include "storage/index/inverted/query/engine/expand_query/expand_weight.h"
 
-#include <cmath>
 #include <memory>
 #include <roaring/roaring.hh>
 #include <vector>
@@ -26,14 +25,10 @@
 #include "storage/index/inverted/query/engine/bit_set_query/bit_set_scorer.h"
 #include 
"storage/index/inverted/query/engine/const_score_query/const_score_scorer.h"
 #include "storage/index/inverted/query/engine/nullable_scorer.h"
-#include "storage/index/inverted/query/engine/score_combiner.h"
-#include "storage/index/inverted/query/engine/term_query/term_scorer.h"
-#include "storage/index/inverted/query/engine/union/buffered_union.h"
 #include "storage/index/inverted/query/exec/block_doc_set.h"
 #include "storage/index/inverted/query/exec/collect_postings.h"
 #include "storage/index/inverted/query/exec/roaring_docid_sink.h"
 #include "storage/index/inverted/query/exec/term_waves.h"
-#include "storage/index/inverted/query/scoring/bm25_similarity.h"
 #include "storage/index/inverted/query/spi/postings_cursor.h"
 
 namespace doris::segment_v2::inverted_index::query_v2 {
@@ -63,15 +58,11 @@ Status collect_expanded_rows(index_query::IndexSource& 
source, index_query::Term
 }
 
 ExpandWeight::ExpandWeight(IndexQueryContextPtr context, std::wstring field,
-                           index_query::TermPatternKind kind, std::string 
pattern,
-                           bool enable_scoring)
+                           index_query::TermPatternKind kind, std::string 
pattern)
         : _context(std::move(context)),
           _field(std::move(field)),
           _kind(kind),
-          _pattern(std::move(pattern)),
-          // Prefix and phrase-edge expansions retain their constant-score 
behavior.
-          _enable_scoring(enable_scoring && (kind == 
index_query::TermPatternKind::kRegexp ||
-                                             kind == 
index_query::TermPatternKind::kWildcard)) {}
+          _pattern(std::move(pattern)) {}
 
 std::shared_ptr<roaring::Roaring> ExpandWeight::_rows(const 
QueryExecutionContext& context,
                                                       const std::string& 
binding_key,
@@ -94,68 +85,12 @@ std::shared_ptr<roaring::Roaring> ExpandWeight::_rows(const 
QueryExecutionContex
 
 ScorerPtr ExpandWeight::scorer(const QueryExecutionContext& context,
                                const std::string& binding_key) {
-    // The listing fast path discards term frequencies, so scored patterns 
need term scorers.
-    if (_enable_scoring) {
-        return make_nullable_scorer(_scored_terms(context, binding_key),
-                                    logical_field_or_fallback(context, 
binding_key, _field),
-                                    context.null_resolver);
-    }
     auto scorer = std::make_shared<ConstScoreScorer<BitSetScorerPtr>>(
             std::make_shared<BitSetScorer>(_rows(context, binding_key, 
nullptr)));
     return make_nullable_scorer(scorer, logical_field_or_fallback(context, 
binding_key, _field),
                                 context.null_resolver);
 }
 
-ScorerPtr ExpandWeight::_scored_terms(const QueryExecutionContext& context,
-                                      const std::string& binding_key) {
-    index_query::TermPattern pattern;
-    THROW_IF_ERROR(index_query::TermPattern::create(_kind, _pattern, 
&pattern));
-    auto source = lookup_source(_field, context, binding_key);
-    // An unbound field has no postings and contributes no matches.
-    if (source == nullptr) {
-        return std::make_shared<EmptyScorer>();
-    }
-    std::vector<std::string> terms;
-    THROW_IF_ERROR(source->expand_terms(
-            pattern, index_query::expansion_limit(_kind, 
index_query::max_expansions(*_context)),
-            &terms));
-    // A valid pattern may match no dictionary terms.
-    if (terms.empty()) {
-        return std::make_shared<EmptyScorer>();
-    }
-    // Expansion terms are absent from collection statistics; use the bound 
source's statistics.
-    index_query::IndexScoringStats stats;
-    THROW_IF_ERROR(source->scoring_stats(&stats));
-    DORIS_CHECK_GT(stats.doc_count, 0);
-    // Docs-only SNII still reports its physical token count. Only legacy 
sources without
-    // that metadata report zero, so use unit average length only for those 
sources.
-    const uint64_t token_count = stats.token_count != 0 ? stats.token_count : 
stats.doc_count;
-    const float avgdl = static_cast<float>(token_count) / stats.doc_count;
-    DORIS_CHECK_GT(avgdl, 0.0F);
-    std::vector<ScorerPtr> scorers;
-    scorers.reserve(terms.size());
-    auto logical_field = logical_field_or_fallback(context, binding_key, 
_field);
-    for (const auto& term : terms) {
-        uint64_t doc_freq = 0;
-        THROW_IF_ERROR(source->doc_freq(term, &doc_freq));
-        // The immutable source just enumerated this term and must have its 
postings.
-        DCHECK_GT(doc_freq, 0);
-        const auto idf = static_cast<float>(std::log1p(
-                (static_cast<double>(stats.doc_count) - doc_freq + 0.5) / 
(doc_freq + 0.5)));
-        auto similarity = std::make_shared<BM25Similarity>(idf, avgdl);
-        auto postings = open_postings(*source, term, /*positions=*/false, 
/*enable_scoring=*/true,
-                                      similarity);
-        DCHECK(postings != nullptr);
-        scorers.emplace_back(std::make_shared<TermScorer>(std::move(postings),
-                                                          
std::move(similarity), logical_field));
-    }
-    // One expanded term needs no union; multiple terms add their per-document 
BM25 scores.
-    if (scorers.size() == 1) {
-        return std::move(scorers.front());
-    }
-    return make_buffered_union(scorers, std::make_shared<SumCombiner>());
-}
-
 index_query::TruthSet ExpandWeight::listed_rows(const QueryExecutionContext& 
context,
                                                 const std::string& binding_key,
                                                 const roaring::Roaring* 
candidates) {
diff --git 
a/be/src/storage/index/inverted/query/engine/expand_query/expand_weight.h 
b/be/src/storage/index/inverted/query/engine/expand_query/expand_weight.h
index 3bbe43531c3..b5e979aca5d 100644
--- a/be/src/storage/index/inverted/query/engine/expand_query/expand_weight.h
+++ b/be/src/storage/index/inverted/query/engine/expand_query/expand_weight.h
@@ -29,25 +29,23 @@ Status collect_expanded_rows(index_query::IndexSource& 
source, index_query::Term
                              int32_t max_expansions, const roaring::Roaring* 
candidates,
                              roaring::Roaring* rows);
 
-// Every document that holds an expanded term; scored regexp and wildcard 
clauses sum BM25.
+// Every document that holds a term the pattern expands to, with a constant 
score.
 class ExpandWeight : public Weight {
 public:
     ExpandWeight(IndexQueryContextPtr context, std::wstring field,
-                 index_query::TermPatternKind kind, std::string pattern,
-                 bool enable_scoring = false);
+                 index_query::TermPatternKind kind, std::string pattern);
     ~ExpandWeight() override = default;
 
     ScorerPtr scorer(const QueryExecutionContext& context, const std::string& 
binding_key) override;
 
     bool lists_rows(const QueryExecutionContext&, const std::string&) const 
override {
-        return !_enable_scoring;
+        return true;
     }
     index_query::TruthSet listed_rows(const QueryExecutionContext& context,
                                       const std::string& binding_key,
                                       const roaring::Roaring* candidates) 
override;
 
 private:
-    ScorerPtr _scored_terms(const QueryExecutionContext& context, const 
std::string& binding_key);
     std::shared_ptr<roaring::Roaring> _rows(const QueryExecutionContext& 
context,
                                             const std::string& binding_key,
                                             const roaring::Roaring* 
candidates);
@@ -55,7 +53,6 @@ private:
     std::wstring _field;
     index_query::TermPatternKind _kind;
     std::string _pattern;
-    bool _enable_scoring;
 };
 
 } // namespace doris::segment_v2::inverted_index::query_v2
diff --git a/be/src/storage/index/inverted/reader/predicate_collector.cpp 
b/be/src/storage/index/inverted/reader/predicate_collector.cpp
index 376e7af5604..3d0b4fbbed6 100644
--- a/be/src/storage/index/inverted/reader/predicate_collector.cpp
+++ b/be/src/storage/index/inverted/reader/predicate_collector.cpp
@@ -369,6 +369,11 @@ Status MatchPredicateCollector::collect(RuntimeState* 
state, const TabletSchemaS
         !IndexReaderHelper::is_need_similarity_score(expr->op(), index_meta)) {
         return Status::OK();
     }
+    // A regexp scores a constant and reads no statistics. Its value is a 
pattern, not text, so
+    // analyzing it would collect statistics for made-up terms: 'ap.*' would 
become "ap".
+    if (expr->op() == TExprOpcode::MATCH_REGEXP) {
+        return Status::OK();
+    }
 
     DORIS_CHECK(analyzer_ctx->analyzer_provider != nullptr);
     auto options = DataTypeSerDe::get_default_format_options();
diff --git a/be/test/exprs/function/search/function_search_test.cpp 
b/be/test/exprs/function/search/function_search_test.cpp
index cdee045e3c2..589104c3cc9 100644
--- a/be/test/exprs/function/search/function_search_test.cpp
+++ b/be/test/exprs/function/search/function_search_test.cpp
@@ -40,7 +40,6 @@
 #include <gtest/gtest.h>
 
 #include <chrono>
-#include <cmath>
 #include <initializer_list>
 #include <map>
 #include <memory>
@@ -1888,8 +1887,6 @@ TEST_F(FunctionSearchTest, 
TestBuildLeafQueryExecutesSelectedSniiWildcardReader)
             std::make_shared<RecordingNativeInvertedIndexReader>(&decoy_meta, 
decoy_file_reader);
     auto selected_reader = 
std::make_shared<RecordingNativeInvertedIndexReader>(
             &selected_meta, selected_file_reader);
-    selected_reader->fake_source->set_doc_count(4);
-    selected_reader->fake_source->set_scoring_stats({.doc_count = 4, 
.token_count = 3});
     selected_reader->fake_source->add("alpha", {0, 2});
     selected_reader->fake_source->add("gamma", {1});
     decoy_reader->fake_source->add("alpha", {1});
@@ -1938,8 +1935,7 @@ TEST_F(FunctionSearchTest, 
TestBuildLeafQueryExecutesSelectedSniiWildcardReader)
                                  binding_key);
     ASSERT_NE(nullptr, scorer);
     EXPECT_EQ(0U, scorer->doc());
-    // The source has three tokens over four rows, even without stored norms.
-    EXPECT_NEAR(0.88F * std::log(2.0F), scorer->score(), 1e-6F);
+    EXPECT_FLOAT_EQ(1.0F, scorer->score());
     expect_bitmap_eq(collect_docs(scorer), {0, 2});
     // The pattern was lowercased by the selected analyzer and enumerated on 
the selected
     // index only; a leading wildcard has no prefix to enumerate from.
@@ -2533,8 +2529,7 @@ static void search_snii_pattern(const 
std::map<std::string, std::string>& proper
     ASSERT_TRUE(status.ok()) << status.to_string();
     EXPECT_EQ(0, reader->query_calls) << value;
     // The scorer enumerates the stem on the source; the reader itself is 
never asked.
-    // This helper checks normalization and routing, without requesting 
scoring statistics.
-    auto weight = query->weight(false);
+    auto weight = query->weight(true);
     ASSERT_NE(nullptr, weight);
     auto scorer = 
weight->scorer(build_variant_search_query_execution_context(4, resolver, 
nullptr),
                                  binding_key);
@@ -2767,12 +2762,13 @@ TEST_F(FunctionSearchTest, 
TestSniiScoredLeafRanksByTheEngineBm25) {
     }
 }
 
-// Scored wildcard leaves read frequencies and statistics through the unified 
source,
-// even when the old reader query API publishes no per-document scores.
-TEST_F(FunctionSearchTest, TestSniiNativeWildcardUsesSourceScoringStatistics) {
+// A leaf for which the reader publishes no per-document score must keep the 
constant that the
+// CLucene path also gives its own constant-score leaves, so that routing 
scored clauses through a
+// new scorer does not quietly re-rank the unscored ones. The fake reader is 
what withholds the
+// scores here, so this pins the "nothing published -> BitSetQuery -> 1.0" 
behaviour; it does not
+// pin which query types the production is_need_similarity_score gate rejects.
+TEST_F(FunctionSearchTest, 
TestSniiNativeLeafWithoutPublishedScoresKeepsConstantScore) {
     SniiScoringFixture fixture(43, 4);
-    fixture.reader->fake_source->set_doc_count(4);
-    fixture.reader->fake_source->set_scoring_stats({.doc_count = 4, 
.token_count = 2});
     fixture.reader->fake_source->add("alpha", {0, 2});
 
     inverted_index::query_v2::QueryPtr query;
@@ -2789,8 +2785,7 @@ TEST_F(FunctionSearchTest, 
TestSniiNativeWildcardUsesSourceScoringStatistics) {
     auto scorer = weight->scorer(exec_ctx, binding_key);
     ASSERT_NE(nullptr, scorer);
     EXPECT_EQ(0U, scorer->doc());
-    // avgdl = 2/4 and implicit length = 1: BM25 is idf * 2.2 / 3.1.
-    EXPECT_NEAR((2.2F / 3.1F) * std::log(2.0F), scorer->score(), 1e-6F);
+    EXPECT_FLOAT_EQ(1.0F, scorer->score());
     expect_bitmap_eq(collect_docs(scorer), {0, 2});
 }
 
diff --git a/be/test/storage/index/inverted/query/engine/prefix_query_test.cpp 
b/be/test/storage/index/inverted/query/engine/prefix_query_test.cpp
index e711deb3ed9..4d68efdf47a 100644
--- a/be/test/storage/index/inverted/query/engine/prefix_query_test.cpp
+++ b/be/test/storage/index/inverted/query/engine/prefix_query_test.cpp
@@ -18,7 +18,6 @@
 #include <CLucene.h>
 #include <gtest/gtest.h>
 
-#include <cmath>
 #include <memory>
 #include <roaring/roaring.hh>
 #include <string>
@@ -33,8 +32,6 @@
 #include "storage/index/inverted/inverted_index_reader.h"
 #include "storage/index/inverted/query/engine/expand_query/expand_query.h"
 #include "storage/index/inverted/query/logical/node.h"
-#include "storage/index/inverted/query/scoring/bm25_similarity.h"
-#include "storage/index/inverted/query/scoring/collection_similarity.h"
 #include "storage/index/inverted/query/spi/fake_index_source.h"
 #include "storage/olap_common.h"
 
@@ -495,8 +492,6 @@ TEST(ExpansionLeafTest, 
KeepsPatternLimitsAndLeavesRootCandidatesToTheCaller) {
         auto source = 
std::make_shared<index_query::testing::FakeIndexSource>();
         source->batches = batches;
         source->set_doc_count(10);
-        // Scored regexp and wildcard leaves need statistics from their bound 
source.
-        source->set_scoring_stats({.doc_count = 10, .token_count = 10});
         source->add("apple", {0, 4});
         source->add("application", {1});
         source->add("apply", {2, 4});
@@ -514,30 +509,12 @@ TEST(ExpansionLeafTest, 
KeepsPatternLimitsAndLeavesRootCandidatesToTheCaller) {
                 expected.add(2);
             }
             for (const bool scoring : {false, true}) {
-                context->collection_similarity =
-                        scoring ? std::make_shared<CollectionSimilarity>() : 
nullptr;
                 auto result = std::make_shared<roaring::Roaring>();
                 result->add(9);
                 const auto status = run_leaf(context, L"content", leaf, 
&candidates, scoring,
                                              source, source->doc_count(), 
result);
                 ASSERT_TRUE(status.ok()) << status;
                 EXPECT_EQ(*result, expected) << pattern << " scoring=" << 
scoring;
-                if (scoring) {
-                    const auto& scores = 
context->collection_similarity->_bm25_scores;
-                    EXPECT_EQ(scores.size(), expected.cardinality() - 1);
-                    const bool bm25 = kind == logical::ExpandKind::kWildcard ||
-                                      kind == logical::ExpandKind::kRegexp;
-                    for (const auto& [doc, score] : scores) {
-                        // Row 9 predates this leaf and must not acquire a 
score from it.
-                        EXPECT_NE(doc, 9U);
-                        EXPECT_TRUE(expected.contains(doc));
-                        const float df = doc == 1 ? 1.0F : 2.0F;
-                        const float idf = std::log1p((10.0F - df + 0.5F) / (df 
+ 0.5F));
-                        const float expected_score =
-                                bm25 ? BM25Similarity(idf, 1.0F).score(1.0F, 
1) : 1.0F;
-                        EXPECT_FLOAT_EQ(score, expected_score) << pattern << " 
doc=" << doc;
-                    }
-                }
             }
         }
     }
diff --git a/be/test/storage/index/inverted/query/engine/regexp_query_test.cpp 
b/be/test/storage/index/inverted/query/engine/regexp_query_test.cpp
index 9695f3da776..6de9c997661 100644
--- a/be/test/storage/index/inverted/query/engine/regexp_query_test.cpp
+++ b/be/test/storage/index/inverted/query/engine/regexp_query_test.cpp
@@ -49,15 +49,13 @@ public:
         st = io::global_local_filesystem()->create_directory(kTestDir);
         ASSERT_TRUE(st.ok()) << st;
         std::string field_name = "content";
-        create_test_index(field_name, kTestDir,
-                          {"apple123",     "apple456",     "banana789",
-                           "test123abc",   "pattern456",   "regex999",
-                           "match123",     "search456",    "prefix123",
-                           "suffix789",    "apple_test",   "banana_data",
-                           "test_pattern", "data_regex",   "apple_banana",
-                           "test_match",   "pattern_test", "prefix_suffix",
-                           "abc123xyz",    "def456ghi",    "apple789 apple789 
apple456"},
-                          "standard");
+        create_test_index(
+                field_name, kTestDir,
+                {"apple123",   "apple456",     "banana789",     "test123abc", 
"pattern456",
+                 "regex999",   "match123",     "search456",     "prefix123",  
"suffix789",
+                 "apple_test", "banana_data",  "test_pattern",  "data_regex", 
"apple_banana",
+                 "test_match", "pattern_test", "prefix_suffix", "abc123xyz",  
"def456ghi"},
+                "standard");
     }
 
     void TearDown() override {
@@ -181,36 +179,6 @@ TEST_F(RegexpQueryV2Test, test_regexp_query_with_scoring) {
     ASSERT_NE(regexp_weight, nullptr);
 }
 
-TEST_F(RegexpQueryV2Test, test_regexp_query_scores_matching_terms) {
-    auto context = std::make_shared<IndexQueryContext>();
-    context->collection_statistics = std::make_shared<CollectionStatistics>();
-    context->collection_similarity = std::make_shared<CollectionSimilarity>();
-
-    auto* dir = FSDirectory::getDirectory(kTestDir.c_str());
-    auto reader_holder = 
make_shared_reader(lucene::index::IndexReader::open(dir, true));
-    ASSERT_NE(reader_holder, nullptr);
-
-    std::wstring field = StringHelper::to_wstring("content");
-    auto query = std::make_shared<query_v2::ExpandQuery>(
-            context, field, index_query::TermPatternKind::kRegexp, 
anchored("apple.*"));
-    auto weight = query->weight(true);
-
-    query_v2::QueryExecutionContext exec_ctx;
-    exec_ctx.segment_num_rows = reader_holder->maxDoc();
-    exec_ctx.sources = {clucene_index_source(reader_holder, field, nullptr)};
-    exec_ctx.field_sources.emplace(field, exec_ctx.sources.front());
-
-    auto scorer = weight->scorer(exec_ctx);
-    ASSERT_EQ(scorer->seek(0), 0);
-    float single_term_score = scorer->score();
-    EXPECT_NE(single_term_score, 1.0F);
-    uint32_t last_doc = reader_holder->maxDoc() - 1;
-    ASSERT_EQ(scorer->seek(last_doc), last_doc);
-    EXPECT_GT(scorer->score(), single_term_score);
-
-    _CLDECDELETE(dir);
-}
-
 // Test regexp query execution
 TEST_F(RegexpQueryV2Test, test_regexp_query_execution) {
     auto context = std::make_shared<IndexQueryContext>();
@@ -659,7 +627,7 @@ TEST_F(RegexpQueryV2Test, test_whole_term_match_any_term) {
         doc = scorer->advance();
     }
 
-    EXPECT_EQ(result.cardinality(), 21);
+    EXPECT_EQ(result.cardinality(), 20);
 
     _CLDECDELETE(dir);
 }
diff --git 
a/be/test/storage/index/inverted/query/engine/wildcard_query_test.cpp 
b/be/test/storage/index/inverted/query/engine/wildcard_query_test.cpp
index 1822c79e9e5..15d1b143ad4 100644
--- a/be/test/storage/index/inverted/query/engine/wildcard_query_test.cpp
+++ b/be/test/storage/index/inverted/query/engine/wildcard_query_test.cpp
@@ -58,20 +58,14 @@ public:
 
 private:
     void create_test_index(const std::string& field_name, const std::string& 
dir) {
-        std::vector<std::string> test_data = {"apple",     "application", 
"apply",
-                                              "apricot",   "banana",      
"band",
-                                              "bandana",   "can",         
"candy",
-                                              "cat",       "catalog",     
"dog",
-                                              "document",  "door",        
"hello",
-                                              "help",      "helpful",     
"test",
-                                              "testing",   "testcase",    
"wildcard",
-                                              "wild",      "wilderness",  
"card",
-                                              "cardboard", "abc",         
"abcd",
-                                              "abcde",     "prefix123",   
"123suffix",
-                                              "pre123",    "123suf",      
"both123both",
-                                              "star",      "start",       
"started",
-                                              "starter",   "question",    
"quest",
-                                              "query",     "queries",     
"test test testing"};
+        std::vector<std::string> test_data = {
+                "apple",     "application", "apply",       "apricot", 
"banana",     "band",
+                "bandana",   "can",         "candy",       "cat",     
"catalog",    "dog",
+                "document",  "door",        "hello",       "help",    
"helpful",    "test",
+                "testing",   "testcase",    "wildcard",    "wild",    
"wilderness", "card",
+                "cardboard", "abc",         "abcd",        "abcde",   
"prefix123",  "123suffix",
+                "pre123",    "123suf",      "both123both", "star",    "start", 
     "started",
+                "starter",   "question",    "quest",       "query",   
"queries"};
 
         CustomAnalyzerConfig::Builder builder;
         builder.with_tokenizer_config("standard", {});
@@ -164,36 +158,6 @@ TEST_F(WildcardQueryV2Test, 
test_wildcard_query_with_scoring) {
     ASSERT_NE(wildcard_weight, nullptr);
 }
 
-TEST_F(WildcardQueryV2Test, test_wildcard_query_scores_matching_terms) {
-    auto context = std::make_shared<IndexQueryContext>();
-    context->collection_statistics = std::make_shared<CollectionStatistics>();
-    context->collection_similarity = std::make_shared<CollectionSimilarity>();
-
-    auto* dir = FSDirectory::getDirectory(kTestDir.c_str());
-    auto reader_holder = 
make_shared_reader(lucene::index::IndexReader::open(dir, true));
-    ASSERT_NE(reader_holder, nullptr);
-
-    std::wstring field = StringHelper::to_wstring("field");
-    auto query = std::make_shared<query_v2::ExpandQuery>(
-            context, field, index_query::TermPatternKind::kWildcard, "test*");
-    auto weight = query->weight(true);
-
-    query_v2::QueryExecutionContext exec_ctx;
-    exec_ctx.segment_num_rows = reader_holder->maxDoc();
-    exec_ctx.sources = {clucene_index_source(reader_holder, field, nullptr)};
-    exec_ctx.field_sources.emplace(field, exec_ctx.sources.front());
-
-    auto scorer = weight->scorer(exec_ctx);
-    ASSERT_EQ(scorer->seek(17), 17);
-    float single_term_score = scorer->score();
-    EXPECT_NE(single_term_score, 1.0F);
-    uint32_t last_doc = reader_holder->maxDoc() - 1;
-    ASSERT_EQ(scorer->seek(last_doc), last_doc);
-    EXPECT_GT(scorer->score(), single_term_score);
-
-    _CLDECDELETE(dir);
-}
-
 // Test wildcard query with asterisk prefix pattern
 TEST_F(WildcardQueryV2Test, test_wildcard_query_prefix_pattern) {
     auto context = std::make_shared<IndexQueryContext>();
@@ -575,4 +539,4 @@ TEST_F(WildcardQueryV2Test, 
test_wildcard_query_move_semantics) {
     ASSERT_NE(weight2, nullptr);
 }
 
-} // namespace doris::segment_v2
+} // namespace doris::segment_v2
\ No newline at end of file
diff --git 
a/be/test/storage/index/inverted/reader/collection_statistics_test.cpp 
b/be/test/storage/index/inverted/reader/collection_statistics_test.cpp
index afabc73b9d7..8eef09fc51b 100644
--- a/be/test/storage/index/inverted/reader/collection_statistics_test.cpp
+++ b/be/test/storage/index/inverted/reader/collection_statistics_test.cpp
@@ -2515,6 +2515,52 @@ TEST_F(CollectionStatisticsTest, 
CollectSkipsIndexWithoutSimilarityScore) {
     EXPECT_TRUE(collect_infos.empty());
 }
 
+// A regexp scores a constant, so it collects no statistics even on an index 
whose MATCH_ANY does:
+// analyzing its pattern 'ap.*' would register the made-up term "ap".
+TEST_F(CollectionStatisticsTest, RegexpCollectsNoStatistics) {
+    auto tablet_schema = std::make_shared<TabletSchema>();
+
+    constexpr int32_t kColUid = 1360;
+    TabletColumn col;
+    col.set_unique_id(kColUid);
+    col.set_name("body");
+    col.set_type(FieldType::OLAP_FIELD_TYPE_STRING);
+    tablet_schema->append_column(col);
+
+    TabletIndexPB index_pb;
+    index_pb.set_index_id(2360);
+    index_pb.set_index_name("body_idx");
+    index_pb.set_index_type(IndexType::INVERTED);
+    index_pb.add_col_unique_id(kColUid);
+    (*index_pb.mutable_properties())["parser"] = "english";
+    (*index_pb.mutable_properties())["support_phrase"] = "true";
+    TabletIndex index;
+    index.init_from_pb(index_pb);
+    tablet_schema->append_index(std::move(index));
+
+    constexpr int kSlotId = 86;
+    runtime_state_->_mock_desc_tbl->add_slot_descriptor(SlotId(kSlotId), 
kColUid, "body", {});
+
+    auto match_expr = 
std::make_shared<collection_statistics::MockVExpr>(TExprNodeType::MATCH_PRED);
+    match_expr->_children.push_back(
+            std::make_shared<collection_statistics::MockVSlotRef>("body", 
SlotId(kSlotId)));
+    
match_expr->_children.push_back(std::make_shared<collection_statistics::MockVLiteral>("ap.*"));
+
+    MatchPredicateCollector collector;
+    std::unordered_map<std::wstring, CollectInfo> collect_infos;
+    match_expr->set_opcode(TExprOpcode::MATCH_ANY);
+    auto status =
+            collector.collect(runtime_state_.get(), tablet_schema, match_expr, 
&collect_infos);
+    ASSERT_TRUE(status.ok()) << status.msg();
+    EXPECT_EQ(collect_infos.size(), 1U);
+
+    collect_infos.clear();
+    match_expr->set_opcode(TExprOpcode::MATCH_REGEXP);
+    status = collector.collect(runtime_state_.get(), tablet_schema, 
match_expr, &collect_infos);
+    ASSERT_TRUE(status.ok()) << status.msg();
+    EXPECT_TRUE(collect_infos.empty());
+}
+
 TEST_F(CollectionStatisticsTest, 
CollectPreservesLogicalClauseShapesForSameFieldName) {
     auto tablet_schema = std::make_shared<TabletSchema>();
 
diff --git a/be/test/storage/segment/index_reader_helper_test.cpp 
b/be/test/storage/segment/index_reader_helper_test.cpp
index 509a5eb4067..2fb699529ec 100644
--- a/be/test/storage/segment/index_reader_helper_test.cpp
+++ b/be/test/storage/segment/index_reader_helper_test.cpp
@@ -421,6 +421,32 @@ TEST_F(IndexReaderHelperTest, 
IsNeedSimilarityScoreWithTExprOpcodeTest) {
     EXPECT_FALSE(IndexReaderHelper::is_need_similarity_score(TExprOpcode::LT, 
&index_meta));
 }
 
+TEST_F(IndexReaderHelperTest, RegexpScoresConstantOnEveryIndex) {
+    // A regexp scores a constant 1 and reads no statistics, so neither the 
analyzer nor phrase
+    // support decides whether it scores.
+    for (const auto& properties : std::vector<std::map<std::string, 
std::string>> {
+                 {},
+                 {{"parser", "none"}},
+                 {{"parser", "english"}},
+                 {{"parser", "english"}, {"support_phrase", "true"}},
+                 {{"normalizer", "split_norm"}}}) {
+        SCOPED_TRACE(::testing::PrintToString(properties));
+        TabletIndexPB pb;
+        pb.set_index_id(1);
+        pb.set_index_name("regexp_index");
+        pb.set_index_type(IndexType::INVERTED);
+        pb.add_col_unique_id(1);
+        for (const auto& [key, value] : properties) {
+            (*pb.mutable_properties())[key] = value;
+        }
+        TabletIndex index;
+        index.init_from_pb(pb);
+        
EXPECT_TRUE(IndexReaderHelper::is_need_similarity_score(TExprOpcode::MATCH_REGEXP,
 &index));
+        EXPECT_TRUE(IndexReaderHelper::is_need_similarity_score(
+                InvertedIndexQueryType::MATCH_REGEXP_QUERY, &index));
+    }
+}
+
 TEST_F(IndexReaderHelperTest, MatchScoringRequiresAnalyzedPhraseIndex) {
     struct Case {
         std::map<std::string, std::string> properties;
diff --git a/regression-test/data/search/test_search_score_pattern_constant.out 
b/regression-test/data/search/test_search_score_pattern_constant.out
new file mode 100644
index 00000000000..e8603f53e4f
--- /dev/null
+++ b/regression-test/data/search/test_search_score_pattern_constant.out
@@ -0,0 +1,124 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !V2_search_regexp --
+2      1
+3      1
+6      1
+
+-- !V2_search_wildcard --
+2      1
+3      1
+6      1
+
+-- !V2_search_prefix --
+2      1
+3      1
+6      1
+
+-- !V2_search_keyword_regexp --
+6      1
+7      1
+
+-- !V2_match_regexp --
+2      1
+3      1
+6      1
+
+-- !V2_match_regexp_keyword --
+6      1
+7      1
+
+-- !V2_match_and_match_regexp --
+2      2.023573
+
+-- !V2_search_and_regexp --
+2      2.023573
+
+-- !V2_match_only --
+2      1.023573
+5      1.163151
+
+-- !V2_match_and_equality --
+2      1.023573
+
+-- !V3_search_regexp --
+2      1
+3      1
+6      1
+
+-- !V3_search_wildcard --
+2      1
+3      1
+6      1
+
+-- !V3_search_prefix --
+2      1
+3      1
+6      1
+
+-- !V3_search_keyword_regexp --
+6      1
+7      1
+
+-- !V3_match_regexp --
+2      1
+3      1
+6      1
+
+-- !V3_match_regexp_keyword --
+6      1
+7      1
+
+-- !V3_match_and_match_regexp --
+2      2.023573
+
+-- !V3_search_and_regexp --
+2      2.023573
+
+-- !V3_match_only --
+2      1.023573
+5      1.163151
+
+-- !V3_match_and_equality --
+2      1.023573
+
+-- !SNII_search_regexp --
+2      1
+3      1
+6      1
+
+-- !SNII_search_wildcard --
+2      1
+3      1
+6      1
+
+-- !SNII_search_prefix --
+2      1
+3      1
+6      1
+
+-- !SNII_search_keyword_regexp --
+6      1
+7      1
+
+-- !SNII_match_regexp --
+2      1
+3      1
+6      1
+
+-- !SNII_match_regexp_keyword --
+6      1
+7      1
+
+-- !SNII_match_and_match_regexp --
+2      2.023573
+
+-- !SNII_search_and_regexp --
+2      2.023573
+
+-- !SNII_match_only --
+2      1.023573
+5      1.163151
+
+-- !SNII_match_and_equality --
+2      1.023573
+
diff --git 
a/regression-test/suites/search/test_search_score_pattern_constant.groovy 
b/regression-test/suites/search/test_search_score_pattern_constant.groovy
new file mode 100644
index 00000000000..f3cf9615850
--- /dev/null
+++ b/regression-test/suites/search/test_search_score_pattern_constant.groovy
@@ -0,0 +1,80 @@
+// 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.
+
+// score() of pattern queries follows Elasticsearch's default rewrite: a 
regexp, wildcard or
+// prefix in SEARCH and a MATCH_REGEXP score a constant 1 per matching row, 
whatever segment the
+// row is in, and add exactly 1 next to a scored clause. An equality predicate 
adds nothing.
+suite("test_search_score_pattern_constant", "p0") {
+    // SEARCH and MATCH need index execution even when CI fuzzes these session 
defaults.
+    sql "set enable_common_expr_pushdown = true"
+    sql "set enable_inverted_index_query = true"
+
+    for (def storageFormat : ["V2", "V3", "SNII"]) {
+        sql "DROP TABLE IF EXISTS test_search_score_pattern_constant"
+        sql """
+            CREATE TABLE test_search_score_pattern_constant (
+                id BIGINT NOT NULL,
+                content STRING NULL,
+                tag STRING NULL,
+                INDEX idx_content(content) USING INVERTED
+                    PROPERTIES("parser" = "english", "support_phrase" = 
"true"),
+                INDEX idx_tag(tag) USING INVERTED
+            ) ENGINE=OLAP
+            DUPLICATE KEY(id)
+            DISTRIBUTED BY HASH(id) BUCKETS 1
+            PROPERTIES (
+                "replication_allocation" = "tag.location.default:1",
+                "inverted_index_storage_format" = "${storageFormat}",
+                "disable_auto_compaction" = "true"
+            )
+        """
+        // Two inserts make two segments with different term statistics; a 
constant score must not
+        // depend on them.
+        sql """INSERT INTO test_search_score_pattern_constant VALUES
+            (1, 'Apache Doris is a high performance analytical database', 
'doris'),
+            (2, 'Inverted indexes are suitable for full text search', 'index'),
+            (3, 'The parser splits text into multiple terms', 'parser'),
+            (4, 'Doris supports fast real time data analysis', 'doris'),
+            (5, 'Users can quickly search product names', 'search')
+        """
+        sql """INSERT INTO test_search_score_pattern_constant VALUES
+            (6, 'text text text', 'text'), (7, 'plain words only', 'textual')
+        """
+        sql "sync"
+
+        for (def query : [
+                ["search_regexp", "SEARCH('content:/.*ext/')"],
+                ["search_wildcard", "SEARCH('content:*ext')"],
+                ["search_prefix", "SEARCH('content:te*')"],
+                ["search_keyword_regexp", "SEARCH('tag:/text.*/')"],
+                ["match_regexp", "content MATCH_REGEXP 'ext'"],
+                ["match_regexp_keyword", "tag MATCH_REGEXP 'text'"],
+                // A pattern next to a scored clause adds exactly 1, as an 
Elasticsearch bool must
+                // with a regexp clause does.
+                ["match_and_match_regexp", "content MATCH 'search' AND content 
MATCH_REGEXP 'ext'"],
+                ["search_and_regexp", "SEARCH('content:search AND 
content:/.*ext/')"],
+                ["match_only", "content MATCH 'search'"],
+                ["match_and_equality", "content MATCH 'search' AND tag = 
'index'"]]) {
+            quickTest("${storageFormat}_${query[0]}", """
+                SELECT id, ROUND(s, 6) FROM (
+                    SELECT id, score() AS s FROM 
test_search_score_pattern_constant
+                    WHERE ${query[1]} ORDER BY s DESC LIMIT 10
+                ) hits ORDER BY id
+            """, true)
+        }
+    }
+}


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

Reply via email to