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 06b990bcd7d branch-4.1: [fix](inverted index) Share BM25 statistics
across parallel scanners of a tablet #69352 (#69418)
06b990bcd7d is described below
commit 06b990bcd7da49e0027e25ad45c289076d39ee0c
Author: guptashivam04 <[email protected]>
AuthorDate: Fri Oct 23 08:11:08 2026 +0530
branch-4.1: [fix](inverted index) Share BM25 statistics across parallel
scanners of a tablet #69352 (#69418)
### What problem does this PR solve?
Issue Number: None
Related PR: #69352
Problem Summary:
Backport of #69352 to branch-4.1.
A parallel scan (`ParallelScannerBuilder`) splits one tablet into
several scanners. Each `OlapScanner` then calls
`CollectionStatistics::collect()` with only the `rs_splits` of its own
split, so the BM25 doc count, avgdl and term doc frequencies come from a
subset of the tablet's rowsets. Rows with identical text get different
scores depending on which scanner reads them. Because the split depends
on the tablet's row count and the core count, a row's score and its rank
in `ORDER BY score()` change when unrelated rows are loaded, and
pagination over a score-ordered search returns repeated and missing
rows.
The reproduction in #69352 (8 rowsets with a different share of matching
rows, forced split with `parallel_scan_min_rows_per_scanner=1024`) gives
the 2,280 identical matching rows **15 distinct scores (1.34 to 5.81)**
on 4.1.4, 4.1.5 and branch-4.1. With `enable_parallel_scan=false` they
all get 3.647686.
Fix (same design as #69352, adapted to branch-4.1):
- `TabletCollectionStatistics` (in
`storage/compaction/collection_statistics.h`) holds the `rs_splits` of a
tablet's entire read source. The first scanner of the tablet to prepare
collects the statistics once with `CollectionStatistics::collect()` over
all of them, and every scanner of that tablet copies them into its own
`CollectionStatistics`. Later scanners wait for the first collection and
get its result or error.
- `CollectionStatistics::copy_collected_from()` copies the collected
counters and resets the derived idf/avgdl caches, so no cache is shared
between scanner threads.
- `ParallelScannerBuilder` creates one holder per tablet, only when the
scan has a `score()` runtime. Both the by-rowid and by-segment build
paths pass it to their scanners through `OlapScanner::Params`.
- The non-parallel path is unchanged. Each of its scanners already
receives the tablet's full read source.
Differences from #69352: on branch-4.1, `CollectionStatistics` lives in
`storage/compaction/`, has no `SearchPredicateCollector` constructor
argument and no `collect_full_collection()`, so the holder keeps the
read source's `rs_splits` and calls `collect()` on them.
`OlapScanner::Params` is initialized positionally on this branch, so the
new field is appended at the end.
Statistics remain per tablet. Making them consistent across tablets and
BEs is out of scope.
### Release note
Fix BM25 `score()` returning different scores for identical rows of the
same tablet when a parallel scan split the tablet into several scanners.
### Check List (For Author)
- Test
- [x] Regression test:
`inverted_index_p0/test_bm25_score_parallel_scan_statistics` (same suite
as #69352). Passes on branch-4.1 with this change.
- [x] Unit Test:
`CollectionStatisticsDetailedTest.CopyCollectedFromScoresLikeSource`,
`CollectionStatisticsTest.TabletCollectionStatisticsCollectsOnceAndCopiesToEveryCaller`
(not run locally; left to CI)
- [x] Manual test: ran the reproduction from #69352 against branch-4.1
builds on macOS arm64.
| Build | `enable_parallel_scan=true` (split forced) |
`enable_parallel_scan=false` |
|---|---|---|
| branch-4.1, without this change | 15 distinct scores (1.34 to 5.81) |
1 score (3.647686) |
| branch-4.1 `95846ddf` with this change | 1 score (3.647686), same over
5 repeated runs | 1 score (3.647686) |
- [ ] No need to test or manual test.
- Behavior changed:
- [x] Yes. Score queries that use a parallel scan now score every row of
a tablet against statistics of the whole tablet. Scores can differ from
earlier releases but are now independent of the scanner split.
- Does this need documentation?
- [x] No.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---------
Co-authored-by: Shivam Gupta <[email protected]>
---
be/src/exec/operator/olap_scan_operator.cpp | 28 ++++----
be/src/exec/scan/olap_scanner.cpp | 14 +++-
be/src/exec/scan/olap_scanner.h | 5 ++
be/src/exec/scan/parallel_scanner_builder.cpp | 30 ++++++--
be/src/exec/scan/parallel_scanner_builder.h | 16 +++--
.../storage/compaction/collection_statistics.cpp | 32 +++++++++
be/src/storage/compaction/collection_statistics.h | 33 +++++++++
.../compaction/collection_statistics_test.cpp | 39 ++++++++++
...test_bm25_score_parallel_scan_statistics.groovy | 82 ++++++++++++++++++++++
9 files changed, 255 insertions(+), 24 deletions(-)
diff --git a/be/src/exec/operator/olap_scan_operator.cpp
b/be/src/exec/operator/olap_scan_operator.cpp
index 5777afd1e73..540c89132be 100644
--- a/be/src/exec/operator/olap_scan_operator.cpp
+++ b/be/src/exec/operator/olap_scan_operator.cpp
@@ -564,6 +564,8 @@ Status
OlapScanLocalState::_init_scanners(std::list<ScannerSPtr>* scanners) {
std::max<int64_t>(1024,
state()->parallel_scan_min_rows_per_scanner());
scanner_builder.set_max_scanners_count(max_scanners_count);
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) {
@@ -626,18 +628,20 @@ Status
OlapScanLocalState::_init_scanners(std::list<ScannerSPtr>* scanners) {
for (auto& split : _read_sources[scan_range_idx].rs_splits) {
split.rs_reader = split.rs_reader->clone();
}
- auto scanner =
- OlapScanner::create_shared(this, OlapScanner::Params {
- state(),
-
_scanner_profile.get(),
- scanner_ranges,
-
_tablets[scan_range_idx].tablet,
- version,
-
_read_sources[scan_range_idx],
- {},
- p._limit,
-
p._olap_scan_node.is_preaggregation,
- });
+ auto scanner = OlapScanner::create_shared(
+ this, OlapScanner::Params {
+ state(),
+ _scanner_profile.get(),
+ scanner_ranges,
+ _tablets[scan_range_idx].tablet,
+ version,
+ _read_sources[scan_range_idx],
+ {},
+ p._limit,
+ p._olap_scan_node.is_preaggregation,
+ // Each scanner reads the tablet's entire
read source.
+ nullptr,
+ });
RETURN_IF_ERROR(scanner->init(state(), _conjuncts));
scanners->push_back(std::move(scanner));
}
diff --git a/be/src/exec/scan/olap_scanner.cpp
b/be/src/exec/scan/olap_scanner.cpp
index 06e6a08c771..7ebf0cba66a 100644
--- a/be/src/exec/scan/olap_scanner.cpp
+++ b/be/src/exec/scan/olap_scanner.cpp
@@ -72,6 +72,7 @@ using ReadSource = TabletReadSource;
OlapScanner::OlapScanner(ScanLocalStateBase* parent, OlapScanner::Params&&
params)
: Scanner(params.state, parent, params.limit, params.profile),
_key_ranges(std::move(params.key_ranges)),
+
_tablet_collection_statistics(std::move(params.tablet_collection_statistics)),
_tablet_reader_params({.tablet = std::move(params.tablet),
.tablet_schema {},
.aggregation = params.aggregation,
@@ -295,9 +296,16 @@ Status OlapScanner::prepare() {
_state, ReaderType::READER_QUERY,
tablet->file_cache_ttl_expiration_time(),
&_tablet_reader->mutable_stats()->file_cache_stats);
- RETURN_IF_ERROR(_tablet_reader_params.collection_statistics->collect(
- _state, _tablet_reader_params.rs_splits,
_tablet_reader_params.tablet_schema,
- _tablet_reader_params.common_expr_ctxs_push_down, &io_ctx));
+ if (_tablet_collection_statistics != nullptr) {
+ RETURN_IF_ERROR(_tablet_collection_statistics->copy_to(
+ _state, _tablet_reader_params.tablet_schema,
+ _tablet_reader_params.common_expr_ctxs_push_down, &io_ctx,
+ _tablet_reader_params.collection_statistics.get()));
+ } else {
+
RETURN_IF_ERROR(_tablet_reader_params.collection_statistics->collect(
+ _state, _tablet_reader_params.rs_splits,
_tablet_reader_params.tablet_schema,
+ _tablet_reader_params.common_expr_ctxs_push_down,
&io_ctx));
+ }
}
_has_prepared = true;
diff --git a/be/src/exec/scan/olap_scanner.h b/be/src/exec/scan/olap_scanner.h
index 117468bd66f..9d7a439a60e 100644
--- a/be/src/exec/scan/olap_scanner.h
+++ b/be/src/exec/scan/olap_scanner.h
@@ -56,6 +56,7 @@ struct IOContext;
} // namespace io
class Block;
+class TabletCollectionStatistics;
io::IOContext build_score_runtime_collection_io_context(RuntimeState* state,
ReaderType reader_type,
int64_t
expiration_time,
@@ -75,6 +76,9 @@ public:
io::FileCacheStatistics initial_file_cache_stats;
int64_t limit;
bool aggregation;
+ // Set when the tablet is split across several scanners, so that they
all score with
+ // statistics of the entire tablet instead of only the rowsets each
one reads.
+ std::shared_ptr<TabletCollectionStatistics>
tablet_collection_statistics;
};
OlapScanner(ScanLocalStateBase* parent, Params&& params);
@@ -105,6 +109,7 @@ private:
[[nodiscard]] Status _init_variant_columns();
std::vector<OlapScanRange*> _key_ranges;
+ std::shared_ptr<TabletCollectionStatistics> _tablet_collection_statistics;
TabletReader::ReaderParams _tablet_reader_params;
std::unique_ptr<TabletReader> _tablet_reader;
diff --git a/be/src/exec/scan/parallel_scanner_builder.cpp
b/be/src/exec/scan/parallel_scanner_builder.cpp
index 825810106b2..c6ae90b22ed 100644
--- a/be/src/exec/scan/parallel_scanner_builder.cpp
+++ b/be/src/exec/scan/parallel_scanner_builder.cpp
@@ -27,6 +27,7 @@
#include "exec/scan/olap_scanner.h"
#include "io/io_common.h"
#include "runtime/query_context.h"
+#include "storage/compaction/collection_statistics.h"
#include "storage/rowset/beta_rowset.h"
#include "storage/segment/segment_loader.h"
#include "storage/tablet/base_tablet.h"
@@ -83,6 +84,8 @@ Status
ParallelScannerBuilder::_build_scanners_by_rowid(std::list<ScannerSPtr>&
for (auto&& [tablet, version] : _tablets) {
DCHECK(_all_read_sources.contains(tablet->tablet_id()));
auto& entire_read_source = _all_read_sources[tablet->tablet_id()];
+ auto tablet_collection_statistics =
+ _create_tablet_collection_statistics(entire_read_source);
if (config::is_cloud_mode()) {
// FIXME(plat1ko): Avoid pointer cast
@@ -147,7 +150,8 @@ Status
ParallelScannerBuilder::_build_scanners_by_rowid(std::list<ScannerSPtr>&
.delete_predicates =
entire_read_source.delete_predicates,
.delete_bitmap =
entire_read_source.delete_bitmap},
take_initial_file_cache_stats(&_tablet_preload_file_cache_stats,
-
tablet->tablet_id())));
+
tablet->tablet_id()),
+ tablet_collection_statistics));
partitial_read_source = {};
split = RowSetSplits(reader->clone());
@@ -194,7 +198,8 @@ Status
ParallelScannerBuilder::_build_scanners_by_rowid(std::list<ScannerSPtr>&
.delete_predicates =
entire_read_source.delete_predicates,
.delete_bitmap =
entire_read_source.delete_bitmap},
take_initial_file_cache_stats(&_tablet_preload_file_cache_stats,
-
tablet->tablet_id())));
+
tablet->tablet_id()),
+ tablet_collection_statistics));
}
}
@@ -211,6 +216,8 @@ Status
ParallelScannerBuilder::_build_scanners_by_segment(std::list<ScannerSPtr>
for (auto&& [tablet, version] : _tablets) {
DCHECK(_all_read_sources.contains(tablet->tablet_id()));
auto& entire_read_source = _all_read_sources[tablet->tablet_id()];
+ auto tablet_collection_statistics =
+ _create_tablet_collection_statistics(entire_read_source);
if (config::is_cloud_mode()) {
// FIXME(plat1ko): Avoid pointer cast
@@ -256,7 +263,8 @@ Status
ParallelScannerBuilder::_build_scanners_by_segment(std::list<ScannerSPtr>
.delete_predicates =
entire_read_source.delete_predicates,
.delete_bitmap =
entire_read_source.delete_bitmap},
take_initial_file_cache_stats(&_tablet_preload_file_cache_stats,
-
tablet->tablet_id())));
+ tablet->tablet_id()),
+ tablet_collection_statistics));
// Reset for next scanner
partitial_read_source = TabletReadSource();
@@ -293,7 +301,8 @@ Status
ParallelScannerBuilder::_build_scanners_by_segment(std::list<ScannerSPtr>
.delete_predicates =
entire_read_source.delete_predicates,
.delete_bitmap =
entire_read_source.delete_bitmap},
take_initial_file_cache_stats(&_tablet_preload_file_cache_stats,
-
tablet->tablet_id())));
+
tablet->tablet_id()),
+ tablet_collection_statistics));
}
}
@@ -340,9 +349,19 @@ Status ParallelScannerBuilder::_load() {
return Status::OK();
}
+std::shared_ptr<TabletCollectionStatistics>
+ParallelScannerBuilder::_create_tablet_collection_statistics(
+ const TabletReadSource& entire_read_source) const {
+ if (!_share_tablet_collection_statistics) {
+ return nullptr;
+ }
+ return
std::make_shared<TabletCollectionStatistics>(entire_read_source.rs_splits);
+}
+
std::shared_ptr<OlapScanner> ParallelScannerBuilder::_build_scanner(
BaseTabletSPtr tablet, int64_t version, const
std::vector<OlapScanRange*>& key_ranges,
- TabletReadSource&& read_source, io::FileCacheStatistics&&
initial_file_cache_stats) {
+ TabletReadSource&& read_source, io::FileCacheStatistics&&
initial_file_cache_stats,
+ std::shared_ptr<TabletCollectionStatistics>
tablet_collection_statistics) {
OlapScanner::Params params {
_state,
_scanner_profile.get(),
@@ -353,6 +372,7 @@ std::shared_ptr<OlapScanner>
ParallelScannerBuilder::_build_scanner(
std::move(initial_file_cache_stats),
_limit,
_is_preaggregation,
+ std::move(tablet_collection_statistics),
};
return OlapScanner::create_shared(_parent, std::move(params));
}
diff --git a/be/src/exec/scan/parallel_scanner_builder.h
b/be/src/exec/scan/parallel_scanner_builder.h
index a8507ad8cc5..1f7dc44c448 100644
--- a/be/src/exec/scan/parallel_scanner_builder.h
+++ b/be/src/exec/scan/parallel_scanner_builder.h
@@ -64,6 +64,9 @@ public:
void set_scan_parallelism_by_segment(bool v) {
_scan_parallelism_by_segment = v; }
+ // Scanners of one tablet share BM25 statistics collected over the entire
tablet.
+ void set_share_tablet_collection_statistics(bool v) {
_share_tablet_collection_statistics = v; }
+
const OlapReaderStatistics* builder_stats() const { return
&_builder_stats; }
private:
@@ -74,10 +77,13 @@ private:
// Build scanners so that each segment is handled by its own scanner.
Status _build_scanners_by_segment(std::list<ScannerSPtr>& scanners);
- std::shared_ptr<OlapScanner> _build_scanner(BaseTabletSPtr tablet, int64_t
version,
- const
std::vector<OlapScanRange*>& key_ranges,
- TabletReadSource&& read_source,
- io::FileCacheStatistics&&
initial_file_cache_stats);
+ std::shared_ptr<TabletCollectionStatistics>
_create_tablet_collection_statistics(
+ const TabletReadSource& entire_read_source) const;
+
+ std::shared_ptr<OlapScanner> _build_scanner(
+ BaseTabletSPtr tablet, int64_t version, const
std::vector<OlapScanRange*>& key_ranges,
+ TabletReadSource&& read_source, io::FileCacheStatistics&&
initial_file_cache_stats,
+ std::shared_ptr<TabletCollectionStatistics>
tablet_collection_statistics);
OlapScanLocalState* _parent;
@@ -97,6 +103,8 @@ private:
// Force building one scanner per segment when true.
bool _scan_parallelism_by_segment {false};
+ bool _share_tablet_collection_statistics {false};
+
std::shared_ptr<RuntimeProfile> _scanner_profile;
OlapReaderStatistics _builder_stats;
RuntimeState* _state;
diff --git a/be/src/storage/compaction/collection_statistics.cpp
b/be/src/storage/compaction/collection_statistics.cpp
index 6b59c1cc1ec..413c8a84c66 100644
--- a/be/src/storage/compaction/collection_statistics.cpp
+++ b/be/src/storage/compaction/collection_statistics.cpp
@@ -237,6 +237,38 @@ Status CollectionStatistics::process_segment(const
RowsetSharedPtr& rowset, int3
return Status::OK();
}
+void CollectionStatistics::copy_collected_from(const CollectionStatistics&
other) {
+ _total_num_docs = other._total_num_docs;
+ _total_num_tokens = other._total_num_tokens;
+ _term_doc_freqs = other._term_doc_freqs;
+ _avg_dl_by_col.clear();
+ _idf_by_col_term.clear();
+}
+
+TabletCollectionStatistics::TabletCollectionStatistics(std::vector<RowSetSplits>
rs_splits)
+ : _rs_splits(std::move(rs_splits)) {}
+
+TabletCollectionStatistics::~TabletCollectionStatistics() = default;
+
+Status TabletCollectionStatistics::copy_to(RuntimeState* state,
+ const TabletSchemaSPtr&
tablet_schema,
+ const VExprContextSPtrs&
common_expr_ctxs_push_down,
+ io::IOContext* io_ctx,
+ CollectionStatistics* statistics) {
+ DORIS_CHECK(statistics != nullptr);
+ std::lock_guard lock(_mutex);
+ if (!_collected) {
+ _status = _statistics.collect(state, _rs_splits, tablet_schema,
common_expr_ctxs_push_down,
+ io_ctx);
+ _collected = true;
+ // Do not hold the rowsets any longer than needed.
+ _rs_splits.clear();
+ }
+ RETURN_IF_ERROR(_status);
+ statistics->copy_collected_from(_statistics);
+ return Status::OK();
+}
+
uint64_t CollectionStatistics::get_term_doc_freq_by_col(const std::wstring&
lucene_col_name,
const std::wstring&
term) {
if (!_term_doc_freqs.contains(lucene_col_name)) {
diff --git a/be/src/storage/compaction/collection_statistics.h
b/be/src/storage/compaction/collection_statistics.h
index e51d5750db6..5658438fb32 100644
--- a/be/src/storage/compaction/collection_statistics.h
+++ b/be/src/storage/compaction/collection_statistics.h
@@ -17,8 +17,10 @@
#pragma once
#include <cstdint>
+#include <mutex>
#include <string>
#include <unordered_map>
+#include <vector>
#include "common/be_mock_util.h"
#include "exprs/vexpr_fwd.h"
@@ -58,6 +60,10 @@ public:
const std::wstring& term);
MOCK_FUNCTION float get_or_calculate_avg_dl(const std::wstring&
lucene_col_name);
+ // Replaces the collected counters with those of `other`. The derived
idf/avgdl caches are
+ // reset rather than copied, so the copy can be used by another thread
without sharing them.
+ void copy_collected_from(const CollectionStatistics& other);
+
private:
Status extract_collect_info(RuntimeState* state,
const VExprContextSPtrs&
common_expr_ctxs_push_down,
@@ -86,5 +92,32 @@ private:
};
using CollectionStatisticsPtr = std::shared_ptr<CollectionStatistics>;
+// BM25 statistics of a whole tablet, shared by every scanner the tablet is
split into.
+//
+// A parallel scan divides a tablet's rowsets among several scanners. If each
scanner
+// collected statistics over only its own rowsets, rows with identical content
would score
+// differently depending on which scanner read them, and the split itself
depends on the
+// tablet's row count and the number of cores. Collecting once over the entire
read source
+// gives every row of the tablet the same idf and avgdl.
+class TabletCollectionStatistics {
+public:
+ explicit TabletCollectionStatistics(std::vector<RowSetSplits> rs_splits);
+ ~TabletCollectionStatistics();
+
+ // Collects the statistics on the first call, then copies them into
`statistics`.
+ // Later callers wait for the first collection and receive its result or
error.
+ Status copy_to(RuntimeState* state, const TabletSchemaSPtr& tablet_schema,
+ const VExprContextSPtrs& common_expr_ctxs_push_down,
io::IOContext* io_ctx,
+ CollectionStatistics* statistics);
+
+private:
+ std::mutex _mutex;
+ bool _collected = false;
+ Status _status;
+ std::vector<RowSetSplits> _rs_splits;
+ CollectionStatistics _statistics;
+};
+using TabletCollectionStatisticsPtr =
std::shared_ptr<TabletCollectionStatistics>;
+
#include "common/compile_check_end.h"
} // namespace doris
diff --git a/be/test/storage/compaction/collection_statistics_test.cpp
b/be/test/storage/compaction/collection_statistics_test.cpp
index ae605c15f2e..279ddf76787 100644
--- a/be/test/storage/compaction/collection_statistics_test.cpp
+++ b/be/test/storage/compaction/collection_statistics_test.cpp
@@ -680,6 +680,45 @@ TEST_F(CollectionStatisticsDetailedTest,
IdfCalculationWithDifferentFrequencies)
EXPECT_GT(rare_idf, 0);
}
+// Scanners of one tablet each copy the tablet's statistics, so they must
score identically.
+TEST_F(CollectionStatisticsDetailedTest, CopyCollectedFromScoresLikeSource) {
+ stats_->set_total_num_docs(1000);
+ stats_->set_total_num_tokens(L"content", 5000);
+ stats_->set_term_doc_freq(L"content", L"jobs", 37);
+
+ CollectionStatistics copy;
+ copy.copy_collected_from(*stats_);
+
+ EXPECT_FLOAT_EQ(copy.get_or_calculate_idf(L"content", L"jobs"),
+ stats_->get_or_calculate_idf(L"content", L"jobs"));
+ EXPECT_FLOAT_EQ(copy.get_or_calculate_avg_dl(L"content"),
+ stats_->get_or_calculate_avg_dl(L"content"));
+
+ // The copy does not share the source's counters or derived caches.
+ stats_->set_term_doc_freq(L"content", L"jobs", 900);
+ CollectionStatistics recopied;
+ recopied.copy_collected_from(*stats_);
+ EXPECT_NE(recopied.get_or_calculate_idf(L"content", L"jobs"),
+ copy.get_or_calculate_idf(L"content", L"jobs"));
+}
+
+TEST_F(CollectionStatisticsTest,
TabletCollectionStatisticsCollectsOnceAndCopiesToEveryCaller) {
+ auto tablet_schema = create_tablet_schema_with_inverted_index();
+ auto expr_contexts = create_match_expr_contexts();
+ TabletCollectionStatistics tablet_statistics({});
+
+ CollectionStatistics first;
+ CollectionStatistics second;
+ EXPECT_TRUE(
+ tablet_statistics
+ .copy_to(runtime_state_.get(), tablet_schema,
expr_contexts, nullptr, &first)
+ .ok());
+ EXPECT_TRUE(
+ tablet_statistics
+ .copy_to(runtime_state_.get(), tablet_schema,
expr_contexts, nullptr, &second)
+ .ok());
+}
+
TEST_F(CollectionStatisticsTest, CollectWithCastWrappedSlotRef) {
auto tablet_schema = create_tablet_schema_with_inverted_index();
diff --git
a/regression-test/suites/inverted_index_p0/test_bm25_score_parallel_scan_statistics.groovy
b/regression-test/suites/inverted_index_p0/test_bm25_score_parallel_scan_statistics.groovy
new file mode 100644
index 00000000000..f6a69dfdb19
--- /dev/null
+++
b/regression-test/suites/inverted_index_p0/test_bm25_score_parallel_scan_statistics.groovy
@@ -0,0 +1,82 @@
+// 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.
+
+// A parallel scan splits one tablet into several scanners. BM25 statistics
must
+// still cover the whole tablet, so rows with identical text get identical
scores
+// no matter which scanner reads them.
+suite("test_bm25_score_parallel_scan_statistics", "p0") {
+ def tableName = "test_bm25_score_parallel_scan_statistics"
+
+ sql "DROP TABLE IF EXISTS ${tableName}"
+ sql """
+ CREATE TABLE ${tableName} (
+ id BIGINT,
+ content TEXT,
+ INDEX idx_content (content) USING INVERTED PROPERTIES("parser" =
"english")
+ ) ENGINE=OLAP
+ UNIQUE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES (
+ "replication_allocation" = "tag.location.default: 1",
+ "enable_unique_key_merge_on_write" = "true",
+ "disable_auto_compaction" = "true"
+ );
+ """
+
+ // Each load is one rowset with a different share of matching rows, so
statistics
+ // collected per scanner would differ between scanners.
+ for (int batch = 0; batch < 8; batch++) {
+ sql """
+ INSERT INTO ${tableName}
+ SELECT ${batch} * 2000 + number,
+ IF(number % ${batch * 3 + 2} = 0,
+ 'we should try these jobs today',
+ 'call the candidate about salary')
+ FROM numbers("number" = "2000");
+ """
+ }
+ sql "sync"
+
+ // SEARCH() must be evaluated by the inverted index; fuzzy mode may turn
these off.
+ sql "set enable_common_expr_pushdown = true"
+ sql "set enable_common_expr_pushdown_for_inverted_index = true"
+
+ def scoreQuery = { String vars ->
+ sql """
+ SELECT /*+ SET_VAR(${vars}) */ score() AS s
+ FROM ${tableName}
+ WHERE SEARCH('content:jobs OR content:try')
+ ORDER BY score() DESC
+ LIMIT 100000
+ """
+ }
+
+ def parallel = scoreQuery("enable_parallel_scan=true,
parallel_scan_min_rows_per_scanner=1024, parallel_scan_max_scanners_count=16")
+ def serial = scoreQuery("enable_parallel_scan=false")
+
+ def parallelScores = parallel.collect { it[0] }.unique()
+ def serialScores = serial.collect { it[0] }.unique()
+
+ assertTrue(parallel.size() > 0)
+ assertEquals(serial.size(), parallel.size())
+ assertEquals(1, serialScores.size())
+ assertEquals(1, parallelScores.size(),
+ "identical rows got different BM25 scores across scanners:
${parallelScores}")
+ assertEquals(serialScores[0], parallelScores[0])
+
+ sql "DROP TABLE IF EXISTS ${tableName}"
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]