github-actions[bot] commented on code in PR #67918:
URL: https://github.com/apache/doris/pull/67918#discussion_r4000130292


##########
be/src/storage/index/inverted/analyzer/ik/IKTokenizer.cpp:
##########
@@ -31,27 +31,36 @@ Token* IKTokenizer::next(Token* token) {
         return nullptr;
     }
 
-    std::string& token_text = tokens_text_[buffer_index_++];
+    TokenData& token_data = tokens_[buffer_index_++];
     // full-width to half-width, and lowercase
     // TODO(ryan19929): do regularizeString in fillBuffer.
-    CharacterUtil::regularizeString(token_text, this->lowercase);
-    size_t size = std::min(token_text.size(), 
static_cast<size_t>(LUCENE_MAX_WORD_LEN));
-    token->setNoCopy(token_text.data(), 0, static_cast<int32_t>(size));
+    CharacterUtil::regularizeString(token_data.text, this->lowercase);
+    size_t size = std::min(token_data.text.size(), 
static_cast<size_t>(LUCENE_MAX_WORD_LEN));
+    set(token, std::string_view(token_data.text.data(), size));
+    token->setStartOffset(token_data.start_offset);
+    token->setEndOffset(token_data.end_offset);
     return token;
 }
 
+void IKTokenizer::reset() {
+    inverted_index::DorisTokenizer::reset();
+    reset(_in.get());
+}
+
 void IKTokenizer::reset(lucene::util::Reader* reader) {
     this->input = reader;
     this->buffer_index_ = 0;
     this->data_length_ = 0;
-    this->tokens_text_.clear();
+    this->tokens_.clear();
 
     try {
         buffer_.reserve(input->size());
         ik_segmenter_->reset(reader);
         Lexeme lexeme;
         while (ik_segmenter_->next(lexeme)) {
-            tokens_text_.emplace_back(lexeme.getText());
+            tokens_.push_back({lexeme.getText(),
+                               
static_cast<int32_t>(lexeme.getByteBeginPosition()),

Review Comment:
   [P1] Fix the absolute base before publishing long-input offsets
   
   After the first 4096-byte refill, these values are already too small. 
`AnalyzeContext::fillBuffer()` carries bytes beginning at 
`typed_runes_[cursor_].getNextBytePosition()`, but `markBufferOffset()` 
advances `buffer_offset_` only by `typed_runes_[cursor_].offset`. Because the 
cursor rune was analyzed and then dropped by the memmove, the next buffer is 
based at its end while the recorded absolute base still points at its start; 
every later `Lexeme` offset is short by that rune's byte length, cumulatively 
across refills. Please advance by the same consumed-byte boundary and add exact 
offset checks on both sides of a multibyte refill.



##########
be/src/storage/index/inverted/analyzer/ik/IKTokenizer.cpp:
##########
@@ -31,27 +31,36 @@ Token* IKTokenizer::next(Token* token) {
         return nullptr;
     }
 
-    std::string& token_text = tokens_text_[buffer_index_++];
+    TokenData& token_data = tokens_[buffer_index_++];
     // full-width to half-width, and lowercase
     // TODO(ryan19929): do regularizeString in fillBuffer.
-    CharacterUtil::regularizeString(token_text, this->lowercase);
-    size_t size = std::min(token_text.size(), 
static_cast<size_t>(LUCENE_MAX_WORD_LEN));
-    token->setNoCopy(token_text.data(), 0, static_cast<int32_t>(size));
+    CharacterUtil::regularizeString(token_data.text, this->lowercase);
+    size_t size = std::min(token_data.text.size(), 
static_cast<size_t>(LUCENE_MAX_WORD_LEN));
+    set(token, std::string_view(token_data.text.data(), size));
+    token->setStartOffset(token_data.start_offset);
+    token->setEndOffset(token_data.end_offset);
     return token;
 }
 
+void IKTokenizer::reset() {

Review Comment:
   [P0] Preserve the indexing reset contract
   
   CLucene always calls `stream->reset()` immediately before consuming a 
tokenized field, but this new eager reset breaks both IK entry paths. Legacy 
`IKAnalyzer` already called `reset(reader)` and filled tokens without setting 
`DorisTokenizer::_in_pending`; CLucene's next call now promotes null and 
`reset(_in.get())` reaches `input->size()`, crashing ordinary existing 
`parser=ik` writes. For custom IK ARRAY elements, 
`CustomAnalyzer::tokenStream()` performs the first eager reset, then CLucene's 
second reset clears those tokens and re-reads the same reader at EOF, so the 
element indexes no terms. Please make CLucene's reset-before-consume the single 
tokenization point (while preserving raw-reader compatibility), and cover 
legacy IK IndexWriter plus custom IK ARRAY indexing.



##########
fe/fe-core/src/main/java/org/apache/doris/indexpolicy/IndexPolicy.java:
##########
@@ -59,7 +59,8 @@ public class IndexPolicy implements Writable, 
GsonPostProcessable {
     public static final String PROP_TOKEN_FILTER = "token_filter";
     public static final String PROP_CHAR_FILTER = "char_filter";
     public static final Set<String> BUILTIN_TOKENIZERS = ImmutableSet.of(
-            "empty", "ngram", "edge_ngram", "keyword", "standard", 
"char_group", "basic", "icu", "pinyin");
+            "empty", "ngram", "edge_ngram", "keyword", "standard", 
"char_group", "basic", "icu", "pinyin",
+            "ik_smart", "ik_max_word");

Review Comment:
   [P1] Accept the IK types in TOKENIZER policy validation
   
   Adding these names to `BUILTIN_TOKENIZERS` makes FE advertise them as 
supported tokenizer types, but `validateTokenizerProperties()` still has no 
`ik_smart`/`ik_max_word` case. `CREATE INVERTED INDEX TOKENIZER t PROPERTIES 
("type"="ik_smart")` therefore reaches the default branch and is rejected with 
an error whose own supported-types list includes `ik_smart`; the max-word form 
behaves the same. BE can resolve both new types if they reach it, and every 
previous built-in has a validator case. Please add type-only validation for 
both modes (and cover a named tokenizer policy), or separate direct built-ins 
from creatable/advertised types.



##########
regression-test/suites/inverted_index_p0/analyzer/test_ik_custom_analyzer.groovy:
##########
@@ -0,0 +1,123 @@
+// 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.
+
+suite("test_ik_custom_analyzer", "p0") {
+    def pinyinFilter = "test_ik_pinyin_filter"
+    def smartAnalyzer = "test_ik_smart_pinyin_analyzer"
+    def maxWordAnalyzer = "test_ik_max_word_pinyin_analyzer"
+    def tableName = "test_ik_custom_analyzer"

Review Comment:
   [P2] Capture these deterministic results as regression goldens
   
   This suite uses `def tableName` for its sole ordinary test table and checks 
every deterministic `TOKENIZE`/`MATCH` result via `assertTrue`/`assertEquals`, 
so it produces no generated `.out` evidence. The repository regression rules 
require a hardcoded name for a single ordinary table and `qt_`/`order_qt_` 
output for determined results. Please convert these checks to named golden 
queries (keeping `ORDER BY` for row results), generate the `.out` through the 
runner, and hardcode the table name.



##########
regression-test/suites/inverted_index_p0/analyzer/test_ik_custom_analyzer.groovy:
##########
@@ -0,0 +1,123 @@
+// 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.
+
+suite("test_ik_custom_analyzer", "p0") {
+    def pinyinFilter = "test_ik_pinyin_filter"
+    def smartAnalyzer = "test_ik_smart_pinyin_analyzer"
+    def maxWordAnalyzer = "test_ik_max_word_pinyin_analyzer"
+    def tableName = "test_ik_custom_analyzer"
+
+    sql "DROP TABLE IF EXISTS ${tableName}"
+    try_sql "DROP INVERTED INDEX ANALYZER IF EXISTS ${smartAnalyzer}"
+    try_sql "DROP INVERTED INDEX ANALYZER IF EXISTS ${maxWordAnalyzer}"
+    try_sql "DROP INVERTED INDEX TOKEN_FILTER IF EXISTS ${pinyinFilter}"
+
+    sql """
+        CREATE INVERTED INDEX TOKEN_FILTER IF NOT EXISTS ${pinyinFilter}
+        PROPERTIES (
+            "type" = "pinyin",
+            "keep_none_chinese" = "false",
+            "keep_first_letter" = "true",
+            "keep_full_pinyin" = "false",
+            "keep_separate_first_letter" = "false",
+            "keep_original" = "true",
+            "keep_joined_full_pinyin" = "true"
+        )
+    """
+    sql """
+        CREATE INVERTED INDEX ANALYZER IF NOT EXISTS ${smartAnalyzer}
+        PROPERTIES (
+            "tokenizer" = "ik_smart",
+            "token_filter" = "${pinyinFilter}"
+        )
+    """
+    sql """
+        CREATE INVERTED INDEX ANALYZER IF NOT EXISTS ${maxWordAnalyzer}
+        PROPERTIES (
+            "tokenizer" = "ik_max_word",
+            "token_filter" = "${pinyinFilter}"
+        )
+    """
+
+    def waitAnalyzerReady = { analyzerName ->
+        int maxRetry = 30
+        Exception lastException = null
+        for (int i = 0; i < maxRetry; i++) {
+            try {
+                sql """SELECT TOKENIZE('probe', 
'"analyzer"="${analyzerName}"')"""
+                return
+            } catch (Exception e) {
+                lastException = e
+                sleep(1000)
+            }
+        }
+        assertTrue(false, "Analyzer ${analyzerName} was not ready: 
${lastException?.message}")
+    }
+
+    waitAnalyzerReady(smartAnalyzer)
+    waitAnalyzerReady(maxWordAnalyzer)
+
+    def smartTokens = sql """SELECT TOKENIZE('我来到北京清华大学', 
'"analyzer"="${smartAnalyzer}"')"""
+    def smartTokenString = smartTokens[0][0].toString()
+    assertTrue(smartTokenString.contains('"token": "清华大学"'))
+    assertTrue(smartTokenString.contains('"token": "qinghuadaxue"'))
+
+    def maxWordTokens = sql """SELECT TOKENIZE('我来到北京清华大学', 
'"analyzer"="${maxWordAnalyzer}"')"""
+    def maxWordTokenString = maxWordTokens[0][0].toString()
+    assertTrue(maxWordTokenString.contains('"token": "清华"'))
+    assertTrue(maxWordTokenString.contains('"token": "qinghua"'))
+
+    sql """
+        CREATE TABLE ${tableName} (
+            id INT,
+            content STRING,
+            INDEX idx_smart (content) USING INVERTED
+                PROPERTIES("analyzer" = "${smartAnalyzer}", "support_phrase" = 
"true"),
+            INDEX idx_max_word (content) USING INVERTED
+                PROPERTIES("analyzer" = "${maxWordAnalyzer}", "support_phrase" 
= "true")

Review Comment:
   [P1] Make the enabled phrase paths handle pinyin alternatives
   
   This index is marked `support_phrase=true`, but the configured filter emits 
清华大学, `qinghuadaxue`, and `qhdx` at the same token position (increments 1,0,0). 
Legacy CLucene assigns those separate `TermInfo`s positions by vector ordinal, 
and SNII likewise flattens them into a sequential term vector, so an exact 
`MATCH_PHRASE` requires the alternatives consecutively and misses the indexed 
row. The query-v2 CLucene path already groups equal-position terms before 
selecting `PhraseQuery`/`MultiPhraseQuery`. Please apply equivalent grouping to 
both direct readers and add `MATCH_PHRASE` coverage here; the suite currently 
exercises only `MATCH`.



##########
be/src/storage/index/inverted/analyzer/ik/IKTokenizer.cpp:
##########
@@ -31,27 +31,36 @@ Token* IKTokenizer::next(Token* token) {
         return nullptr;
     }
 
-    std::string& token_text = tokens_text_[buffer_index_++];
+    TokenData& token_data = tokens_[buffer_index_++];
     // full-width to half-width, and lowercase
     // TODO(ryan19929): do regularizeString in fillBuffer.
-    CharacterUtil::regularizeString(token_text, this->lowercase);
-    size_t size = std::min(token_text.size(), 
static_cast<size_t>(LUCENE_MAX_WORD_LEN));
-    token->setNoCopy(token_text.data(), 0, static_cast<int32_t>(size));
+    CharacterUtil::regularizeString(token_data.text, this->lowercase);
+    size_t size = std::min(token_data.text.size(), 
static_cast<size_t>(LUCENE_MAX_WORD_LEN));
+    set(token, std::string_view(token_data.text.data(), size));
+    token->setStartOffset(token_data.start_offset);

Review Comment:
   [P1] Preserve IK offsets through the pinyin filter
   
   These absolute offsets are immediately lost in the composition this PR adds: 
`PinyinFilter` records the input token's 
`current_start_offset_`/`current_end_offset_`, but builds each candidate from 
zero and writes those token-local values directly. For smart tokens such as 我 
(0-3), 来到 (3-9), 北京 (9-15), filtered outputs restart at 0 for every source 
token, so offsets become non-monotonic and no longer refer to the document. 
Please rebase candidate offsets on the input token span (and use the full input 
span for whole-token alternatives), then add exact multi-token/multibyte offset 
assertions; the new tests currently collect only term strings.



##########
fe/fe-core/src/main/java/org/apache/doris/indexpolicy/IndexPolicy.java:
##########
@@ -59,7 +59,8 @@ public class IndexPolicy implements Writable, 
GsonPostProcessable {
     public static final String PROP_TOKEN_FILTER = "token_filter";
     public static final String PROP_CHAR_FILTER = "char_filter";
     public static final Set<String> BUILTIN_TOKENIZERS = ImmutableSet.of(
-            "empty", "ngram", "edge_ngram", "keyword", "standard", 
"char_group", "basic", "icu", "pinyin");
+            "empty", "ngram", "edge_ngram", "keyword", "standard", 
"char_group", "basic", "icu", "pinyin",
+            "ik_smart", "ik_max_word");

Review Comment:
   [P1] Define an upgrade rule for existing policies with these names
   
   Before this change, users could legally persist an index policy named 
`ik_smart` or `ik_max_word`. Replay/image loading keeps such policies, but 
after adding the names here FE validates the spelling as the new built-in 
before consulting the stored policy, while BE gives its `_name_to_id` entry 
precedence. An old TOKENIZER policy named `ik_smart` (for example 
`type=standard`) will therefore shadow the new IK tokenizer on BE even though 
FE/identity treat it as built-in; a collision of another policy type can make 
later analyzer materialization fail. Please make collision precedence 
consistent and migration-safe (for example preserve an existing policy on both 
sides and reserve the name only for new DDL), with replay coverage.



##########
fe/fe-core/src/main/java/org/apache/doris/indexpolicy/IndexPolicy.java:
##########
@@ -59,7 +59,8 @@ public class IndexPolicy implements Writable, 
GsonPostProcessable {
     public static final String PROP_TOKEN_FILTER = "token_filter";
     public static final String PROP_CHAR_FILTER = "char_filter";
     public static final Set<String> BUILTIN_TOKENIZERS = ImmutableSet.of(
-            "empty", "ngram", "edge_ngram", "keyword", "standard", 
"char_group", "basic", "icu", "pinyin");
+            "empty", "ngram", "edge_ngram", "keyword", "standard", 
"char_group", "basic", "icu", "pinyin",
+            "ik_smart", "ik_max_word");

Review Comment:
   [P1] Canonicalize case-insensitive built-in references before BE lookup
   
   FE normalizes a tokenizer reference before checking this set, so 
`tokenizer=IK_SMART` is accepted and persisted, but BE passes that original 
spelling to `AnalysisFactoryMgr` after only using a normalized copy to look for 
a named policy. The factory registry contains only lowercase 
`ik_smart`/`ik_max_word` keys, so `TOKENIZE`, index construction, or query 
analysis later fails with `Unknown factory name`. Please persist or resolve the 
canonical built-in spelling consistently (and use the same canonicalization in 
analyzer identity); the new tests currently cover lowercase only.



-- 
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]

Reply via email to