github-actions[bot] commented on code in PR #67538:
URL: https://github.com/apache/doris/pull/67538#discussion_r3998863391
##########
fe/fe-core/src/main/java/org/apache/doris/alter/SchemaChangeHandler.java:
##########
@@ -3314,6 +3314,12 @@ private boolean processAddIndex(CreateIndexOp
createIndexOp, OlapTable olapTable
}
}
+ // CreateIndexOp#validate materializes the IndexDefinition into
alterIndex before
+ // checkColumn runs, so the property defaults that are only filled in
during checkColumn
+ // (currently the gram family's support_phrase=false) have to be
written back here, or the
+ // index that is persisted and shipped to BE would lose them.
+ indexDef.applyPropertiesTo(alterIndex);
Review Comment:
[P1] Reapply the MOR value-column rule before persisting these analyzer
defaults. This ADD INDEX path called the catalog-Column `checkColumn` overload,
whose UNIQUE/MOW-disabled guard recognizes only `parser`; a gram index is
identified by `analyzer`, so ALTER ADD passes here while CREATE TABLE's
overload rejects the same analyzed value-column index. The write-back then
persists an index that the scan path deliberately cannot push below UNIQUE
merging. Please make the catalog overload use `isAnalyzedInvertedIndex()` (or
the same four keys) and add a UNIQUE_KEYS, MOW-disabled ADD INDEX test; the
current test uses DUP_KEYS.
##########
be/src/exprs/function/like.cpp:
##########
@@ -1107,6 +1114,78 @@ Status FunctionRegexpLike::open(FunctionContext* context,
return Status::OK();
}
+// R8 (unity build): file-scope helpers use a namespace private to this file.
+namespace like_gram_index_detail {
+
+// Index acceleration may be skipped, but cancellation and memory failures
stop the query.
+Status dispatch_query(bool is_like, const std::string& pattern,
segment_v2::IndexIterator* iter,
+ const IndexFieldNameAndTypePair& data_type_with_name,
uint32_t num_rows,
+ segment_v2::InvertedIndexResultBitmap* bitmap_result) {
+ segment_v2::InvertedIndexParam param;
+ param.column_name = data_type_with_name.first;
+ param.column_type = data_type_with_name.second;
+ param.query_value = Field::create_field<TYPE_STRING>(pattern);
+ param.query_type = is_like ?
segment_v2::InvertedIndexQueryType::LIKE_GRAM_QUERY
Review Comment:
[P1] Select a gram-capable reader before dispatching this query. The param
carries no analyzer/capability key, so gram types are treated as match queries
and the iterator picks the lowest-ID FULLTEXT reader. In a supported migration
layout with an older ordinary English/SNII or CLucene index and a later gram
index, the ordinary reader wins, returns SKIPPED/NOT_SUPPORTED, and
`dispatch_query` converts that to success without ever trying the gram reader.
Scalar evaluation preserves results, but the new acceleration is silently
disabled. Please require/prefer gram capability or retry compatible readers,
and cover ordinary-first plus gram-second ordering.
##########
be/src/storage/index/snii/writer/logical_index_writer.cpp:
##########
@@ -505,6 +562,93 @@ struct LogicalIndexWriter::BlockState {
// Out-of-line so unique_ptr<BlockState> sees the complete type (see header).
LogicalIndexWriter::~LogicalIndexWriter() = default;
+// Offers one term to the high-df digest. The digest keeps the highest-df
terms seen, as a
+// min-heap on df so the cheapest entry to evict is always at the front, which
makes the
+// whole pass O(terms log K) time and O(K) space -- no second sort over the
vocabulary.
+//
+// Terms below the floor are dropped outright: the digest exists to let a
query GIVE UP
+// early, and a term rare enough to sit below the floor is one no cost gate
would give up on.
+void LogicalIndexWriter::note_high_df_term(uint64_t term_hash, uint32_t df) {
+ // A zero floor means no digest for this index (see high_df_floor_for),
and it has to be
+ // tested separately: `df < 0` is never true, so the comparison below
would let every
+ // term through on exactly the indexes that want none.
+ if (high_df_floor_ == 0 || df < high_df_floor_) {
+ return;
+ }
+ const auto by_df = [](const HighDfEntry& lhs, const HighDfEntry& rhs) {
+ return lhs.df > rhs.df; // greater-than gives a MIN-heap on df
+ };
+ if (high_df_terms_.size() < format::kMaxHighDfDigestTerms) {
+ high_df_terms_.push_back({df, term_hash});
Review Comment:
[P2] Charge this retained digest heap to the writer's `MemoryReporter`. It
grows to 4,096 aligned entries (64 KiB) per logical index without a
reservation, is kept in the compound writer until teardown, and
`finish_high_df_digest` creates another heap copy plus parallel hash/df vectors
before encoding. Those bytes appear in the task allocator tracker, but they are
absent from both the dedicated SNII build metric and
`snii_registered_build_bytes()`, the only build-share signal used by the global
limiter; concurrent multi-index builds can therefore bypass that share by the
sum of these heaps. Please pre-charge capacity growth, account the finalization
buffers (or serialize through tracked storage), release retained digest storage
once metadata is materialized, and add reporter-balance coverage.
##########
fe/fe-core/src/main/java/org/apache/doris/analysis/InvertedIndexUtil.java:
##########
@@ -353,12 +358,70 @@ private static void checkAnalyzerName(String
analyzerName, PrimitiveType colType
+ " is not supported for column of type " + colType);
}
try {
-
Env.getCurrentEnv().getIndexPolicyMgr().validateAnalyzerExists(analyzerName);
+ IndexPolicyMgr indexPolicyMgr =
Env.getCurrentEnv().getIndexPolicyMgr();
+ indexPolicyMgr.validateAnalyzerExists(analyzerName);
+ // Gram-family analyzer (an ngram tokenizer carrying mode, see
+ // IndexPolicyMgr#resolveGramTokenizerMode): BE builds
sparse/dense gram postings for it
+ // only on SNII, and those postings carry no positions, so phrase
queries are impossible.
+ Optional<String> gramMode =
indexPolicyMgr.resolveGramTokenizerMode(analyzerName);
+ if (gramMode.isPresent()) {
+ if (colType.isArrayType()) {
+ throw new AnalysisException("gram tokenizer (mode=" +
gramMode.get()
+ + ") analyzer '" + analyzerName + "' does not
support ARRAY columns");
+ }
+ if (!colType.isCharFamily()) {
+ throw new AnalysisException("gram tokenizer (mode=" +
gramMode.get()
+ + ") analyzer '" + analyzerName
+ + "' is supported only on scalar CHAR, VARCHAR, or
STRING columns");
+ }
+ if (storageFormat != TInvertedIndexFileStorageFormat.SNII) {
+ throw new AnalysisException("gram tokenizer (mode=" +
gramMode.get()
+ + ") requires inverted_index_storage_format =
SNII");
+ }
+ if ("true".equals(supportPhrase)) {
+ throw new AnalysisException(
+ "gram tokenizer index does not support phrase
(support_phrase must be false)");
+ }
+ }
} catch (DdlException e) {
throw new AnalysisException("Invalid custom analyzer: " +
e.getMessage());
}
}
+ /**
+ * Constraints a gram-family analyzer (an ngram tokenizer carrying mode)
enforces at the index
+ * property level:
+ * 1) an index-level char_filter (char_filter_type/pattern/replacement)
conflicts with the
+ * semantics of gram split boundaries (the character replacement
happens after the tokenizer
+ * has already split by the gram rule, which breaks reproducibility),
so it is rejected;
+ * 2) when support_phrase is not given explicitly it defaults to "false",
overriding the general
+ * rule of the {@link Index} constructor that "an analyzer implies
true" -- a gram index is
+ * forced to docs-only on the BE side and has no positions for a phrase
query to use.
+ *
+ * <p>The {@code properties} held by the caller ({@link
#checkInvertedIndexProperties}) and the
+ * {@link IndexDefinition} field are the same mutable Map reference, so
the defaults written
+ * here are seen when {@code IndexDefinition#translateToCatalogStyle}
builds the {@link Index}.
+ */
+ private static void applyGramFamilyIndexDefaults(String analyzerName,
Map<String, String> properties)
+ throws AnalysisException {
+ if (analyzerName == null || analyzerName.isEmpty()) {
+ return;
+ }
+ Optional<String> gramMode =
Env.getCurrentEnv().getIndexPolicyMgr().resolveGramTokenizerMode(analyzerName);
+ if (!gramMode.isPresent()) {
+ return;
+ }
+ if (properties.get(INVERTED_INDEX_PARSER_CHAR_FILTER_TYPE) != null
+ || properties.get(INVERTED_INDEX_PARSER_CHAR_FILTER_PATTERN)
!= null
+ ||
properties.get(INVERTED_INDEX_PARSER_CHAR_FILTER_REPLACEMENT) != null) {
+ throw new AnalysisException("char_filter cannot be used with gram
tokenizer (mode="
+ + gramMode.get() + ")");
+ }
+ if (properties.get(INVERTED_INDEX_SUPPORT_PHRASE_KEY) == null) {
+ properties.put(INVERTED_INDEX_SUPPORT_PHRASE_KEY, "false");
Review Comment:
[P1] Check phrase capability on the analyzer-selected reader. This default
makes a gram-family index docs-only, and a supported column can have that index
first plus a later positional English/standard index. For `MATCH_PHRASE ...
USING ANALYZER` targeting the later index, the expression carries the right
analyzer key and `read_from_index` would select it, but `FunctionMatchBase`
first calls analyzer-blind `get_reader(FULLTEXT)`, sees the leading gram
reader's `support_phrase=false`, and returns `INDEX_INVALID_PARAMETERS`. That
status is not downgraded, so the valid query fails solely by index order.
Please perform the capability check on the keyed selected reader (and reuse it
for execution), with gram-first/positional-second phrase and phrase-prefix
coverage.
##########
be/src/storage/index/snii/format/core_metadata.cpp:
##########
@@ -137,6 +219,21 @@ Status encode_core_metadata(const CoreMetadata& metadata,
ByteSink* out) {
}
encode_region_ref(metadata.section_refs.null_bitmap,
refs->mutable_null_bitmap());
encode_region_ref(metadata.section_refs.bsbf, refs->mutable_bsbf());
+ if (metadata.gram_scheme.has_value()) {
Review Comment:
[P1] Fence gram writes from pre-feature readers even when no posting is
dropped. This optional protobuf field is the only marker on a small gram
segment: format/min-reader stay at v1, the directory raises no required feature
unless a posting is physically dropped, and an old factory simply ignores
`mode` and uses legacy ngram semantics. For example, a new dense min=3/max=4
segment for `abcd` stores `abc,bcd`; an old legacy reader also requires `abcd`
for MATCH_ALL and can return an empty exact bitmap although scalar legacy
analysis matches. The existing stopped-posting feature cannot protect
sub-2,000-row/no-drop segments. Please gate gram DDL/writes on fleet capability
(and/or introduce a reader fence with a safe unsupported downgrade), preserve
that contract through inheritance, and add a frozen-old-reader test.
##########
be/src/storage/index/snii/snii_index_reader.cpp:
##########
@@ -651,6 +741,16 @@ Status SniiIndexReader::_query(const IndexQueryContextPtr&
context, const std::s
const ::doris::snii::reader::LogicalIndexReader* logical_reader = nullptr;
RETURN_IF_ERROR(_get_logical_reader(context, &searcher_cache_handle,
&uncached_reader,
&logical_reader));
+ // Compare the two optionals, not just two schemes: a segment written by a
legacy ngram
+ // tokenizer carries no scheme at all, and its dictionary holds that
tokenizer's terms. Once
+ // the current analyzer cuts grams, looking those grams up in that
dictionary answers a
+ // different question, so an absent persisted scheme is a mismatch like
any other.
+ if (analyzed_query && current_gram_scheme.has_value() &&
Review Comment:
[P1] Handle the reverse optional mismatch as well. A dense/sparse gram
segment can be recovered after its same-named analyzer is recreated as legacy
ngram (no `mode`), leaving `current_gram_scheme=nullopt` while the persisted
scheme is present. This guard then skips: current bigram MATCH terms are looked
up in a dense-trigram dictionary, so an exact empty bitmap can remove rows that
scalar current-analyzer MATCH accepts; MATCH_REGEXP is affected too. The cache
is also re-enabled and consulted before the persisted scheme is known, making
that bitmap reusable. Compare the full optionals after opening the reader (and
keep this case out of the scheme-blind cache), with gram-to-legacy recovery
coverage.
##########
be/src/storage/index/snii/snii_index_writer.cpp:
##########
@@ -377,6 +638,7 @@ void SniiIndexColumnWriter::close_on_error() {
// Balance the observation-tracker mirror before dropping the reporter.
_report_null_docids_capacity(/*release_all=*/true);
_report_encoded_norms_capacity(/*release_all=*/true);
+ _report_gram_buffers_capacity(/*release_all=*/true);
_memory_reporter.reset();
Review Comment:
[P2] Release the retained density sample before resetting this reporter. If
a held row fails analysis during `_finish_density_calibration()`, the latch
calls `close_on_error()` from inside replay; this path clears other buffers,
but leaves `_density_sample` and `_density_sample_bytes` owned by the
still-live column writer. `MemoryReporter`'s destructor then drains the
sample's charge from the process-wide build-RAM counter even though the
vector/strings (up to the configured budget plus the final row) remain resident
until the segment writer is cleared. Please swap/clear the sample and report
its release before dropping the reporter, with a replay-failure cleanup test.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]