This is an automated email from the ASF dual-hosted git repository.
airborne12 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 7a9d745388b [fix](inverted index) Align phrase position defaults with
analysis (#69199)
7a9d745388b is described below
commit 7a9d745388bd8b71cedcbc91c95747cd4758ab27
Author: Jack <[email protected]>
AuthorDate: Sun Oct 18 20:33:12 2026 +0800
[fix](inverted index) Align phrase position defaults with analysis (#69199)
### What problem does this PR solve?
Issue Number: CIR-22263
Related PR: #69192
Problem Summary: FE persists `support_phrase=true` for new tokenized
indexes and leaves exact indexes docs-only. BE must treat an omitted
property as false because older persisted analyzer indexes can omit it
despite having docs-only postings; this also preserves `TOKENIZE`
output. Built-in `lowercase` normalization emits one term, so phrase
predicates can use docs-only postings and BM25 can use unit frequency
and document length. Custom normalizers can emit multiple terms, so new
indexes retain positions by default, including when a legacy custom
policy shadows the built-in name.
This PR changes phrase-position defaults and normalizer compatibility.
PR #69192 adds direct SNII compaction of compatible docs-only postings.
Neither change addresses the tokenized T2 merge hotspot identified in
CIR-22263.
---
be/src/storage/index/index_reader_helper.h | 25 +++++++--
.../inverted/clucene/spi/clucene_index_source.cpp | 1 +
.../inverted/clucene/spi/clucene_postings_cursor.h | 9 ++-
.../inverted/reader/collection_statistics.cpp | 30 +++++++---
.../index/inverted/reader/collection_statistics.h | 10 ++--
.../analysis/inverted_index_parser_test.cpp | 23 ++++++++
.../query/engine/boolean_postings_bench_test.cpp | 4 +-
.../query/engine/multi_segment_collector_test.cpp | 5 +-
.../inverted/query/exec/block_doc_set_test.cpp | 16 ++++++
.../inverted/reader/collection_statistics_test.cpp | 65 ++++++++++++++++++----
.../index/inverted/snii/snii_writer_test.cpp | 51 +++++++++++++++++
.../storage/segment/index_reader_helper_test.cpp | 43 ++++++++++++++
.../main/java/org/apache/doris/catalog/Index.java | 11 ++--
.../apache/doris/analysis/InvertedIndexUtil.java | 13 ++++-
.../analysis/InvertedIndexPropertiesTest.java | 5 +-
.../java/org/apache/doris/catalog/IndexTest.java | 35 ++++++++++++
.../suites/index_p0/test_index_meta.groovy | 4 +-
17 files changed, 307 insertions(+), 43 deletions(-)
diff --git a/be/src/storage/index/index_reader_helper.h
b/be/src/storage/index/index_reader_helper.h
index bd2aacdabb1..c1b0b65008d 100644
--- a/be/src/storage/index/index_reader_helper.h
+++ b/be/src/storage/index/index_reader_helper.h
@@ -17,6 +17,10 @@
#pragma once
+#include <boost/algorithm/string/predicate.hpp>
+#include <map>
+#include <string>
+
#include "storage/index/index_iterator.h"
#include "storage/index/inverted/inverted_index_reader.h"
@@ -24,6 +28,16 @@ namespace doris::segment_v2 {
class IndexReaderHelper {
public:
+ static bool is_normalizer_only(const std::map<std::string, std::string>&
properties) {
+ const auto normalizer =
properties.find(INVERTED_INDEX_NORMALIZER_NAME_KEY);
+ const auto analyzer =
properties.find(INVERTED_INDEX_ANALYZER_NAME_KEY);
+ // A custom normalizer can split a value into multiple terms that need
positions.
+ return !properties.contains(INVERTED_INDEX_PARSER_PHRASE_SUPPORT_KEY)
&&
+ normalizer != properties.end() &&
boost::iequals(normalizer->second, "lowercase") &&
+ (analyzer == properties.end() || analyzer->second.empty()) &&
+ get_parser_string_from_properties(properties) ==
INVERTED_INDEX_PARSER_NONE;
+ }
+
static bool is_fulltext_index(const IndexReaderPtr& reader) {
if (reader == nullptr || reader->index_type() != IndexType::INVERTED) {
return false;
@@ -59,7 +73,8 @@ public:
auto inverted_index_reader =
std::static_pointer_cast<InvertedIndexReader>(reader);
const auto& properties = inverted_index_reader->get_index_properties();
return get_parser_phrase_support_string_from_properties(properties) ==
- INVERTED_INDEX_PARSER_PHRASE_SUPPORT_YES;
+ INVERTED_INDEX_PARSER_PHRASE_SUPPORT_YES ||
+ is_normalizer_only(properties);
}
// only string type or bkd index reader can be used for equal
@@ -96,7 +111,8 @@ public:
query_type == InvertedIndexQueryType::MATCH_PHRASE_PREFIX_QUERY) {
const auto& properties = index_meta->properties();
if (get_parser_phrase_support_string_from_properties(properties) ==
- INVERTED_INDEX_PARSER_PHRASE_SUPPORT_YES) {
+ INVERTED_INDEX_PARSER_PHRASE_SUPPORT_YES ||
+ is_normalizer_only(properties)) {
return true;
}
}
@@ -110,7 +126,8 @@ public:
query_type == TExprOpcode::MATCH_PHRASE_PREFIX) {
const auto& properties = index_meta->properties();
if (get_parser_phrase_support_string_from_properties(properties) ==
- INVERTED_INDEX_PARSER_PHRASE_SUPPORT_YES) {
+ INVERTED_INDEX_PARSER_PHRASE_SUPPORT_YES ||
+ is_normalizer_only(properties)) {
return true;
}
}
@@ -118,4 +135,4 @@ public:
}
};
-} // namespace doris::segment_v2
\ No newline at end of file
+} // namespace doris::segment_v2
diff --git a/be/src/storage/index/inverted/clucene/spi/clucene_index_source.cpp
b/be/src/storage/index/inverted/clucene/spi/clucene_index_source.cpp
index b7be72f4770..c509c07a2a6 100644
--- a/be/src/storage/index/inverted/clucene/spi/clucene_index_source.cpp
+++ b/be/src/storage/index/inverted/clucene/spi/clucene_index_source.cpp
@@ -68,6 +68,7 @@ ClucenePostingsCursor::Partition
open_postings(lucene::index::IndexReader* reade
lucene::index::Term* term, bool
positions,
bool scoring, const
io::IOContext* io_ctx) {
ClucenePostingsCursor::Partition result;
+ result.has_norms = reader->hasNorms(term->field());
if (positions) {
auto iter = make_term_positions_ptr(reader, scoring, io_ctx);
result.positions = iter.get();
diff --git
a/be/src/storage/index/inverted/clucene/spi/clucene_postings_cursor.h
b/be/src/storage/index/inverted/clucene/spi/clucene_postings_cursor.h
index 2786eb7a811..2bb3406eaad 100644
--- a/be/src/storage/index/inverted/clucene/spi/clucene_postings_cursor.h
+++ b/be/src/storage/index/inverted/clucene/spi/clucene_postings_cursor.h
@@ -39,12 +39,14 @@ public:
lucene::index::TermPositions* positions = nullptr;
uint32_t begin = 0;
uint32_t end = 0;
+ bool has_norms = true;
};
explicit ClucenePostingsCursor(Partition part)
: _single_iter(std::move(part.iter)),
_iter(_single_iter.get()),
- _raw_positions(part.positions) {
+ _raw_positions(part.positions),
+ _has_norms(part.has_norms) {
_check_iterator();
_doc_freq = _iter->docFreq();
}
@@ -87,12 +89,13 @@ public:
DORIS_CHECK(_block.freq_many != nullptr);
DORIS_CHECK_GE(_block.freq_many_size_,
_block.doc_many_size_);
}
+ // CLucene fills norm_many with zeroes when the field omits
norms.
*out = {.docs = {_block.doc_many->data(),
_block.doc_many_size_},
.freqs = _block.freq_many
? std::span<const
uint32_t>(_block.freq_many->data(),
_block.freq_many_size_)
: std::span<const uint32_t>(),
- .norms = _block.norm_many
+ .norms = _has_norms && _block.norm_many
? std::span<const
uint32_t>(_block.norm_many->data(),
_block.norm_many_size_)
: std::span<const uint32_t>()};
@@ -312,6 +315,7 @@ private:
_iter = part.iter.get();
_raw_positions = part.positions;
_doc_base = part.begin;
+ _has_norms = part.has_norms;
}
}
@@ -376,6 +380,7 @@ private:
const uint32_t* _freqs = nullptr;
uint32_t _prox_cursor = 0;
lucene::index::TermPositions* _raw_positions = nullptr;
+ bool _has_norms = true;
uint32_t _position_remaining = 0;
uint32_t _position = 0;
bool _position_open = false;
diff --git a/be/src/storage/index/inverted/reader/collection_statistics.cpp
b/be/src/storage/index/inverted/reader/collection_statistics.cpp
index 67c4ac348a5..276b1d2c2dd 100644
--- a/be/src/storage/index/inverted/reader/collection_statistics.cpp
+++ b/be/src/storage/index/inverted/reader/collection_statistics.cpp
@@ -43,8 +43,10 @@ namespace collection_statistics_detail {
Result<SniiScoringSegmentStats> resolve_snii_scoring_segment(uint64_t
index_doc_count,
uint64_t
sum_total_term_freq,
- bool
has_positions, bool has_norms) {
- if (!has_positions || !has_norms) {
+ bool
has_positions, bool has_norms,
+ bool
single_term_normalizer) {
+ // Lowercase-normalized docs-only postings use unit frequency and length
for scoring.
+ if ((!has_positions || !has_norms) && !single_term_normalizer) {
return
ResultError(Status::Error<ErrorCode::INVERTED_INDEX_NOT_SUPPORTED, false>(
"SNII scoring requires positions and norms; this segment was
written without "
"positions or without norms. Norms are left out when the index
sets \"norms\" = "
@@ -262,7 +264,10 @@ Status CollectionStatistics::process_segment(const
RowsetSharedPtr& rowset,
const uint64_t segment_doc_count = scoring_stats.doc_count;
RETURN_IF_ERROR(admit_snii_scoring_segment(
ws_field_name, segment_doc_count,
scoring_stats.token_count,
- scoring_stats.has_positions, scoring_stats.has_norms,
&segment_accumulator));
+ scoring_stats.has_positions, scoring_stats.has_norms,
+ segment_v2::IndexReaderHelper::is_normalizer_only(
+ collect_info.index_meta->properties()),
+ &segment_accumulator));
for (const auto& logical_term_bytes : collect_info.unique_terms) {
const auto logical_term =
@@ -332,9 +337,12 @@ Status CollectionStatistics::process_segment(const
RowsetSharedPtr& rowset,
// BM25 on an analyzed index needs the record length of every row, and
CLucene keeps
// them, together with the field's token count, in the norms. A
segment written without
// norms would feed a zero avgdl, or rank its rows as zero-length
documents next to the
- // segments that have norms, so refuse to score the collection, as
SNII does. An index
- // that is not analyzed never writes norms and is left as it is.
- if (!scoring_stats.has_norms &&
+ // segments that have norms, so refuse to score the collection, as
SNII does. A
+ // single-term normalizer can instead use unit frequency and length.
+ const bool single_term_normalizer =
segment_v2::IndexReaderHelper::is_normalizer_only(
+ collect_info.index_meta->properties());
+ // Docs-only lowercase indexes have no norms, but each matching row
has one term.
+ if (!scoring_stats.has_norms && !single_term_normalizer &&
segment_v2::inverted_index::InvertedIndexAnalyzer::should_analyzer(
collect_info.index_meta->properties())) {
return Status::Error<ErrorCode::INVERTED_INDEX_NOT_SUPPORTED>(
@@ -343,7 +351,10 @@ Status CollectionStatistics::process_segment(const
RowsetSharedPtr& rowset,
"for a variant path, when
inverted_index_skip_norms_for_variant is on",
seg_path,
segment_v2::inverted_index::StringHelper::to_string(ws_field_name));
}
- _total_num_tokens[ws_field_name] += scoring_stats.token_count;
+ // CLucene omits term totals without norms; one token per row gives a
unit-length estimate.
+ _total_num_tokens[ws_field_name] += !scoring_stats.has_norms &&
single_term_normalizer
+ ? scoring_stats.doc_count
+ :
scoring_stats.token_count;
for (const auto& logical_term_bytes : collect_info.unique_terms) {
const auto logical_term =
@@ -364,10 +375,11 @@ Status CollectionStatistics::process_segment(const
RowsetSharedPtr& rowset,
Status CollectionStatistics::admit_snii_scoring_segment(
const std::wstring& field_name, uint64_t index_doc_count, uint64_t
sum_total_term_freq,
- bool has_positions, bool has_norms, SniiScoringSegmentAccumulator*
segment_accumulator) {
+ bool has_positions, bool has_norms, bool single_term_normalizer,
+ SniiScoringSegmentAccumulator* segment_accumulator) {
DORIS_CHECK(segment_accumulator != nullptr);
auto segment_stats =
collection_statistics_detail::resolve_snii_scoring_segment(
- index_doc_count, sum_total_term_freq, has_positions, has_norms);
+ index_doc_count, sum_total_term_freq, has_positions, has_norms,
single_term_normalizer);
if (!segment_stats.has_value()) {
clear();
return segment_stats.error();
diff --git a/be/src/storage/index/inverted/reader/collection_statistics.h
b/be/src/storage/index/inverted/reader/collection_statistics.h
index 62e6e73ad26..e62258c2b81 100644
--- a/be/src/storage/index/inverted/reader/collection_statistics.h
+++ b/be/src/storage/index/inverted/reader/collection_statistics.h
@@ -86,7 +86,7 @@ private:
io::IOContext* io_ctx);
Status admit_snii_scoring_segment(const std::wstring& field_name, uint64_t
index_doc_count,
uint64_t sum_total_term_freq, bool
has_positions,
- bool has_norms,
+ bool has_norms, bool
single_term_normalizer,
SniiScoringSegmentAccumulator*
segment_accumulator);
void commit_snii_scoring_segment(SniiScoringSegmentAccumulator&&
segment_accumulator);
void clear();
@@ -121,12 +121,12 @@ struct SniiScoringSegmentStats {
uint64_t token_count = 0;
};
-// SNII scoring requires positions (which provide term frequencies) and norms.
The current writer
-// emits norms for every analyzed index with positions. Older segments without
norms return
-// NOT_SUPPORTED until an index rebuild or compaction supplies them.
+// Single-term normalizers can score docs-only postings with frequency and
length one.
+// Other analyzed indexes need positions and norms for accurate BM25 scores.
Result<SniiScoringSegmentStats> resolve_snii_scoring_segment(uint64_t
index_doc_count,
uint64_t
sum_total_term_freq,
- bool
has_positions, bool has_norms);
+ bool
has_positions, bool has_norms,
+ bool
single_term_normalizer = false);
void add_term_doc_frequency(
std::unordered_map<std::wstring, std::unordered_map<std::wstring,
uint64_t>>*
diff --git
a/be/test/storage/index/inverted/analysis/inverted_index_parser_test.cpp
b/be/test/storage/index/inverted/analysis/inverted_index_parser_test.cpp
index 30b065b24d5..526f61461d9 100644
--- a/be/test/storage/index/inverted/analysis/inverted_index_parser_test.cpp
+++ b/be/test/storage/index/inverted/analysis/inverted_index_parser_test.cpp
@@ -130,6 +130,29 @@ TEST_F(InvertedIndexParserTest,
TestGetParserPhraseSupportStringFromProperties)
EXPECT_EQ(get_parser_phrase_support_string_from_properties(properties),
INVERTED_INDEX_PARSER_PHRASE_SUPPORT_NO);
+ properties[INVERTED_INDEX_PARSER_KEY] = INVERTED_INDEX_PARSER_NONE;
+ EXPECT_EQ(get_parser_phrase_support_string_from_properties(properties),
+ INVERTED_INDEX_PARSER_PHRASE_SUPPORT_NO);
+
+ properties[INVERTED_INDEX_PARSER_KEY] = INVERTED_INDEX_PARSER_ENGLISH;
+ EXPECT_EQ(get_parser_phrase_support_string_from_properties(properties),
+ INVERTED_INDEX_PARSER_PHRASE_SUPPORT_NO);
+
+ properties.clear();
+ properties[INVERTED_INDEX_ANALYZER_NAME_KEY] = "english";
+ EXPECT_EQ(get_parser_phrase_support_string_from_properties(properties),
+ INVERTED_INDEX_PARSER_PHRASE_SUPPORT_NO);
+
+ properties.clear();
+ properties[INVERTED_INDEX_NORMALIZER_NAME_KEY] = "lowercase";
+ EXPECT_EQ(get_parser_phrase_support_string_from_properties(properties),
+ INVERTED_INDEX_PARSER_PHRASE_SUPPORT_NO);
+
+ properties.clear();
+ properties[INVERTED_INDEX_PARSER_KEY_ALIAS] = INVERTED_INDEX_PARSER_NONE;
+ EXPECT_EQ(get_parser_phrase_support_string_from_properties(properties),
+ INVERTED_INDEX_PARSER_PHRASE_SUPPORT_NO);
+
// Test with phrase support key present
properties[INVERTED_INDEX_PARSER_PHRASE_SUPPORT_KEY] =
INVERTED_INDEX_PARSER_PHRASE_SUPPORT_YES;
EXPECT_EQ(get_parser_phrase_support_string_from_properties(properties),
diff --git
a/be/test/storage/index/inverted/query/engine/boolean_postings_bench_test.cpp
b/be/test/storage/index/inverted/query/engine/boolean_postings_bench_test.cpp
index c19e4f9f4ab..6e1e2dfc717 100644
---
a/be/test/storage/index/inverted/query/engine/boolean_postings_bench_test.cpp
+++
b/be/test/storage/index/inverted/query/engine/boolean_postings_bench_test.cpp
@@ -202,7 +202,7 @@ uint64_t posting_row_checksum(uint32_t doc, float score,
bool scoring) {
uint64_t expected_posting_checksum(const PostingSpec& spec, uint32_t profile,
bool sparse,
bool scoring) {
BM25Similarity similarity(2.0F, 8.0F);
- const float term_score = similarity.score(1.0F, 0);
+ const float term_score = similarity.score(1.0F, 1);
uint64_t result = 0;
for (uint32_t doc = 0; doc < kPostingRows; ++doc) {
bool required = true;
@@ -255,7 +255,7 @@ PhysicalTruthRows expected_physical_truth(const
PostingSpec& spec, uint32_t prof
PhysicalTruthRows result;
result.scores.resize(kPostingRows);
BM25Similarity similarity(2.0F, 8.0F);
- const float term_score = similarity.score(1.0F, 0);
+ const float term_score = similarity.score(1.0F, 1);
for (uint32_t doc = 0; doc < kPostingRows; ++doc) {
bool required_true = true;
bool required_possible = true;
diff --git
a/be/test/storage/index/inverted/query/engine/multi_segment_collector_test.cpp
b/be/test/storage/index/inverted/query/engine/multi_segment_collector_test.cpp
index fcf80f0c197..d2e096fffc0 100644
---
a/be/test/storage/index/inverted/query/engine/multi_segment_collector_test.cpp
+++
b/be/test/storage/index/inverted/query/engine/multi_segment_collector_test.cpp
@@ -249,7 +249,8 @@ static void check_native_posting_block(const
index_query::PostingsBlock& block,
EXPECT_EQ(block.doc_at(0), doc);
EXPECT_EQ(block.docs.front(), doc);
EXPECT_EQ(block.freq_at(0), 1);
- EXPECT_EQ(block.norm_at(0), 0);
+ EXPECT_TRUE(block.norms.empty());
+ EXPECT_EQ(block.norm_at(0), 1);
}
static void check_partition_position(index_query::PostingsCursor& cursor,
uint32_t position) {
@@ -407,7 +408,7 @@ TEST_F(MultiSegmentCollectorTest,
PhraseCandidatesRemainValidAfterWeightDestruct
auto scorer = weight->scorer(segment, "");
weight.reset();
ASSERT_EQ(scorer->doc(), 3);
- const float expected_score = scoring ? similarity->score(1.0F, 0)
: 1.0F;
+ const float expected_score = scoring ? similarity->score(1.0F, 1)
: 1.0F;
EXPECT_FLOAT_EQ(scorer->score(), expected_score);
EXPECT_EQ(scorer->seek(3), 3);
EXPECT_EQ(scorer->advance(), TERMINATED);
diff --git a/be/test/storage/index/inverted/query/exec/block_doc_set_test.cpp
b/be/test/storage/index/inverted/query/exec/block_doc_set_test.cpp
index d5e631f149b..a28514a757a 100644
--- a/be/test/storage/index/inverted/query/exec/block_doc_set_test.cpp
+++ b/be/test/storage/index/inverted/query/exec/block_doc_set_test.cpp
@@ -67,6 +67,22 @@ private:
size_t _next = 0;
};
+TEST(BlockDocSetTest, MissingNormsUseUnitDocumentLength) {
+ const std::array<uint32_t, 1> docs {5};
+ PostingsBlock block {.docs = docs,
+ .freqs = {},
+ .norms = {},
+ .range_begin = 0,
+ .range_end = 0,
+ .dense = false};
+ EXPECT_EQ(block.freq_at(0), 1U);
+ EXPECT_EQ(block.norm_at(0), 1U);
+
+ const std::array<uint32_t, 1> stored_norm {0};
+ block.norms = stored_norm;
+ EXPECT_EQ(block.norm_at(0), 0U);
+}
+
TEST(BlockDocSetTest, ReadFailureDoesNotBecomeEndOfPostings) {
TestBlockCursor source({{.docs = {},
.freqs = {},
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 6d99585c55b..6d00d6da478 100644
--- a/be/test/storage/index/inverted/reader/collection_statistics_test.cpp
+++ b/be/test/storage/index/inverted/reader/collection_statistics_test.cpp
@@ -414,7 +414,7 @@ protected:
return tablet_schema;
}
- TabletSchemaSPtr create_snii_schema(int64_t index_id = 1) {
+ TabletSchemaSPtr create_snii_schema(int64_t index_id = 1, bool
single_term_normalizer = false) {
TabletSchemaPB schema_pb;
schema_pb.set_keys_type(DUP_KEYS);
schema_pb.set_inverted_index_storage_format(InvertedIndexStorageFormatPB::SNII);
@@ -431,8 +431,12 @@ protected:
index._index_id = index_id;
index._index_type = IndexType::INVERTED;
index._col_unique_ids.push_back(1);
- index._properties["parser"] = "standard";
- index._properties["support_phrase"] = "true";
+ if (single_term_normalizer) {
+ index._properties["normalizer"] = "lowercase";
+ } else {
+ index._properties["parser"] = "standard";
+ index._properties["support_phrase"] = "true";
+ }
tablet_schema->append_index(std::move(index));
return tablet_schema;
}
@@ -470,7 +474,7 @@ protected:
}
// A normal analyzed SNII segment with positions and norms, as emitted for
scoring indexes.
- Status write_snii_scoring_segment(const std::string& segment_path) {
+ Status write_snii_scoring_segment(const std::string& segment_path, bool
docs_only = false) {
const std::string index_path_prefix {
segment_v2::InvertedIndexDescriptor::get_index_file_path_prefix(segment_path)};
io::FileWriterPtr file_writer;
@@ -486,8 +490,11 @@ protected:
snii::writer::TermPostings alpha;
alpha.term = "alpha";
alpha.docids = {0, 1};
- alpha.freqs = {1, 1};
- alpha.positions_flat = {0, 0};
+ alpha.retain_positions = !docs_only;
+ if (!docs_only) {
+ alpha.freqs = {1, 1};
+ alpha.positions_flat = {0, 0};
+ }
snii::writer::TermPostings beta;
beta.term = "beta";
@@ -497,10 +504,15 @@ protected:
snii::writer::SniiIndexInput input;
input.index_id = 1;
- input.config = snii::format::IndexConfig::kDocsPositions;
+ input.config = docs_only ? snii::format::IndexConfig::kDocsOnly
+ : snii::format::IndexConfig::kDocsPositions;
input.doc_count = 2;
- input.encoded_norms = {snii::query::encode_norm(2),
snii::query::encode_norm(1)};
- input.terms = {std::move(alpha), std::move(beta)};
+ if (!docs_only) {
+ input.encoded_norms = {snii::query::encode_norm(2),
snii::query::encode_norm(1)};
+ input.terms = {std::move(alpha), std::move(beta)};
+ } else {
+ input.terms = {std::move(alpha)};
+ }
RETURN_IF_ERROR(writer.add_logical_index(input));
RETURN_IF_ERROR(writer.finish());
@@ -655,6 +667,7 @@ protected:
uint64_t sum_total_term_freq = 0;
bool has_positions = true;
bool has_norms = true;
+ bool single_term_normalizer = false;
};
Status stage_snii_fields_for_test(
@@ -663,7 +676,8 @@ protected:
for (const auto& field : fields) {
RETURN_IF_ERROR(statistics->admit_snii_scoring_segment(
field.field_name, field.index_doc_count,
field.sum_total_term_freq,
- field.has_positions, field.has_norms,
segment_accumulator));
+ field.has_positions, field.has_norms,
field.single_term_normalizer,
+ segment_accumulator));
}
return Status::OK();
}
@@ -1006,6 +1020,27 @@ TEST_F(CollectionStatisticsTest,
SniiScoringUsesPhysicalStatistics) {
expect_collected_term(L"1", L"alpha", 2);
}
+TEST_F(CollectionStatisticsTest,
SniiDocsOnlyNormalizerStillCollectsScoringStatistics) {
+ auto tablet_schema = create_snii_schema(1,
/*single_term_normalizer=*/true);
+ const std::string segment_path = test_dir_ + "/snii_normalizer_0.dat";
+ auto write_status = write_snii_scoring_segment(segment_path,
/*docs_only=*/true);
+ 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,
+ create_match_expr_contexts("alpha"),
nullptr);
+
+ ASSERT_TRUE(status.ok()) << status;
+ expect_collected_stats(L"1", 2, 2);
+ expect_collected_term(L"1", L"alpha", 2);
+}
+
TEST_F(CollectionStatisticsTest, SniiScoringLookupUsesCallerIoContext) {
snii::snii_test::ScopedEnv
force_nonresident_dict("SNII_DICT_RESIDENT_MAX", "0");
auto tablet_schema = create_snii_schema();
@@ -1194,6 +1229,16 @@ TEST(CollectionStatisticsSniiScoringTest,
ResolveRejectsSegmentWithoutPositions)
EXPECT_EQ(result.error().code(), ErrorCode::INVERTED_INDEX_NOT_SUPPORTED);
}
+TEST(CollectionStatisticsSniiScoringTest,
ResolveAcceptsSingleTermNormalizerWithoutPositions) {
+ auto result = resolve_snii_scoring_segment(3, 2, /*has_positions=*/false,
+ /*has_norms=*/false,
+
/*single_term_normalizer=*/true);
+
+ ASSERT_TRUE(result.has_value()) << result.error();
+ EXPECT_EQ(result->doc_count, 3U);
+ EXPECT_EQ(result->token_count, 2U);
+}
+
TEST_F(CollectionStatisticsTest,
CollectionStatisticsInstancesKeepAdmissionStateIsolated) {
CollectionStatistics first;
CollectionStatistics second;
diff --git a/be/test/storage/index/inverted/snii/snii_writer_test.cpp
b/be/test/storage/index/inverted/snii/snii_writer_test.cpp
index 2383dfd6e4c..8be7f7bba98 100644
--- a/be/test/storage/index/inverted/snii/snii_writer_test.cpp
+++ b/be/test/storage/index/inverted/snii/snii_writer_test.cpp
@@ -137,6 +137,28 @@ private:
doris::IndexPolicyMgr* previous_ = nullptr;
};
+class ScopedSplitNormalizerPolicy {
+public:
+ ScopedSplitNormalizerPolicy() {
+ auto* exec_env = doris::ExecEnv::GetInstance();
+ previous_ = exec_env->index_policy_mgr();
+ exec_env->_index_policy_mgr = &manager_;
+
+ doris::TIndexPolicy normalizer;
+ normalizer.id = 9020;
+ normalizer.name = "split_norm";
+ normalizer.type = doris::TIndexPolicyType::NORMALIZER;
+ normalizer.properties["token_filter"] = "word_delimiter";
+ manager_.apply_policy_changes({normalizer}, {});
+ }
+
+ ~ScopedSplitNormalizerPolicy() {
doris::ExecEnv::GetInstance()->_index_policy_mgr = previous_; }
+
+private:
+ doris::IndexPolicyMgr manager_;
+ doris::IndexPolicyMgr* previous_ = nullptr;
+};
+
// A fatal assertion inside a helper FUNCTION only aborts the helper; the
calling
// test keeps running and may dereference state that failed to initialize (this
// bit us as a null-analyzer SEGV). A macro expands in the test body, so the
@@ -551,6 +573,35 @@ TEST(SniiWriterNorms, WritesNormsFollowSharedNormsPolicy) {
doris::config::inverted_index_skip_norms_for_variant =
original_skip_norms_for_variant;
}
+TEST(SniiWriterTest, SplitNormalizerRetainsPhrasePositions) {
+ ScopedSplitNormalizerPolicy policies;
+ doris::TabletIndex index_meta;
+ init_gram_index_meta(&index_meta, 9021,
+ {{"normalizer", "split_norm"}, {"support_phrase",
"true"}});
+
+ doris::segment_v2::SniiIndexColumnWriter writer(nullptr, &index_meta,
+
doris::FieldType::OLAP_FIELD_TYPE_VARCHAR);
+ ASSERT_OK(writer.init());
+ ASSERT_EQ(writer.config_for_test(), IndexConfig::kDocsPositions);
+
+ const doris::Slice value("foo-bar");
+ ASSERT_OK(writer.add_values("content", &value, 1));
+ auto postings = writer.term_buffer_for_test()->finalize_sorted();
+ bool saw_foo = false;
+ bool saw_bar = false;
+ for (const auto& posting : postings) {
+ if (posting.term == "foo") {
+ saw_foo = true;
+ EXPECT_EQ(posting.positions_flat, std::vector<uint32_t>({1}));
+ } else if (posting.term == "bar") {
+ saw_bar = true;
+ EXPECT_EQ(posting.positions_flat, std::vector<uint32_t>({2}));
+ }
+ }
+ EXPECT_TRUE(saw_foo);
+ EXPECT_TRUE(saw_bar);
+}
+
TEST(SniiWriterTest, GramTokenizerForcesDocsOnlyAndIsRecognised) {
ScopedConfiguredDensity fixed_density;
ScopedGramPolicies policies;
diff --git a/be/test/storage/segment/index_reader_helper_test.cpp
b/be/test/storage/segment/index_reader_helper_test.cpp
index d2038e8bd8f..b7a6da09eda 100644
--- a/be/test/storage/segment/index_reader_helper_test.cpp
+++ b/be/test/storage/segment/index_reader_helper_test.cpp
@@ -396,6 +396,49 @@ TEST_F(IndexReaderHelperTest,
IsNeedSimilarityScoreWithInvertedIndexQueryTypeTes
InvertedIndexQueryType::LESS_THAN_QUERY, &index_meta));
}
+TEST_F(IndexReaderHelperTest, NormalizerOnlyPhraseAndScoringNeedNoPositions) {
+ TabletIndexPB index_pb;
+ index_pb.set_index_type(IndexType::INVERTED);
+ index_pb.set_index_id(2);
+ index_pb.set_index_name("normalized");
+ index_pb.add_col_unique_id(1);
+ (*index_pb.mutable_properties())[INVERTED_INDEX_NORMALIZER_NAME_KEY] =
"lowercase";
+ TabletIndex index_meta;
+ index_meta.init_from_pb(index_pb);
+
+ auto reader =
std::make_shared<MockInvertedIndexReader>(InvertedIndexReaderType::FULLTEXT,
+
index_meta.properties());
+
EXPECT_EQ(get_parser_phrase_support_string_from_properties(index_meta.properties()),
+ INVERTED_INDEX_PARSER_PHRASE_SUPPORT_NO);
+ EXPECT_TRUE(IndexReaderHelper::is_support_phrase(reader));
+
EXPECT_TRUE(IndexReaderHelper::is_need_similarity_score(InvertedIndexQueryType::MATCH_ANY_QUERY,
+ &index_meta));
+
EXPECT_TRUE(IndexReaderHelper::is_need_similarity_score(TExprOpcode::MATCH_ANY,
&index_meta));
+
+ auto explicit_disabled = index_meta.properties();
+ explicit_disabled[INVERTED_INDEX_PARSER_PHRASE_SUPPORT_KEY] =
+ INVERTED_INDEX_PARSER_PHRASE_SUPPORT_NO;
+ auto disabled_reader = std::make_shared<MockInvertedIndexReader>(
+ InvertedIndexReaderType::FULLTEXT, explicit_disabled);
+ EXPECT_FALSE(IndexReaderHelper::is_support_phrase(disabled_reader));
+
+ auto builtin_alias = index_meta.properties();
+ builtin_alias[INVERTED_INDEX_NORMALIZER_NAME_KEY] = "LowerCase";
+ auto alias_reader =
std::make_shared<MockInvertedIndexReader>(InvertedIndexReaderType::FULLTEXT,
+
builtin_alias);
+ EXPECT_TRUE(IndexReaderHelper::is_support_phrase(alias_reader));
+
+ TabletIndexPB custom_pb = index_pb;
+ (*custom_pb.mutable_properties())[INVERTED_INDEX_NORMALIZER_NAME_KEY] =
"split_norm";
+ TabletIndex custom_meta;
+ custom_meta.init_from_pb(custom_pb);
+ auto custom_reader = std::make_shared<MockInvertedIndexReader>(
+ InvertedIndexReaderType::FULLTEXT, custom_meta.properties());
+ EXPECT_FALSE(IndexReaderHelper::is_support_phrase(custom_reader));
+ EXPECT_FALSE(IndexReaderHelper::is_need_similarity_score(
+ InvertedIndexQueryType::MATCH_ANY_QUERY, &custom_meta));
+}
+
TEST_F(IndexReaderHelperTest, IsNeedSimilarityScoreWithTExprOpcodeTest) {
TabletIndex index_meta;
auto index_meta_pb = std::make_unique<TabletIndexPB>();
diff --git a/fe/fe-catalog/src/main/java/org/apache/doris/catalog/Index.java
b/fe/fe-catalog/src/main/java/org/apache/doris/catalog/Index.java
index ee85e1daf93..4045e07f7f7 100644
--- a/fe/fe-catalog/src/main/java/org/apache/doris/catalog/Index.java
+++ b/fe/fe-catalog/src/main/java/org/apache/doris/catalog/Index.java
@@ -75,10 +75,13 @@ public class Index implements Writable {
this.comment = comment;
if (indexType == IndexType.INVERTED) {
if (this.properties != null && !this.properties.isEmpty()) {
- if
(this.properties.containsKey(InvertedIndexProperties.INVERTED_INDEX_PARSER_KEY)
- ||
this.properties.containsKey(InvertedIndexProperties.INVERTED_INDEX_PARSER_KEY_ALIAS)
- ||
this.properties.containsKey(InvertedIndexProperties.INVERTED_INDEX_ANALYZER_NAME_KEY)
- ||
this.properties.containsKey(InvertedIndexProperties.INVERTED_INDEX_NORMALIZER_NAME_KEY))
{
+ String normalizerName = this.properties.get(
+
InvertedIndexProperties.INVERTED_INDEX_NORMALIZER_NAME_KEY);
+ if
(!InvertedIndexProperties.INVERTED_INDEX_PARSER_NONE.equalsIgnoreCase(getInvertedIndexParser())
+ || StringUtils.isNotEmpty(this.properties.get(
+
InvertedIndexProperties.INVERTED_INDEX_ANALYZER_NAME_KEY))
+ || (StringUtils.isNotEmpty(normalizerName)
+ &&
!"lowercase".equalsIgnoreCase(normalizerName))) {
String supportPhraseKey = InvertedIndexProperties
.INVERTED_INDEX_SUPPORT_PHRASE_KEY;
if (!this.properties.containsKey(supportPhraseKey)) {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/analysis/InvertedIndexUtil.java
b/fe/fe-core/src/main/java/org/apache/doris/analysis/InvertedIndexUtil.java
index dd545e45020..0778f68e418 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/analysis/InvertedIndexUtil.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/analysis/InvertedIndexUtil.java
@@ -354,11 +354,20 @@ public class InvertedIndexUtil {
}
/**
- * Canonicalize analyzer and normalizer names in index properties. Legacy
metadata may contain
- * case-distinct policy names, so a resolved custom policy must keep its
exact stored name.
+ * Resolve policy names and retain positions for custom normalizers that
may split terms.
+ * Preserve exact custom policy spellings when legacy names differ only by
case.
*/
private static void normalizeInvertedIndexProperties(Map<String, String>
properties) {
resolvePolicyNames(properties);
+ String normalizer = properties.get(INVERTED_INDEX_NORMALIZER_NAME_KEY);
+ if (normalizer != null && !normalizer.isEmpty()
+ && !properties.containsKey(INVERTED_INDEX_SUPPORT_PHRASE_KEY))
{
+ boolean builtin = Env.getCurrentEnv().getIndexPolicyMgr()
+ .getTopLevelBuiltin(normalizer,
IndexPolicy.BUILTIN_NORMALIZERS) != null;
+ if (!builtin) {
+ properties.put(INVERTED_INDEX_SUPPORT_PHRASE_KEY, "true");
+ }
+ }
AnalyzerKeyNormalizer.normalizeInvertedIndexProperties(
properties,
INVERTED_INDEX_PARSER_KEY,
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/analysis/InvertedIndexPropertiesTest.java
b/fe/fe-core/src/test/java/org/apache/doris/analysis/InvertedIndexPropertiesTest.java
index 3cf1d5d9c57..54481181aa8 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/analysis/InvertedIndexPropertiesTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/analysis/InvertedIndexPropertiesTest.java
@@ -849,10 +849,11 @@ public class InvertedIndexPropertiesTest {
policyMgr.replayCreateIndexPolicy(new IndexPolicy(
92, "norm_lower", IndexPolicyTypeEnum.NORMALIZER,
Map.of("token_filter", "lowercase")));
Map<String, String> mixedNormalizer = new
HashMap<>(Map.of("normalizer", "LowerCase"));
+ Map<String, String> customNormalizer = new
HashMap<>(Map.of("normalizer", "lowercase"));
Map<String, String> mixedAnalyzer = new HashMap<>(Map.of("analyzer",
"Standard"));
withIndexPolicyManager(policyMgr, () -> {
- for (Map<String, String> properties : List.of(mixedNormalizer,
mixedAnalyzer)) {
+ for (Map<String, String> properties : List.of(mixedNormalizer,
customNormalizer, mixedAnalyzer)) {
Assertions.assertDoesNotThrow(() ->
InvertedIndexUtil.checkInvertedIndexParser("c",
PrimitiveType.VARCHAR, properties,
TInvertedIndexFileStorageFormat.V3));
}
@@ -860,6 +861,8 @@ public class InvertedIndexPropertiesTest {
// The exact policy shadows the canonical name, so only
the mixed spelling still
// reaches the built-in normalizer on BE.
() -> Assertions.assertEquals("LowerCase",
mixedNormalizer.get("normalizer")),
+ () ->
Assertions.assertFalse(mixedNormalizer.containsKey("support_phrase")),
+ () -> Assertions.assertEquals("true",
customNormalizer.get("support_phrase")),
() -> Assertions.assertEquals("LowerCase",
InvertedIndexUtil.resolveAnalyzerName("LowerCase")),
() -> Assertions.assertEquals("lowercase",
diff --git a/fe/fe-core/src/test/java/org/apache/doris/catalog/IndexTest.java
b/fe/fe-core/src/test/java/org/apache/doris/catalog/IndexTest.java
index 814a00f66e3..d796ec619b8 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/catalog/IndexTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/catalog/IndexTest.java
@@ -24,9 +24,44 @@ import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
public class IndexTest {
+ @Test
+ public void testInvertedIndexPhraseDefaultsFollowTokenization() {
+ Index exact = new Index(1, "exact", List.of("col"),
IndexType.INVERTED, Map.of(), "");
+
Assertions.assertFalse(exact.getProperties().containsKey("support_phrase"));
+
+ Index keyword = new Index(2, "keyword", List.of("col"),
IndexType.INVERTED,
+ Map.of("parser", "none"), "");
+
Assertions.assertFalse(keyword.getProperties().containsKey("support_phrase"));
+
+ Index normalized = new Index(3, "normalized", List.of("col"),
IndexType.INVERTED,
+ Map.of("normalizer", "lowercase"), "");
+
Assertions.assertFalse(normalized.getProperties().containsKey("support_phrase"));
+
+ Index customNormalizer = new Index(7, "custom_normalizer",
List.of("col"), IndexType.INVERTED,
+ Map.of("normalizer", "split_norm"), "");
+ Assertions.assertEquals("true",
customNormalizer.getProperties().get("support_phrase"));
+
+ Index disabledCustomNormalizer = new Index(8,
"disabled_custom_normalizer", List.of("col"),
+ IndexType.INVERTED, Map.of("normalizer", "split_norm",
"support_phrase", "false"), "");
+ Assertions.assertEquals("false",
disabledCustomNormalizer.getProperties().get("support_phrase"));
+
+ Index parsed = new Index(4, "parsed", List.of("col"),
IndexType.INVERTED,
+ Map.of("parser", "english"), "");
+ Assertions.assertEquals("true",
parsed.getProperties().get("support_phrase"));
+
+ Index analyzed = new Index(5, "analyzed", List.of("col"),
IndexType.INVERTED,
+ Map.of("analyzer", "english"), "");
+ Assertions.assertEquals("true",
analyzed.getProperties().get("support_phrase"));
+
+ Index disabled = new Index(6, "disabled", List.of("col"),
IndexType.INVERTED,
+ Map.of("parser", "english", "support_phrase", "false"), "");
+ Assertions.assertEquals("false",
disabled.getProperties().get("support_phrase"));
+ }
+
@Test
public void testGetColumnUniqueIds() {
// Create test columns with unique IDs
diff --git a/regression-test/suites/index_p0/test_index_meta.groovy
b/regression-test/suites/index_p0/test_index_meta.groovy
index d7647f2a9a3..bcaaf2922b1 100644
--- a/regression-test/suites/index_p0/test_index_meta.groovy
+++ b/regression-test/suites/index_p0/test_index_meta.groovy
@@ -71,7 +71,7 @@ suite("index_meta", "p0") {
assertEquals(show_result[1][4], "name")
assertEquals(show_result[1][10], "INVERTED")
assertEquals(show_result[1][11], "index for name")
- assertEquals(show_result[1][12], "(\"lower_case\" = \"true\", \"parser\" =
\"none\", \"support_phrase\" = \"true\")")
+ assertEquals(show_result[1][12], "(\"lower_case\" = \"true\", \"parser\" =
\"none\")")
// add index on column description
sql "create index idx_desc on ${tableName}(description) USING INVERTED
PROPERTIES(\"parser\"=\"standard\") COMMENT 'index for description';"
@@ -90,7 +90,7 @@ suite("index_meta", "p0") {
assertEquals(show_result[1][4], "name")
assertEquals(show_result[1][10], "INVERTED")
assertEquals(show_result[1][11], "index for name")
- assertEquals(show_result[1][12], "(\"lower_case\" = \"true\", \"parser\" =
\"none\", \"support_phrase\" = \"true\")")
+ assertEquals(show_result[1][12], "(\"lower_case\" = \"true\", \"parser\" =
\"none\")")
assertEquals(show_result[2][2], "idx_desc")
assertEquals(show_result[2][4], "description")
assertEquals(show_result[2][10], "INVERTED")
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]