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 e4263cbafc1 branch-4.2: [fix](be) Score MATCH on keyword inverted
indexes like SEARCH #69790 (#70008)
e4263cbafc1 is described below
commit e4263cbafc1418914b9ebd83aa276774adb2ec15
Author: Jack <[email protected]>
AuthorDate: Thu Nov 5 11:28:55 2026 +0800
branch-4.2: [fix](be) Score MATCH on keyword inverted indexes like SEARCH
#69790 (#70008)
Backport of #69790 to branch-4.2.
### What problem does this PR solve?
Issue Number: None
Related PR: #69790, #69051
Problem Summary: score() of a MATCH on an untokenized (keyword) inverted
index is always 0, while SEARCH on the same field scores BM25:
INDEX idx_redirect (redirect) USING INVERTED
-- one row ('BP')
SELECT score() FROM t WHERE redirect MATCH 'BP'; -- 0
SELECT score() FROM t WHERE SEARCH('redirect:BP'); -- 0.2876821
Elasticsearch scores match, term and match_phrase on a keyword field
with BM25 in query context, so the score is the idf of the whole value.
#69051 (already on 4.2) made the engine score keyword postings but kept
MATCH out. This makes a keyword index eligible for MATCH scoring and
collects its whole value as one term, the one the reader looks up. It
also carries the follow-up fixes from #69790's review:
- A keyword MATCH_PHRASE drops its ` ~N` slop before collecting.
- A MATCH on a non-string leaf such as `CAST(n AS STRING)` collects
nothing.
- A VARIANT path that a segment indexed as BKD, or does not hold, adds
no statistics. This covers the BKD searcher coming from the searcher
cache, and SNII.
- A SNII index kind is looked up without copying the directory.
### Branch adaptations
- `can_score_match` keeps 4.2's analyzed rule (support_phrase only);
branch-4.2 has no lowercase-normalizer clause.
- `test_omit_norms` does not exist on 4.2, so its `.out` change is
dropped. The branch was cut before #69922 (the 4.2 backport of #69815)
and then merged with branch-4.2 to resolve its conflict in
`index_reader_helper_test.cpp`: both tests are kept, as on master.
- `test_match_keyword_score` inserts its VARIANT rows as JSON strings:
`parse_to_variant` in `VALUES` fails on 4.2 with a max-subcolumns
conversion error. The expected output is master's file, unchanged.
- **#69901 (BM25 statistics per index) is not on 4.2.** A MATCH on a
keyword index and a MATCH on an analyzed index of the same column in one
`score()` query (`name MATCH 'apple' USING ANALYZER english AND name
MATCH 'apple' USING ANALYZER none`) now fails with "Scoring predicates
for one field select different inverted indexes". Master behaved the
same before #69901. Before this change, that query ran with the keyword
clause scoring 0. Each clause alone scores fine. Two analyzed indexes on
one column already fail this way on 4.2. Backporting #69901 lifts this.
### Validation
On this branch merged with branch-4.2 c2aaed99db, ASAN build:
- BE UT: 1611 tests in `IndexReaderHelperTest`, `CollectionStatistics*`
and `*Snii*` pass.
- Regression: `test_match_keyword_score` passes against master's `.out`
on V2/V3/SNII. The following also pass:
`test_match_phrase_prefix_score`, `test_bm25_score`,
`test_score_filter_matrix`, `test_score_filter_usage`,
`test_search_score_topn_predicates`, `test_multi_analyzer_query`,
`test_match_using_analyzer`, `test_single_column_multi_index` (with
debug points on), `test_search_score_keyword_filters`,
`test_search_score_raw_index`, `test_search_score_pattern_constant`.
- Local note: the local thirdparty predates the lance-c upgrade, so
`lance_reader.cpp`'s `lance_scanner_set_scalar_index_auto_selection`
call was stubbed for the local build only. It is not part of this PR.
### Release note
score() of MATCH / MATCH_ALL / MATCH_PHRASE on a keyword (untokenized)
inverted index now returns the BM25 score of the value, the same as
SEARCH, instead of 0.
### Check List (For Author)
- Test: Regression test / Unit Test (see Validation)
- Behavior changed: Yes. Keyword MATCH contributes its BM25 score to
score() instead of 0.
- Does this need documentation: No
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
be/src/storage/index/index_file_reader.cpp | 16 ++
be/src/storage/index/index_file_reader.h | 5 +
be/src/storage/index/index_reader_helper.cpp | 14 +-
.../inverted/reader/collection_statistics.cpp | 37 ++-
.../index/inverted/reader/predicate_collector.cpp | 36 ++-
.../inverted/snii/reader/snii_segment_reader.h | 6 +
.../inverted/reader/collection_statistics_test.cpp | 190 +++++++++++++++-
.../snii_segment_reader_logical_indexes_test.cpp | 9 +
.../storage/segment/index_reader_helper_test.cpp | 13 +-
.../inverted_index_p0/test_match_keyword_score.out | 253 +++++++++++++++++++++
.../search/test_search_score_keyword_filters.out | 97 ++++----
.../test_match_keyword_score.groovy | 146 ++++++++++++
.../test_search_score_keyword_filters.groovy | 3 +-
13 files changed, 750 insertions(+), 75 deletions(-)
diff --git a/be/src/storage/index/index_file_reader.cpp
b/be/src/storage/index/index_file_reader.cpp
index 12ddda2711e..5082151b510 100644
--- a/be/src/storage/index/index_file_reader.cpp
+++ b/be/src/storage/index/index_file_reader.cpp
@@ -402,6 +402,22 @@ IndexFileReader::snii_logical_indexes() const {
return _snii_segment_reader->logical_indexes();
}
+Result<std::optional<doris::snii::format::LogicalIndexKind>>
+IndexFileReader::snii_logical_index_kind(uint64_t index_id, std::string_view
suffix) const {
+ DCHECK(_storage_format == InvertedIndexStorageFormatPB::SNII);
+ std::shared_lock<std::shared_mutex> lock(_mutex);
+ if (_snii_segment_reader == nullptr) {
+ return
ResultError(Status::Error<ErrorCode::INVERTED_INDEX_FILE_NOT_FOUND>(
+ "SNII index file {} is not opened",
+
InvertedIndexDescriptor::get_index_file_path_v2(_index_path_prefix)));
+ }
+ const auto* entry = _snii_segment_reader->find_logical_index(index_id,
suffix);
+ if (entry == nullptr) {
+ return std::nullopt;
+ }
+ return entry->kind;
+}
+
Status IndexFileReader::snii_core_metadata(uint64_t index_id, std::string_view
suffix,
doris::snii::format::CoreMetadata*
out,
const io::IOContext* io_ctx) const {
diff --git a/be/src/storage/index/index_file_reader.h
b/be/src/storage/index/index_file_reader.h
index f08474fd8fe..62fdbe9fd97 100644
--- a/be/src/storage/index/index_file_reader.h
+++ b/be/src/storage/index/index_file_reader.h
@@ -32,6 +32,7 @@
#include <map>
#include <memory>
#include <mutex>
+#include <optional>
#include <shared_mutex>
#include <string>
#include <unordered_map>
@@ -97,6 +98,10 @@ public:
doris::snii::reader::LogicalIndexOpenMode open_mode) const;
// SNII only: every entry of the container directory, text and blob kinds
alike.
Result<std::vector<doris::snii::format::LogicalIndexMetadataRef>>
snii_logical_indexes() const;
+ // SNII only: the kind of one directory entry, nothing when the directory
has no such entry.
+ // A directory can hold thousands of VARIANT paths, so a lookup does not
copy it.
+ Result<std::optional<doris::snii::format::LogicalIndexKind>>
snii_logical_index_kind(
+ uint64_t index_id, std::string_view suffix) const;
// SNII only: reads the CoreMetadata of one text logical index with the
caller's IO context.
Status snii_core_metadata(uint64_t index_id, std::string_view suffix,
doris::snii::format::CoreMetadata* out,
diff --git a/be/src/storage/index/index_reader_helper.cpp
b/be/src/storage/index/index_reader_helper.cpp
index efd92d9b2d3..1a8f92476a2 100644
--- a/be/src/storage/index/index_reader_helper.cpp
+++ b/be/src/storage/index/index_reader_helper.cpp
@@ -24,11 +24,15 @@ namespace {
bool can_score_match(const TabletIndex* index_meta) {
const auto& properties = index_meta->properties();
- // parser=none can inherit support_phrase=true from FE defaults. MATCH
statistics skip
- // these keyword indexes, so their filters must not request a scoring
context either.
- return inverted_index::InvertedIndexAnalyzer::should_analyzer(properties)
&&
- get_parser_phrase_support_string_from_properties(properties) ==
- INVERTED_INDEX_PARSER_PHRASE_SUPPORT_YES;
+ // A keyword index keeps each value as one term, without positions or
norms, and its postings
+ // score with unit frequency and length whatever support_phrase says, as
an Elasticsearch
+ // keyword field does: over tags (x, x, y, z), tag MATCH 'x' scores idf(x)
= ln(2) for both
+ // matching rows, the same as SEARCH('tag:x'). MATCH statistics collect
these terms too.
+ if (!inverted_index::InvertedIndexAnalyzer::should_analyzer(properties)) {
+ return true;
+ }
+ return get_parser_phrase_support_string_from_properties(properties) ==
+ INVERTED_INDEX_PARSER_PHRASE_SUPPORT_YES;
}
} // namespace
diff --git a/be/src/storage/index/inverted/reader/collection_statistics.cpp
b/be/src/storage/index/inverted/reader/collection_statistics.cpp
index b73d80825af..daaefc8c6c1 100644
--- a/be/src/storage/index/inverted/reader/collection_statistics.cpp
+++ b/be/src/storage/index/inverted/reader/collection_statistics.cpp
@@ -73,6 +73,15 @@ Result<std::shared_ptr<lucene::search::IndexSearcher>>
open_optional_clucene_sta
}
return ResultError(status);
}
+ // A segment can index a field with BKD although the tablet schema calls
it text: a VARIANT
+ // path is typed per segment, so CAST(v['n'] AS STRING) MATCH '1' collects
statistics for v.n,
+ // which a segment holding numbers indexed as BKD. That index has no terms
and no segments file
+ // to open, so it adds no statistics, as a segment without the path does;
its rows match
+ // without the index and score nothing.
+ if (compound.value()->fileExists(
+
InvertedIndexDescriptor::get_temporary_bkd_index_data_file_name())) {
+ return std::shared_ptr<lucene::search::IndexSearcher> {};
+ }
lucene::index::IndexReader* reader = nullptr;
try {
reader = lucene::index::IndexReader::open(compound.value().get());
@@ -89,6 +98,20 @@ Result<std::shared_ptr<lucene::search::IndexSearcher>>
open_optional_clucene_sta
return std::make_shared<lucene::search::IndexSearcher>(reader, true);
}
+// Whether a SNII segment has no terms for this field: it indexed the field
with BKD, see
+// open_optional_clucene_stats_searcher, or the field is a VARIANT path this
segment does not hold,
+// as segments of {"a": 1} hold no path b. Any other missing index was not
built, and the caller
+// refuses to score the collection without it.
+Result<bool> snii_field_has_no_terms(const segment_v2::IndexFileReader&
file_reader,
+ const TabletIndex& index) {
+ const auto kind = DORIS_TRY(file_reader.snii_logical_index_kind(
+ static_cast<uint64_t>(index.index_id()),
index.get_index_suffix()));
+ if (!kind.has_value()) {
+ return !index.get_index_suffix().empty();
+ }
+ return *kind == ::doris::snii::format::LogicalIndexKind::kBkd;
+}
+
} // namespace
namespace collection_statistics_detail {
@@ -555,6 +578,11 @@ Status CollectionStatistics::process_segment(const
RowsetSharedPtr& rowset, int6
segment_v2::snii_doris::DorisSniiFileReader::ScopedIOContext
io_context_scope(io_ctx);
SniiScoringSegmentAccumulator segment_accumulator;
for (const auto& [ws_field_name, collect_info] : collect_infos) {
+ const bool no_terms =
+ DORIS_TRY(snii_field_has_no_terms(*idx_file_reader,
*collect_info.index_meta));
+ if (no_terms) {
+ continue;
+ }
auto logical_reader_result =
idx_file_reader->open_snii_index(collect_info.index_meta,
io_ctx);
if (!logical_reader_result.has_value()) {
@@ -665,8 +693,13 @@ Status CollectionStatistics::process_segment(const
RowsetSharedPtr& rowset, int6
auto searcher_variant =
inverted_index_cache_handle.get<CluceneSearcherCacheValue>()->searcher;
- auto index_searcher_ptr =
std::get<FulltextIndexSearcherPtr>(searcher_variant);
- index_searcher = index_searcher_ptr.get();
+ // CAST(v['n'] AS BIGINT) = 1 caches the BKD searcher of v.n under
this key, so a BKD index
+ // can also arrive from the cache: no terms, as in
open_optional_clucene_stats_searcher.
+ const auto* fulltext_searcher =
std::get_if<FulltextIndexSearcherPtr>(&searcher_variant);
+ if (fulltext_searcher == nullptr) {
+ continue;
+ }
+ index_searcher = fulltext_searcher->get();
index_reader = index_searcher->getReader();
#endif
auto source =
segment_v2::clucene_index_source(segment_v2::non_owning_reader(index_reader),
diff --git a/be/src/storage/index/inverted/reader/predicate_collector.cpp
b/be/src/storage/index/inverted/reader/predicate_collector.cpp
index 3d0b4fbbed6..3ab6c785ac6 100644
--- a/be/src/storage/index/inverted/reader/predicate_collector.cpp
+++ b/be/src/storage/index/inverted/reader/predicate_collector.cpp
@@ -34,6 +34,7 @@
#include "storage/index/inverted/analysis/analyzer/analyzer.h"
#include "storage/index/inverted/common/string_helper.h"
#include "storage/index/inverted/query/exec/term_pattern.h"
+#include "storage/index/inverted/query/logical/match_lowering.h"
#include "storage/index/inverted/reader/inverted_index_selector.h"
#include "storage/tablet/tablet_schema.h"
#include "storage/utils.h"
@@ -285,6 +286,32 @@ void preserve_selected_index_metadata(const
ScoringIndexCandidates& candidates,
using namespace scoring_collection_detail;
+namespace {
+
+// The terms a MATCH scores, the ones its reader looks up. A keyword index
stores the whole value
+// as one term, case and spaces included: tag MATCH 'Hello World' scores the
single term
+// "Hello World", as SEARCH('tag:"Hello World"') does. Any other index cuts
the value with the
+// query's analyzer.
+Result<std::vector<TermInfo>> match_scoring_terms(const std::string& value,
+ InvertedIndexQueryType
query_type,
+ const TabletIndex&
index_meta,
+ const
InvertedIndexAnalyzerCtx& analyzer_ctx) {
+ if
(!inverted_index::InvertedIndexAnalyzer::should_analyzer(index_meta.properties()))
{
+ // A keyword phrase reads a trailing " ~N" as its slop, as lower_match
does before the
+ // reader looks the term up: tag MATCH_PHRASE 'BP ~1' looks up BP.
Collecting "BP ~1"
+ // would leave the scorer without statistics for BP and fail the query.
+ std::string_view term = value;
+ if (query_type == InvertedIndexQueryType::MATCH_PHRASE_QUERY) {
+ index_query::logical::take_slop(&term);
+ }
+ return std::vector<TermInfo> {TermInfo(std::string(term))};
+ }
+ DORIS_CHECK(analyzer_ctx.analyzer_provider != nullptr);
+ return analyze_plain_query(value, analyzer_ctx);
+}
+
+} // namespace
+
VSlotRef* PredicateCollector::find_slot_ref(const VExprSPtr& expr) const {
if (!expr) {
return nullptr;
@@ -365,7 +392,10 @@ Status MatchPredicateCollector::collect(RuntimeState*
state, const TabletSchemaS
const auto* index_meta = DORIS_TRY(
select_index_meta(candidates.index_metas, candidates.field_type,
query_type,
analyzer_ctx->analyzer_key,
analyzer_ctx->legacy_analyzer_key));
- if
(!inverted_index::InvertedIndexAnalyzer::should_analyzer(index_meta->properties())
||
+ // Only a text index is read with a score. CAST(n AS STRING) MATCH '1' on
an INT column runs as
+ // a scalar filter, and the index selected for n is a BKD index that holds
no terms: opening it
+ // as a text index to count them would fail the query.
+ if (!is_string_type(candidates.field_type) ||
!IndexReaderHelper::is_need_similarity_score(expr->op(), index_meta)) {
return Status::OK();
}
@@ -375,10 +405,10 @@ Status MatchPredicateCollector::collect(RuntimeState*
state, const TabletSchemaS
return Status::OK();
}
- DORIS_CHECK(analyzer_ctx->analyzer_provider != nullptr);
auto options = DataTypeSerDe::get_default_format_options();
options.timezone = &state->timezone_obj();
- auto term_infos =
DORIS_TRY(analyze_plain_query(right_literal->value(options), *analyzer_ctx));
+ auto term_infos =
DORIS_TRY(match_scoring_terms(right_literal->value(options), query_type,
+ *index_meta,
*analyzer_ctx));
// The prefix of a phrase prefix is scored by the terms it expands to, so
the statistics are
// collected for them per segment; a prefix alone scores a constant and
needs none.
std::optional<TermInfo> scoring_prefix;
diff --git a/be/src/storage/index/inverted/snii/reader/snii_segment_reader.h
b/be/src/storage/index/inverted/snii/reader/snii_segment_reader.h
index c28329d66e3..4c640504489 100644
--- a/be/src/storage/index/inverted/snii/reader/snii_segment_reader.h
+++ b/be/src/storage/index/inverted/snii/reader/snii_segment_reader.h
@@ -115,6 +115,12 @@ public:
return directory_.entries();
}
+ // The directory entry of (index_id, suffix), owned by this reader;
nullptr when it has none.
+ const format::LogicalIndexMetadataRef* find_logical_index(uint64_t
index_id,
+ std::string_view
suffix) const {
+ return directory_.find(index_id, suffix);
+ }
+
Status index_exists(uint64_t index_id, std::string_view suffix, bool*
const exists) const;
// Loads the adjacent Core/STI/DBD group for (index_id, suffix) and builds
a
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 8eef09fc51b..2f5fc8650c9 100644
--- a/be/test/storage/index/inverted/reader/collection_statistics_test.cpp
+++ b/be/test/storage/index/inverted/reader/collection_statistics_test.cpp
@@ -1581,6 +1581,69 @@ TEST_F(CollectionStatisticsTest,
SniiScoringRejectsMissingLogicalIndexForWholeCo
expect_no_collected_tokens(L"1");
}
+// A VARIANT path can be absent from a segment, as in {"a": 1} next to
{"host": "x"}. Unlike the
+// plain index above, whose absence means it was not built, the segment just
adds no statistics.
+TEST_F(CollectionStatisticsTest,
SniiScoringSkipsVariantPathMissingFromSegment) {
+ TabletSchemaPB schema_pb;
+ schema_pb.set_keys_type(DUP_KEYS);
+
schema_pb.set_inverted_index_storage_format(InvertedIndexStorageFormatPB::SNII);
+ auto tablet_schema = std::make_shared<TabletSchema>();
+ tablet_schema->init_from_pb(schema_pb);
+
+ constexpr int32_t kVariantUid = 9002;
+ TabletColumn variant_col;
+ variant_col.set_unique_id(kVariantUid);
+ variant_col.set_name("v");
+ variant_col.set_type(FieldType::OLAP_FIELD_TYPE_VARIANT);
+ tablet_schema->append_column(variant_col);
+
+ TabletColumn sub_col;
+ sub_col.set_unique_id(-1);
+ sub_col.set_name("v.host");
+ sub_col.set_type(FieldType::OLAP_FIELD_TYPE_STRING);
+ sub_col.set_parent_unique_id(kVariantUid);
+ sub_col.set_path_info(PathInData("v.host"));
+ tablet_schema->append_column(sub_col);
+
+ TabletIndexPB index_pb;
+ index_pb.set_index_id(2002);
+ index_pb.set_index_name("variant_subcolumn_idx");
+ index_pb.set_index_type(IndexType::INVERTED);
+ index_pb.add_col_unique_id(kVariantUid);
+ (*index_pb.mutable_properties())["parser"] = "standard";
+ (*index_pb.mutable_properties())["support_phrase"] = "true";
+ TabletIndex sub_index;
+ sub_index.init_from_pb(index_pb);
+ sub_index.set_escaped_escaped_index_suffix_path(sub_col.suffix_path());
+ tablet_schema->append_index(std::move(sub_index));
+
+ constexpr int kSlotId = 43;
+ runtime_state_->_mock_desc_tbl->add_slot_descriptor(SlotId(kSlotId),
kVariantUid);
+ auto match_expr =
std::make_shared<collection_statistics::MockVExpr>(TExprNodeType::MATCH_PRED);
+ match_expr->_children.push_back(
+ std::make_shared<collection_statistics::MockVSlotRef>("v.host",
SlotId(kSlotId)));
+
match_expr->_children.push_back(std::make_shared<collection_statistics::MockVLiteral>("alpha"));
+ VExprContextSPtrs expr_contexts
{std::make_shared<VExprContext>(match_expr)};
+
+ // The segment holds another logical index only.
+ const std::string segment_path = test_dir_ +
"/snii_missing_variant_path_0.dat";
+ auto write_status = write_snii_scoring_segment(segment_path);
+ ASSERT_TRUE(write_status.ok()) << write_status;
+ auto rowset_meta =
std::make_shared<collection_statistics::MockRowsetMeta>();
+ auto rowset =
std::make_shared<collection_statistics::MockRowset>(tablet_schema, rowset_meta);
+ rowset->set_num_segments(1);
+ rowset->set_segment_path(0, segment_path);
+ auto reader =
std::make_shared<collection_statistics::MockRowsetReader>(rowset);
+ std::vector<RowSetSplits> splits {RowSetSplits(reader)};
+
+ auto status =
+ stats_->collect(runtime_state_.get(), splits, tablet_schema,
expr_contexts, nullptr);
+
+ ASSERT_TRUE(status.ok()) << status.msg();
+
expect_no_collected_tokens(segment_v2::inverted_index::StringHelper::to_wstring(
+ std::to_string(kVariantUid) + ".v.host"));
+}
+
TEST_F(CollectionStatisticsTest, CollectWithMultipleRowsetSplits) {
auto tablet_schema = create_tablet_schema_with_inverted_index();
auto expr_contexts = create_match_expr_contexts();
@@ -2382,10 +2445,9 @@ TEST_F(CollectionStatisticsTest,
CollectNotExtractedColumnSkipsFallback) {
EXPECT_TRUE(collect_infos.empty());
}
-// L1: Index whose properties do not request an analyzer
-// (should_analyzer returns false). The matching index_meta is iterated
-// but skipped before insertion.
-TEST_F(CollectionStatisticsTest, CollectSkipsIndexWithoutAnalyzer) {
+// L1: Index whose properties do not request an analyzer (should_analyzer
returns false). A keyword
+// MATCH scores like SEARCH on the same field, so its whole value is collected
as one term.
+TEST_F(CollectionStatisticsTest, CollectsKeywordIndexValueAsOneTerm) {
auto tablet_schema = std::make_shared<TabletSchema>();
constexpr int32_t kColUid = 1300;
@@ -2412,7 +2474,7 @@ TEST_F(CollectionStatisticsTest,
CollectSkipsIndexWithoutAnalyzer) {
auto match_expr =
std::make_shared<collection_statistics::MockVExpr>(TExprNodeType::MATCH_PRED);
auto slot_ref =
std::make_shared<collection_statistics::MockVSlotRef>("kw", SlotId(kSlotId));
- auto literal = std::make_shared<collection_statistics::MockVLiteral>("v");
+ auto literal =
std::make_shared<collection_statistics::MockVLiteral>("Hello World");
match_expr->_children.push_back(slot_ref);
match_expr->_children.push_back(literal);
auto analyzer_ctx = std::make_shared<InvertedIndexAnalyzerCtx>();
@@ -2420,12 +2482,118 @@ TEST_F(CollectionStatisticsTest,
CollectSkipsIndexWithoutAnalyzer) {
analyzer_ctx->parser_type = InvertedIndexParserType::PARSER_NONE;
match_expr->set_analyzer_ctx(std::move(analyzer_ctx));
- MatchPredicateCollector collector;
- std::unordered_map<std::wstring, CollectInfo> collect_infos;
- auto status =
- collector.collect(runtime_state_.get(), tablet_schema, match_expr,
&collect_infos);
- ASSERT_TRUE(status.ok()) << status.msg();
- EXPECT_TRUE(collect_infos.empty());
+ for (auto op : {TExprOpcode::MATCH_ANY, TExprOpcode::MATCH_ALL,
TExprOpcode::MATCH_PHRASE}) {
+ SCOPED_TRACE(op);
+ match_expr->set_opcode(op);
+ MatchPredicateCollector collector;
+ std::unordered_map<std::wstring, CollectInfo> collect_infos;
+ auto status =
+ collector.collect(runtime_state_.get(), tablet_schema,
match_expr, &collect_infos);
+ ASSERT_TRUE(status.ok()) << status.msg();
+ ASSERT_EQ(collect_infos.size(), 1U);
+ const auto& collect_info = collect_infos.begin()->second;
+ ASSERT_NE(collect_info.index_meta, nullptr);
+ EXPECT_EQ(collect_info.index_meta->index_id(), 2300);
+ EXPECT_EQ(collect_info.unique_terms, std::vector<std::string>({"Hello
World"}));
+ ASSERT_EQ(collect_info.logical_scoring_leaves.size(), 1U);
+ EXPECT_EQ(collect_info.logical_scoring_leaves[0].clauses.size(), 1U);
+ }
+}
+
+// A keyword MATCH_PHRASE reads a trailing " ~N" or " ~N+" as its slop and
looks up the rest, so the
+// statistics must be collected for that term too; any other MATCH looks up
the whole value.
+TEST_F(CollectionStatisticsTest, KeywordPhraseCollectsTermWithoutSlop) {
+ auto tablet_schema = std::make_shared<TabletSchema>();
+
+ constexpr int32_t kColUid = 1301;
+ TabletColumn col;
+ col.set_unique_id(kColUid);
+ col.set_name("kw");
+ col.set_type(FieldType::OLAP_FIELD_TYPE_STRING);
+ tablet_schema->append_column(col);
+
+ TabletIndexPB index_pb;
+ index_pb.set_index_id(2301);
+ index_pb.set_index_name("kw_idx");
+ index_pb.set_index_type(IndexType::INVERTED);
+ index_pb.add_col_unique_id(kColUid);
+ TabletIndex index;
+ index.init_from_pb(index_pb);
+ tablet_schema->append_index(std::move(index));
+
+ constexpr int kSlotId = 81;
+ runtime_state_->_mock_desc_tbl->add_slot_descriptor(SlotId(kSlotId),
kColUid, "kw", {});
+
+ struct Case {
+ TExprOpcode::type op;
+ std::string value;
+ std::string term;
+ };
+ for (const auto& c : {Case {TExprOpcode::MATCH_PHRASE, "Hello World ~1",
"Hello World"},
+ Case {TExprOpcode::MATCH_PHRASE, "Hello World ~2+",
"Hello World"},
+ Case {TExprOpcode::MATCH_ANY, "Hello World ~1",
"Hello World ~1"}}) {
+ SCOPED_TRACE(c.value);
+ auto match_expr =
+
std::make_shared<collection_statistics::MockVExpr>(TExprNodeType::MATCH_PRED);
+ match_expr->_children.push_back(
+ std::make_shared<collection_statistics::MockVSlotRef>("kw",
SlotId(kSlotId)));
+ match_expr->_children.push_back(
+
std::make_shared<collection_statistics::MockVLiteral>(c.value));
+ auto analyzer_ctx = std::make_shared<InvertedIndexAnalyzerCtx>();
+ analyzer_ctx->parser_type = InvertedIndexParserType::PARSER_NONE;
+ match_expr->set_analyzer_ctx(std::move(analyzer_ctx));
+ match_expr->set_opcode(c.op);
+
+ MatchPredicateCollector collector;
+ std::unordered_map<std::wstring, CollectInfo> collect_infos;
+ auto status =
+ collector.collect(runtime_state_.get(), tablet_schema,
match_expr, &collect_infos);
+ ASSERT_TRUE(status.ok()) << status.msg();
+ ASSERT_EQ(collect_infos.size(), 1U);
+ EXPECT_EQ(collect_infos.begin()->second.unique_terms,
std::vector<std::string>({c.term}));
+ }
+}
+
+// CAST(n AS STRING) MATCH '1' on an INT column runs as a scalar filter. The
index selected for n is
+// a BKD index without terms, so the predicate collects nothing instead of
opening it as text.
+TEST_F(CollectionStatisticsTest, MatchOnNumericColumnCollectsNothing) {
+ auto tablet_schema = std::make_shared<TabletSchema>();
+
+ constexpr int32_t kColUid = 1302;
+ TabletColumn col;
+ col.set_unique_id(kColUid);
+ col.set_name("n");
+ col.set_type(FieldType::OLAP_FIELD_TYPE_INT);
+ tablet_schema->append_column(col);
+
+ TabletIndexPB index_pb;
+ index_pb.set_index_id(2302);
+ index_pb.set_index_name("n_idx");
+ index_pb.set_index_type(IndexType::INVERTED);
+ index_pb.add_col_unique_id(kColUid);
+ TabletIndex index;
+ index.init_from_pb(index_pb);
+ tablet_schema->append_index(std::move(index));
+
+ constexpr int kSlotId = 82;
+ runtime_state_->_mock_desc_tbl->add_slot_descriptor(SlotId(kSlotId),
kColUid, "n", {});
+
+ auto match_expr =
std::make_shared<collection_statistics::MockVExpr>(TExprNodeType::MATCH_PRED);
+ match_expr->_children.push_back(
+ std::make_shared<collection_statistics::MockVSlotRef>("n",
SlotId(kSlotId)));
+
match_expr->_children.push_back(std::make_shared<collection_statistics::MockVLiteral>("1"));
+ match_expr->set_analyzer_ctx(std::make_shared<InvertedIndexAnalyzerCtx>());
+
+ for (auto op : {TExprOpcode::MATCH_ANY, TExprOpcode::MATCH_ALL,
TExprOpcode::MATCH_PHRASE}) {
+ SCOPED_TRACE(op);
+ match_expr->set_opcode(op);
+ MatchPredicateCollector collector;
+ std::unordered_map<std::wstring, CollectInfo> collect_infos;
+ auto 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,
ExplicitNoneDoesNotSelectNormalizerIndexForScoring) {
diff --git
a/be/test/storage/index/inverted/snii/reader/snii_segment_reader_logical_indexes_test.cpp
b/be/test/storage/index/inverted/snii/reader/snii_segment_reader_logical_indexes_test.cpp
index c97e13c5478..7fd61ed5542 100644
---
a/be/test/storage/index/inverted/snii/reader/snii_segment_reader_logical_indexes_test.cpp
+++
b/be/test/storage/index/inverted/snii/reader/snii_segment_reader_logical_indexes_test.cpp
@@ -112,6 +112,15 @@ TEST_F(SniiSegmentReaderLogicalIndexesTest,
ListsEveryLogicalIndex) {
EXPECT_TRUE(saw_positions);
}
+TEST_F(SniiSegmentReaderLogicalIndexesTest,
FindsDirectoryEntryWithoutCopyingIt) {
+ const std::vector<LogicalIndexMetadataRef>& entries =
segment_.logical_indexes();
+ for (const LogicalIndexMetadataRef& entry : entries) {
+ EXPECT_EQ(&entry, segment_.find_logical_index(entry.index_id,
entry.index_suffix));
+ }
+ EXPECT_EQ(nullptr, segment_.find_logical_index(7, "body"));
+ EXPECT_EQ(nullptr, segment_.find_logical_index(8, ""));
+}
+
TEST_F(SniiSegmentReaderLogicalIndexesTest, ReadsCoreMetadataOfEachIndex) {
CoreMetadata docs_only;
ASSERT_TRUE(segment_.core_metadata_for_index(7, "", &docs_only).ok());
diff --git a/be/test/storage/segment/index_reader_helper_test.cpp
b/be/test/storage/segment/index_reader_helper_test.cpp
index 2fb699529ec..5cb3d0e604f 100644
--- a/be/test/storage/segment/index_reader_helper_test.cpp
+++ b/be/test/storage/segment/index_reader_helper_test.cpp
@@ -447,17 +447,20 @@ TEST_F(IndexReaderHelperTest,
RegexpScoresConstantOnEveryIndex) {
}
}
-TEST_F(IndexReaderHelperTest, MatchScoringRequiresAnalyzedPhraseIndex) {
+TEST_F(IndexReaderHelperTest,
MatchScoringRequiresKeywordOrAnalyzedPhraseIndex) {
struct Case {
std::map<std::string, std::string> properties;
bool expected;
};
+ // Keyword indexes score with unit frequency and length, so phrase support
does not matter;
+ // analyzed indexes still need it to score.
for (const auto& test : std::vector<Case> {
- {.properties = {{"support_phrase", "true"}}, .expected =
false},
- {.properties = {{"parser", "none"}, {"support_phrase",
"true"}},
- .expected = false},
+ {.properties = {}, .expected = true},
+ {.properties = {{"support_phrase", "true"}}, .expected =
true},
+ {.properties = {{"parser", "none"}}, .expected = true},
+ {.properties = {{"parser", "none"}, {"support_phrase",
"true"}}, .expected = true},
{.properties = {{"parser", "unknown"}, {"support_phrase",
"true"}},
- .expected = false},
+ .expected = true},
{.properties = {{"normalizer", "lowercase"},
{"support_phrase", "true"}},
.expected = true},
{.properties = {{"parser", "english"}, {"support_phrase",
"true"}},
diff --git
a/regression-test/data/inverted_index_p0/test_match_keyword_score.out
b/regression-test/data/inverted_index_p0/test_match_keyword_score.out
new file mode 100644
index 00000000000..5c3b79d48e2
--- /dev/null
+++ b/regression-test/data/inverted_index_p0/test_match_keyword_score.out
@@ -0,0 +1,253 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !V2_single_row_match --
+1 0.287682
+
+-- !V2_match --
+1 0.693147
+2 0.693147
+5 0.693147
+
+-- !V2_match_all --
+1 0.693147
+2 0.693147
+5 0.693147
+
+-- !V2_match_phrase --
+1 0.693147
+2 0.693147
+5 0.693147
+
+-- !V2_match_phrase_slop --
+1 0.693147
+2 0.693147
+5 0.693147
+
+-- !V2_match_phrase_ordered_slop --
+1 0.693147
+2 0.693147
+5 0.693147
+
+-- !V2_search --
+1 0.693147
+2 0.693147
+5 0.693147
+
+-- !V2_none_match --
+1 0.693147
+3 0.693147
+6 0.693147
+
+-- !V2_none_match_phrase --
+1 0.693147
+3 0.693147
+6 0.693147
+
+-- !V2_none_search --
+1 0.693147
+3 0.693147
+6 0.693147
+
+-- !V2_analyzed_and_keyword --
+1 0.983863
+2 0.893357
+5 0.961783
+
+-- !V2_search_and --
+1 0.983863
+2 0.893357
+5 0.961783
+
+-- !V2_analyzed_and_equality --
+1 0.290716
+2 0.20021
+5 0.268636
+
+-- !V2_analyzed_and_numeric_cast --
+1 0.290716
+4 0.268636
+
+-- !V2_variant_numeric_path --
+1 0
+3 0
+4 0.287682
+
+-- !V2_variant_text_path --
+1 0.693147
+4 0.693147
+
+-- !V2_variant_sparse_path --
+1 0.980829
+
+-- !V2_variant_numeric_path_cached_bkd --
+1 0
+3 0
+4 0.287682
+
+-- !V3_single_row_match --
+1 0.287682
+
+-- !V3_match --
+1 0.693147
+2 0.693147
+5 0.693147
+
+-- !V3_match_all --
+1 0.693147
+2 0.693147
+5 0.693147
+
+-- !V3_match_phrase --
+1 0.693147
+2 0.693147
+5 0.693147
+
+-- !V3_match_phrase_slop --
+1 0.693147
+2 0.693147
+5 0.693147
+
+-- !V3_match_phrase_ordered_slop --
+1 0.693147
+2 0.693147
+5 0.693147
+
+-- !V3_search --
+1 0.693147
+2 0.693147
+5 0.693147
+
+-- !V3_none_match --
+1 0.693147
+3 0.693147
+6 0.693147
+
+-- !V3_none_match_phrase --
+1 0.693147
+3 0.693147
+6 0.693147
+
+-- !V3_none_search --
+1 0.693147
+3 0.693147
+6 0.693147
+
+-- !V3_analyzed_and_keyword --
+1 0.983863
+2 0.893357
+5 0.961783
+
+-- !V3_search_and --
+1 0.983863
+2 0.893357
+5 0.961783
+
+-- !V3_analyzed_and_equality --
+1 0.290716
+2 0.20021
+5 0.268636
+
+-- !V3_analyzed_and_numeric_cast --
+1 0.290716
+4 0.268636
+
+-- !V3_variant_numeric_path --
+1 0
+3 0
+4 0.287682
+
+-- !V3_variant_text_path --
+1 0.693147
+4 0.693147
+
+-- !V3_variant_sparse_path --
+1 0.980829
+
+-- !V3_variant_numeric_path_cached_bkd --
+1 0
+3 0
+4 0.287682
+
+-- !SNII_single_row_match --
+1 0.287682
+
+-- !SNII_match --
+1 0.640724
+2 0.640724
+5 0.640724
+
+-- !SNII_match_all --
+1 0.640724
+2 0.640724
+5 0.640724
+
+-- !SNII_match_phrase --
+1 0.640724
+2 0.640724
+5 0.640724
+
+-- !SNII_match_phrase_slop --
+1 0.640724
+2 0.640724
+5 0.640724
+
+-- !SNII_match_phrase_ordered_slop --
+1 0.640724
+2 0.640724
+5 0.640724
+
+-- !SNII_search --
+1 0.640724
+2 0.640724
+5 0.640724
+
+-- !SNII_none_match --
+1 0.640724
+3 0.640724
+6 0.640724
+
+-- !SNII_none_match_phrase --
+1 0.640724
+3 0.640724
+6 0.640724
+
+-- !SNII_none_search --
+1 0.640724
+3 0.640724
+6 0.640724
+
+-- !SNII_analyzed_and_keyword --
+1 0.93144
+2 0.840934
+5 0.90936
+
+-- !SNII_search_and --
+1 0.93144
+2 0.840934
+5 0.90936
+
+-- !SNII_analyzed_and_equality --
+1 0.290716
+2 0.20021
+5 0.268636
+
+-- !SNII_analyzed_and_numeric_cast --
+1 0.290716
+4 0.268636
+
+-- !SNII_variant_numeric_path --
+1 0
+3 0
+4 0.287682
+
+-- !SNII_variant_text_path --
+1 0.693147
+4 0.693147
+
+-- !SNII_variant_sparse_path --
+1 0.980829
+
+-- !SNII_variant_numeric_path_cached_bkd --
+1 0
+3 0
+4 0.287682
+
diff --git a/regression-test/data/search/test_search_score_keyword_filters.out
b/regression-test/data/search/test_search_score_keyword_filters.out
index 5b1d6730b55..8175ca310c2 100644
--- a/regression-test/data/search/test_search_score_keyword_filters.out
+++ b/regression-test/data/search/test_search_score_keyword_filters.out
@@ -12,12 +12,12 @@
4 0.268636 true
-- !V2_false_keyword_match --
-1 0.290716 true
-4 0.268636 true
+1 0.983863 true
+4 0.961783 true
-- !V2_false_keyword_match_all --
-1 0.290716 true
-4 0.268636 true
+1 0.983863 true
+4 0.961783 true
-- !V2_true_internal_and --
1 0.983863 true
@@ -32,12 +32,12 @@
4 0.268636 true
-- !V2_true_keyword_match --
-1 0.290716 true
-4 0.268636 true
+1 0.983863 true
+4 0.961783 true
-- !V2_true_keyword_match_all --
-1 0.290716 true
-4 0.268636 true
+1 0.983863 true
+4 0.961783 true
-- !explicit_none_V2_false_internal_and --
1 0.983863 true
@@ -52,12 +52,12 @@
4 0.268636 true
-- !explicit_none_V2_false_keyword_match --
-1 0.290716 true
-4 0.268636 true
+1 0.983863 true
+4 0.961783 true
-- !explicit_none_V2_false_keyword_match_all --
-1 0.290716 true
-4 0.268636 true
+1 0.983863 true
+4 0.961783 true
-- !explicit_none_V2_true_internal_and --
1 0.983863 true
@@ -72,12 +72,12 @@
4 0.268636 true
-- !explicit_none_V2_true_keyword_match --
-1 0.290716 true
-4 0.268636 true
+1 0.983863 true
+4 0.961783 true
-- !explicit_none_V2_true_keyword_match_all --
-1 0.290716 true
-4 0.268636 true
+1 0.983863 true
+4 0.961783 true
-- !V3_false_internal_and --
1 0.983863 true
@@ -92,12 +92,12 @@
4 0.268636 true
-- !V3_false_keyword_match --
-1 0.290716 true
-4 0.268636 true
+1 0.983863 true
+4 0.961783 true
-- !V3_false_keyword_match_all --
-1 0.290716 true
-4 0.268636 true
+1 0.983863 true
+4 0.961783 true
-- !V3_true_internal_and --
1 0.983863 true
@@ -112,12 +112,12 @@
4 0.268636 true
-- !V3_true_keyword_match --
-1 0.290716 true
-4 0.268636 true
+1 0.983863 true
+4 0.961783 true
-- !V3_true_keyword_match_all --
-1 0.290716 true
-4 0.268636 true
+1 0.983863 true
+4 0.961783 true
-- !explicit_none_V3_false_internal_and --
1 0.983863 true
@@ -132,12 +132,12 @@
4 0.268636 true
-- !explicit_none_V3_false_keyword_match --
-1 0.290716 true
-4 0.268636 true
+1 0.983863 true
+4 0.961783 true
-- !explicit_none_V3_false_keyword_match_all --
-1 0.290716 true
-4 0.268636 true
+1 0.983863 true
+4 0.961783 true
-- !explicit_none_V3_true_internal_and --
1 0.983863 true
@@ -152,12 +152,12 @@
4 0.268636 true
-- !explicit_none_V3_true_keyword_match --
-1 0.290716 true
-4 0.268636 true
+1 0.983863 true
+4 0.961783 true
-- !explicit_none_V3_true_keyword_match_all --
-1 0.290716 true
-4 0.268636 true
+1 0.983863 true
+4 0.961783 true
-- !SNII_false_internal_and --
1 0.93144 true
@@ -172,12 +172,12 @@
4 0.268636 true
-- !SNII_false_keyword_match --
-1 0.290716 true
-4 0.268636 true
+1 0.93144 true
+4 0.90936 true
-- !SNII_false_keyword_match_all --
-1 0.290716 true
-4 0.268636 true
+1 0.93144 true
+4 0.90936 true
-- !SNII_true_internal_and --
1 0.93144 true
@@ -192,12 +192,12 @@
4 0.268636 true
-- !SNII_true_keyword_match --
-1 0.290716 true
-4 0.268636 true
+1 0.93144 true
+4 0.90936 true
-- !SNII_true_keyword_match_all --
-1 0.290716 true
-4 0.268636 true
+1 0.93144 true
+4 0.90936 true
-- !explicit_none_SNII_false_internal_and --
1 0.93144 true
@@ -212,12 +212,12 @@
4 0.268636 true
-- !explicit_none_SNII_false_keyword_match --
-1 0.290716 true
-4 0.268636 true
+1 0.93144 true
+4 0.90936 true
-- !explicit_none_SNII_false_keyword_match_all --
-1 0.290716 true
-4 0.268636 true
+1 0.93144 true
+4 0.90936 true
-- !explicit_none_SNII_true_internal_and --
1 0.93144 true
@@ -232,9 +232,10 @@
4 0.268636 true
-- !explicit_none_SNII_true_keyword_match --
-1 0.290716 true
-4 0.268636 true
+1 0.93144 true
+4 0.90936 true
-- !explicit_none_SNII_true_keyword_match_all --
-1 0.290716 true
-4 0.268636 true
+1 0.93144 true
+4 0.90936 true
+
diff --git
a/regression-test/suites/inverted_index_p0/test_match_keyword_score.groovy
b/regression-test/suites/inverted_index_p0/test_match_keyword_score.groovy
new file mode 100644
index 00000000000..32b5a58ef69
--- /dev/null
+++ b/regression-test/suites/inverted_index_p0/test_match_keyword_score.groovy
@@ -0,0 +1,146 @@
+// 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() over MATCH on a keyword (untokenized) inverted index. Like an
Elasticsearch match or term
+// query on a keyword field, it scores BM25 with unit frequency and length, so
every matching row
+// scores the idf of the whole value, the same as SEARCH on that field; it
used to score 0. An
+// equality predicate stays a pure filter.
+suite("test_match_keyword_score", "p0") {
+ // MATCH and SEARCH need index execution even when CI fuzzes these session
defaults.
+ sql "set enable_common_expr_pushdown = true"
+ sql "set enable_inverted_index_query = true"
+ sql "set enable_match_without_inverted_index = true"
+
+ for (def storageFormat : ["V2", "V3", "SNII"]) {
+ sql "DROP TABLE IF EXISTS test_match_keyword_score"
+ sql """
+ CREATE TABLE test_match_keyword_score (
+ id INT NOT NULL,
+ body STRING NULL,
+ tag STRING NULL,
+ tag_none STRING NULL,
+ n INT NULL,
+ INDEX idx_body(body) USING INVERTED
+ PROPERTIES("parser" = "english", "support_phrase" =
"true"),
+ INDEX idx_tag(tag) USING INVERTED PROPERTIES("ignore_above" =
"1024"),
+ INDEX idx_tag_none(tag_none) USING INVERTED
+ PROPERTIES("parser" = "none", "support_phrase" = "true"),
+ INDEX idx_n(n) 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}"
+ )
+ """
+ // The first row alone is the reported case: one document scores idf =
ln(4/3) = 0.287682.
+ sql """INSERT INTO test_match_keyword_score VALUES (1, 'apple apple',
'BP', 'Hello World', 1)"""
+ sql "sync"
+ quickTest("${storageFormat}_single_row_match", """
+ SELECT id, ROUND(s, 6) FROM (
+ SELECT id, score() AS s FROM test_match_keyword_score
+ WHERE tag MATCH 'BP' ORDER BY s DESC LIMIT 10
+ ) hits ORDER BY id
+ """, true)
+
+ sql """INSERT INTO test_match_keyword_score VALUES
+ (2, 'apple pear', 'BP', 'hello world', 2), (3, 'pear', 'other',
'Hello World', 1),
+ (4, 'apple', 'x', 'Hello', 1), (5, 'apple', 'BP', NULL, NULL),
+ (6, 'apple', NULL, 'Hello World', 2)
+ """
+ sql "sync"
+
+ // The whole value is one term: MATCH, MATCH_ALL and MATCH_PHRASE
score as SEARCH does.
+ for (def query : [
+ ["match", "tag MATCH 'BP'"],
+ ["match_all", "tag MATCH_ALL 'BP'"],
+ ["match_phrase", "tag MATCH_PHRASE 'BP'"],
+ // A trailing " ~N" is the phrase slop, not part of the term:
it scores BP.
+ ["match_phrase_slop", "tag MATCH_PHRASE 'BP ~1'"],
+ ["match_phrase_ordered_slop", "tag MATCH_PHRASE 'BP ~1+'"],
+ ["search", "SEARCH('tag:BP')"],
+ // Case and spaces are kept, and phrase support inherited by
parser=none is ignored.
+ ["none_match", "tag_none MATCH 'Hello World'"],
+ ["none_match_phrase", "tag_none MATCH_PHRASE 'Hello World'"],
+ ["none_search", "SEARCH('tag_none:\"Hello World\"')"],
+ // Both leaves score: idf(apple) from body plus idf(BP) from
tag.
+ ["analyzed_and_keyword", "body MATCH 'apple' AND tag MATCH
'BP'"],
+ ["search_and", "SEARCH('body:apple AND tag:BP')"],
+ // An equality is a filter: only the body leaf scores.
+ ["analyzed_and_equality", "body MATCH 'apple' AND tag = 'BP'"],
+ // A MATCH on a number cast to text is a filter over the BKD
index of n: only the
+ // body leaf scores.
+ ["analyzed_and_numeric_cast", "body MATCH 'apple' AND CAST(n
AS STRING) MATCH '1'"]]) {
+ quickTest("${storageFormat}_${query[0]}", """
+ SELECT id, ROUND(s, 6) FROM (
+ SELECT id, score() AS s FROM test_match_keyword_score
+ WHERE ${query[1]} ORDER BY s DESC LIMIT 10
+ ) hits ORDER BY id
+ """, true)
+ }
+
+ // A VARIANT path is typed per segment: n holds numbers, indexed as
BKD, in the first
+ // insert and text in the second. The plain VARIANT index makes every
path a keyword
+ // index, so CAST(v['n'] AS STRING) MATCH collects statistics for n;
the BKD segment adds
+ // none instead of failing, and its rows match without the index with
score 0. p exists
+ // only in the first insert: the other segment has no index for it and
adds nothing.
+ sql "set default_variant_enable_typed_paths_to_sparse = false"
+ sql "set default_variant_enable_doc_mode = false"
+ sql "DROP TABLE IF EXISTS test_match_keyword_score_variant"
+ sql """
+ CREATE TABLE test_match_keyword_score_variant (
+ id INT NOT NULL,
+ v VARIANT NULL,
+ INDEX idx_v(v) 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"
+ )
+ """
+ sql """INSERT INTO test_match_keyword_score_variant VALUES
+ (1, '{"n": 1, "s": "abc", "p": "abc"}'), (2, '{"n": 2, "s": "abd",
"p": "abd"}'),
+ (3, '{"n": 1, "s": "xyz", "p": "xyz"}')
+ """
+ sql """INSERT INTO test_match_keyword_score_variant VALUES (4, '{"n":
"1", "s": "abc"}')"""
+ sql "sync"
+ for (def query : [
+ ["variant_numeric_path", "CAST(v['n'] AS STRING) MATCH '1'"],
+ ["variant_text_path", "CAST(v['s'] AS STRING) MATCH 'abc'"],
+ ["variant_sparse_path", "CAST(v['p'] AS STRING) MATCH
'abc'"]]) {
+ quickTest("${storageFormat}_${query[0]}", """
+ SELECT id, ROUND(s, 6) FROM (
+ SELECT id, score() AS s FROM
test_match_keyword_score_variant
+ WHERE ${query[1]} ORDER BY s DESC LIMIT 10
+ ) hits ORDER BY id
+ """, true)
+ }
+ // A numeric predicate leaves the BKD searcher of n in the searcher
cache, where collecting
+ // statistics finds it next: it used to take it for a text searcher
and abort the BE.
+ sql "SELECT COUNT(*) FROM test_match_keyword_score_variant WHERE
CAST(v['n'] AS BIGINT) = 1"
+ quickTest("${storageFormat}_variant_numeric_path_cached_bkd", """
+ SELECT id, ROUND(s, 6) FROM (
+ SELECT id, score() AS s FROM test_match_keyword_score_variant
+ WHERE CAST(v['n'] AS STRING) MATCH '1' ORDER BY s DESC LIMIT 10
+ ) hits ORDER BY id
+ """, true)
+ }
+}
diff --git
a/regression-test/suites/search/test_search_score_keyword_filters.groovy
b/regression-test/suites/search/test_search_score_keyword_filters.groovy
index a1d41ab4861..1e89bcb2bfb 100644
--- a/regression-test/suites/search/test_search_score_keyword_filters.groovy
+++ b/regression-test/suites/search/test_search_score_keyword_filters.groovy
@@ -66,7 +66,8 @@ suite("test_search_score_keyword_filters", "p0") {
["equality", "tag = 'x'"],
["keyword_match", "tag MATCH_ANY 'x'"],
["keyword_match_all", "tag MATCH_ALL 'x'"]]) {
- // The keyword predicate filters rows; only the SEARCH
leaf contributes a score.
+ // IN and equality only filter rows; a keyword MATCH also
adds the idf of its
+ // value, as an Elasticsearch match on a keyword field
does.
quickTest("${variant}${storageFormat}_${wand}_${filter[0]}", """
SELECT id, ROUND(bm25_score, 6), bm25_score > 0 AND
bm25_score < 1000000
FROM (
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]