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


##########
be/src/tools/CMakeLists.txt:
##########
@@ -116,3 +116,51 @@ if (BUILD_INDEX_TOOL)
             )
     endif()
 endif()
+
+# Offline generator for the Japanese kuromoji dictionary. Compiles the UTF-8
+# mecab-ipadic source into the four dict/kuromoji/*.bin files that the BE 
install
+# rule ships. EXCLUDE_FROM_ALL and not installed: it is a maintenance tool, not
+# part of doris_be. Regenerate with `ninja kuromoji_dict`.
+add_executable(kuromoji_build_dict EXCLUDE_FROM_ALL
+    kuromoji_build_dict.cpp
+)
+
+target_include_directories(kuromoji_build_dict PRIVATE 
${PROJECT_SOURCE_DIR}/..)
+
+pch_reuse(kuromoji_build_dict)
+
+set_target_properties(kuromoji_build_dict PROPERTIES ENABLE_EXPORTS 1)
+
+if (COMPILER_CLANG)
+    target_compile_options(kuromoji_build_dict PRIVATE
+    -Wno-implicit-int-conversion
+    -Wno-shorten-64-to-32
+    )
+endif()
+
+target_link_libraries(kuromoji_build_dict
+    ${DORIS_LINK_LIBS}
+)
+
+# `ninja kuromoji_dict` runs the tool over the staged mecab-ipadic source and
+# (re)writes dict/kuromoji/*.bin. Point KUROMOJI_IPADIC_SRC elsewhere if the
+# source is not under the thirdparty share directory.
+set(KUROMOJI_IPADIC_SRC "${THIRDPARTY_DIR}/share/mecab-ipadic-2.7.0-20250920"

Review Comment:
   Making `kuromoji_dict` part of `ALL` means a normal non-test BE build now 
requires `${THIRDPARTY_DIR}/share/mecab-ipadic-2.7.0-20250920`, but that 
directory is only staged when the new `mecab_ipadic` thirdparty package runs. 
`build.sh` still skips `build-thirdparty.sh` as soon as the old last-library 
sentinel exists, so an existing thirdparty cache from before this PR can pass 
the wrapper check and then fail here with a missing IPADIC source. Please 
extend the thirdparty freshness/pre-build check to require the staged 
mecab-ipadic directory, or otherwise ensure the default build path refreshes it 
before CMake runs this target.



##########
fe/fe-catalog/src/main/java/org/apache/doris/analysis/InvertedIndexProperties.java:
##########
@@ -87,7 +91,8 @@ public static String getInvertedIndexParserMode(Map<String, 
String> properties)
         }
         return mode != null ? mode :
             INVERTED_INDEX_PARSER_IK.equals(parser) ? 
INVERTED_INDEX_PARSER_SMART :
-                INVERTED_INDEX_PARSER_COARSE_GRANULARITY;

Review Comment:
   This adds the Kuromoji default for the `parser` path, but the PR also makes 
`PROPERTIES("analyzer"="kuromoji")` FE-valid via 
`IndexPolicy.BUILTIN_ANALYZERS`. That accepted analyzer-only path has no 
`parser` key, so this helper still falls through to `coarse_grained`; BE has 
the same parser-only defaulting before 
`create_builtin_analyzer(PARSER_KUROMOJI, ...)`, and 
`kuromoji_mode_from_string("coarse_grained")` rejects it during index 
build/TOKENIZE/MATCH. Please either reject analyzer-only `kuromoji` and require 
`parser="kuromoji"`, or make the analyzer key participate in the Kuromoji mode 
default/validation and add coverage for omitted and explicit modes on 
`analyzer="kuromoji"`.



##########
be/src/storage/index/inverted/analyzer/kuromoji/kuromoji_viterbi.cpp:
##########
@@ -0,0 +1,266 @@
+// 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.
+
+#include "storage/index/inverted/analyzer/kuromoji/kuromoji_viterbi.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <limits>
+
+namespace doris::segment_v2::inverted_index::kuromoji {
+
+namespace {
+
+constexpr int64_t KMJ_INF = std::numeric_limits<int64_t>::max() / 4;
+constexpr uint32_t MAX_UNKNOWN_GROUP_CHARS = 1024;
+
+// Search/Extended-mode compound-decomposition penalties, matching Lucene's
+// JapaneseTokenizer. Lengths are counted in code points. A token longer than 
the
+// length threshold is penalized so the minimum-cost path prefers its shorter
+// parts: all-kanji runs over KANJI_LENGTH chars, other runs over OTHER_LENGTH.
+constexpr uint32_t SEARCH_MODE_KANJI_LENGTH = 2;
+constexpr int64_t SEARCH_MODE_KANJI_PENALTY = 3000;
+constexpr uint32_t SEARCH_MODE_OTHER_LENGTH = 7;
+constexpr int64_t SEARCH_MODE_OTHER_PENALTY = 1700;
+
+struct DecodedCp {
+    char32_t cp;
+    uint32_t len;
+};
+
+// Decode one UTF-8 code point at text[pos]. Invalid/truncated -> single byte.
+DecodedCp decode_utf8(std::string_view text, std::size_t pos) {
+    auto b0 = static_cast<unsigned char>(text[pos]);
+    const std::size_t avail = text.size() - pos;
+    if (b0 < 0x80) {
+        return {b0, 1};
+    }
+    if ((b0 >> 5) == 0x6 && avail >= 2) {
+        auto b1 = static_cast<unsigned char>(text[pos + 1]);
+        return {static_cast<char32_t>(((b0 & 0x1FU) << 6) | (b1 & 0x3FU)), 2};
+    }
+    if ((b0 >> 4) == 0xE && avail >= 3) {
+        auto b1 = static_cast<unsigned char>(text[pos + 1]);
+        auto b2 = static_cast<unsigned char>(text[pos + 2]);
+        return {static_cast<char32_t>(((b0 & 0x0FU) << 12) | ((b1 & 0x3FU) << 
6) | (b2 & 0x3FU)),
+                3};
+    }
+    if ((b0 >> 3) == 0x1E && avail >= 4) {
+        auto b1 = static_cast<unsigned char>(text[pos + 1]);
+        auto b2 = static_cast<unsigned char>(text[pos + 2]);
+        auto b3 = static_cast<unsigned char>(text[pos + 3]);
+        return {static_cast<char32_t>(((b0 & 0x07U) << 18) | ((b1 & 0x3FU) << 
12) |
+                                      ((b2 & 0x3FU) << 6) | (b3 & 0x3FU)),
+                4};
+    }
+    return {b0, 1};
+}
+
+// Lucene JapaneseTokenizer's search-mode penalty for the token covering
+// [start, end) bytes: penalize long compounds so the Viterbi prefers their
+// shorter parts. Returns 0 for tokens at or under the length thresholds.
+int64_t compute_penalty(const KuromojiDictionary& dict, std::string_view text, 
uint32_t start,
+                        uint32_t end) {
+    uint32_t length = 0;
+    bool all_kanji = true;
+    for (uint32_t p = start; p < end;) {
+        const DecodedCp d = decode_utf8(text, p);
+        if (dict.char_category(d.cp) != CAT_KANJI) {
+            all_kanji = false;
+        }
+        p += d.len;
+        ++length;
+    }
+    if (length > SEARCH_MODE_KANJI_LENGTH) {
+        if (all_kanji) {
+            return static_cast<int64_t>(length - SEARCH_MODE_KANJI_LENGTH) *
+                   SEARCH_MODE_KANJI_PENALTY;
+        }
+        if (length > SEARCH_MODE_OTHER_LENGTH) {
+            return static_cast<int64_t>(length - SEARCH_MODE_OTHER_LENGTH) *
+                   SEARCH_MODE_OTHER_PENALTY;
+        }
+    }
+    return 0;
+}
+
+// A lattice node spanning [start, end) bytes of the input.
+struct VNode {
+    uint32_t start;
+    uint32_t end;
+    int16_t left_id;
+    int16_t right_id;
+    int16_t word_cost;
+    bool known;
+    uint32_t word_id;
+    int64_t total_cost;
+    int back; // previous node index, -1 if none
+};
+
+} // namespace
+
+void KuromojiViterbi::segment(std::string_view text, 
std::vector<KuromojiMorpheme>* out) const {
+    out->clear();
+    const auto n = static_cast<uint32_t>(text.size());
+    if (n == 0) {
+        return;
+    }
+
+    std::vector<VNode> nodes;
+    std::vector<int32_t> end_head(n + 1, -1);
+    std::vector<int32_t> end_next;
+
+    // BOS (index 0): ends at position 0, context id 0, zero cost.
+    nodes.push_back(VNode {0, 0, 0, 0, 0, false, 0, 0, -1});
+    end_next.push_back(-1);
+    end_head[0] = 0;
+
+    // Add a node and relax it against all nodes ending at its start position.
+    auto add_node = [&](uint32_t s, uint32_t e, int16_t lid, int16_t rid, 
int16_t wcost, bool known,
+                        uint32_t wid) {
+        int64_t best = KMJ_INF;
+        int best_prev = -1;
+        for (int pe = end_head[s]; pe >= 0; pe = end_next[pe]) {
+            const VNode& pv = nodes[static_cast<std::size_t>(pe)];
+            if (pv.total_cost >= KMJ_INF) {
+                continue;
+            }
+            const int64_t c =
+                    pv.total_cost + 
_dict.connection_cost(static_cast<uint32_t>(pv.right_id),
+                                                          
static_cast<uint32_t>(lid));
+            if (c <= best) {
+                best = c;
+                best_prev = pe;
+            }
+        }
+        if (best_prev < 0) {
+            return;
+        }
+        // Search/Extended mode penalizes long compounds so shorter parts win.

Review Comment:
   This penalty changes the primary Viterbi path, so search/extended mode emits 
only the decompounded path and no longer indexes the original compound token. 
Lucene/OpenSearch-style search mode normally emits the alternate segmentation 
in addition to the compound unless an explicit discard-compound option is 
enabled; this PR exposes no such option, and the tests only assert that `東京` 
appears. Please either emit both the original compound and decompounded tokens 
for the default mode, or add an explicit validated 
`discard_compound_token`-style property and test both the full-compound and 
part-token MATCH/TOKENIZE behavior.



##########
be/src/storage/index/inverted/analyzer/kuromoji/dict/kuromoji_dictionary.cpp:
##########
@@ -0,0 +1,314 @@
+// 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.
+
+#include "storage/index/inverted/analyzer/kuromoji/dict/kuromoji_dictionary.h"
+
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <cstring>
+#include <limits>
+#include <map>
+#include <mutex>
+
+#include "common/logging.h"
+
+namespace doris::segment_v2::inverted_index::kuromoji {
+
+namespace {
+Status check_region(const char* what, uint64_t offset, uint64_t count, 
uint64_t elem,
+                    uint64_t min_offset, std::size_t size) {
+    if (elem != 0 && count > std::numeric_limits<uint64_t>::max() / elem) {
+        return Status::Corruption("kuromoji dict: {} count overflow ({} x 
{})", what, count, elem);
+    }
+    const uint64_t bytes = count * elem;
+    if (offset < min_offset || offset > size || bytes > 
static_cast<uint64_t>(size) - offset) {
+        return Status::Corruption("kuromoji dict: {} out of range (offset={}, 
bytes={}, file={})",
+                                  what, offset, bytes, size);
+    }
+    return Status::OK();
+}
+} // namespace
+
+MappedFile::~MappedFile() {
+    if (_data != nullptr) {
+        ::munmap(const_cast<uint8_t*>(_data), _size);
+        _data = nullptr;
+        _size = 0;
+    }
+}
+
+Status MappedFile::open(const std::string& path) {
+    int fd = ::open(path.c_str(), O_RDONLY);
+    if (fd < 0) {
+        return Status::IOError("kuromoji dict: cannot open {}", path);
+    }
+    struct stat st {};
+    if (::fstat(fd, &st) != 0 || st.st_size <= 0) {
+        ::close(fd);
+        return Status::IOError("kuromoji dict: cannot stat {}", path);
+    }
+    auto bytes = static_cast<std::size_t>(st.st_size);
+    void* m = ::mmap(nullptr, bytes, PROT_READ, MAP_PRIVATE, fd, 0);
+    ::close(fd);
+    if (m == MAP_FAILED) {
+        return Status::IOError("kuromoji dict: mmap failed for {}", path);
+    }
+    _data = static_cast<const uint8_t*>(m);
+    _size = bytes;
+    return Status::OK();
+}
+
+Status KuromojiDictionary::check_header(const uint8_t* p, std::size_t size, 
KmjFileKind kind) {
+    if (size < sizeof(KmjFileHeader)) {
+        return Status::Corruption("kuromoji dict: file too small");
+    }
+    KmjFileHeader h {};
+    std::memcpy(&h, p, sizeof(h));
+    if (std::memcmp(h.magic, KMJ_MAGIC, sizeof(h.magic)) != 0) {
+        return Status::Corruption("kuromoji dict: bad magic");
+    }
+    if (h.format_version != KMJ_FORMAT_VERSION) {
+        return Status::Corruption("kuromoji dict: version {} != {}", 
h.format_version,
+                                  KMJ_FORMAT_VERSION);
+    }
+    if (h.file_kind != static_cast<uint32_t>(kind)) {
+        return Status::Corruption("kuromoji dict: wrong file_kind {}", 
h.file_kind);
+    }
+    if (h.file_size != size) {
+        return Status::Corruption("kuromoji dict: file_size {} != actual {}", 
h.file_size, size);
+    }
+    return Status::OK();
+}
+
+std::string_view KuromojiDictionary::feature_at(const uint8_t* blob, uint64_t 
blob_bytes,
+                                                uint32_t off) {
+    if (off == KMJ_NO_FEATURE || blob == nullptr || static_cast<uint64_t>(off) 
+ 2 > blob_bytes) {
+        return {};
+    }
+    auto len = static_cast<uint16_t>(static_cast<uint16_t>(blob[off]) |
+                                     static_cast<uint16_t>(blob[off + 1] << 
8));
+    if (static_cast<uint64_t>(off) + 2 + len > blob_bytes) {
+        return {};
+    }
+    return {reinterpret_cast<const char*>(blob + off + 2), len};
+}
+
+Status KuromojiDictionary::map_system(const std::string& path) {
+    RETURN_IF_ERROR(_system_map.open(path));
+    const uint8_t* p = _system_map.data();
+    const std::size_t size = _system_map.size();
+    RETURN_IF_ERROR(check_header(p, size, KMJ_KIND_SYSTEM));
+    constexpr uint64_t kHdrEnd = sizeof(KmjFileHeader) + 
sizeof(KmjSystemHeader);
+    if (size < kHdrEnd) {
+        return Status::Corruption("kuromoji dict: system.bin truncated 
sub-header");
+    }
+    KmjSystemHeader s {};
+    std::memcpy(&s, p + sizeof(KmjFileHeader), sizeof(s));
+    // The trie is read as 4-byte Darts units, so both offset and length must 
be
+    // 4-byte aligned/sized before set_array() walks them.
+    if (s.trie_offset % 4 != 0 || s.trie_bytes % 4 != 0) {
+        return Status::Corruption("kuromoji dict: trie not 4-byte aligned");
+    }
+    RETURN_IF_ERROR(check_region("system trie", s.trie_offset, s.trie_bytes, 
1, kHdrEnd, size));
+    RETURN_IF_ERROR(check_region("system runs", s.runs_offset, s.runs_count, 
sizeof(WordIdRun),
+                                 kHdrEnd, size));
+    RETURN_IF_ERROR(check_region("system entries", s.entries_offset, 
s.entries_count,
+                                 sizeof(WordEntry), kHdrEnd, size));
+    RETURN_IF_ERROR(
+            check_region("system features", s.features_offset, 
s.features_bytes, 1, kHdrEnd, size));
+    _runs = reinterpret_cast<const WordIdRun*>(p + s.runs_offset);
+    _runs_count = s.runs_count;
+    _entries = reinterpret_cast<const WordEntry*>(p + s.entries_offset);
+    _entries_count = s.entries_count;
+    _features = p + s.features_offset;
+    _features_bytes = s.features_bytes;
+    if (s.trie_bytes > 0) {
+        // size is in 4-byte units; the mmap outlives _trie (both owned by 
this object).
+        _trie.set_array(p + s.trie_offset, 
static_cast<std::size_t>(s.trie_bytes / 4));
+    }
+    return Status::OK();
+}
+
+Status KuromojiDictionary::map_matrix(const std::string& path) {
+    RETURN_IF_ERROR(_matrix_map.open(path));
+    const uint8_t* p = _matrix_map.data();
+    const std::size_t size = _matrix_map.size();
+    RETURN_IF_ERROR(check_header(p, size, KMJ_KIND_MATRIX));
+    constexpr uint64_t kHdrEnd = sizeof(KmjFileHeader) + 
sizeof(KmjMatrixHeader);
+    if (size < kHdrEnd) {
+        return Status::Corruption("kuromoji dict: matrix.bin truncated 
sub-header");
+    }
+    KmjMatrixHeader m {};
+    std::memcpy(&m, p + sizeof(KmjFileHeader), sizeof(m));
+    if (m.forward_size == 0 || m.backward_size == 0) {
+        return Status::Corruption("kuromoji dict: matrix has a zero 
dimension");
+    }
+    const uint64_t cells = static_cast<uint64_t>(m.forward_size) * 
m.backward_size;
+    RETURN_IF_ERROR(
+            check_region("matrix cells", m.cells_offset, cells, 
sizeof(int16_t), kHdrEnd, size));
+    _forward_size = m.forward_size;
+    _backward_size = m.backward_size;
+    _cells = reinterpret_cast<const int16_t*>(p + m.cells_offset);
+    return Status::OK();
+}
+
+Status KuromojiDictionary::map_chardef(const std::string& path) {
+    RETURN_IF_ERROR(_chardef_map.open(path));
+    const uint8_t* p = _chardef_map.data();
+    const std::size_t size = _chardef_map.size();
+    RETURN_IF_ERROR(check_header(p, size, KMJ_KIND_CHARDEF));
+    constexpr uint64_t kHdrEnd = sizeof(KmjFileHeader) + 
sizeof(KmjCharDefHeader);
+    if (size < kHdrEnd) {
+        return Status::Corruption("kuromoji dict: chardef.bin truncated 
sub-header");
+    }
+    KmjCharDefHeader c {};
+    std::memcpy(&c, p + sizeof(KmjFileHeader), sizeof(c));
+    if (c.class_count != CAT_CLASS_COUNT) {
+        return Status::Corruption("kuromoji dict: chardef class_count {} != 
{}", c.class_count,
+                                  static_cast<uint32_t>(CAT_CLASS_COUNT));
+    }
+    // catmap is exactly one byte per BMP code point.
+    RETURN_IF_ERROR(check_region("chardef catmap", c.catmap_offset, 0x10000, 
1, kHdrEnd, size));
+    RETURN_IF_ERROR(check_region("chardef defs", c.defs_offset, c.class_count, 
sizeof(CategoryDef),
+                                 kHdrEnd, size));
+    _catmap = p + c.catmap_offset;
+    _defs = reinterpret_cast<const CategoryDef*>(p + c.defs_offset);
+    return Status::OK();
+}
+
+Status KuromojiDictionary::map_unkdict(const std::string& path) {
+    RETURN_IF_ERROR(_unk_map.open(path));
+    const uint8_t* p = _unk_map.data();
+    const std::size_t size = _unk_map.size();
+    RETURN_IF_ERROR(check_header(p, size, KMJ_KIND_UNKDICT));
+    constexpr uint64_t kHdrEnd = sizeof(KmjFileHeader) + sizeof(KmjUnkHeader);
+    if (size < kHdrEnd) {
+        return Status::Corruption("kuromoji dict: unkdict.bin truncated 
sub-header");
+    }
+    KmjUnkHeader u {};
+    std::memcpy(&u, p + sizeof(KmjFileHeader), sizeof(u));
+    if (u.class_count != CAT_CLASS_COUNT) {
+        return Status::Corruption("kuromoji dict: unkdict class_count {} != 
{}", u.class_count,
+                                  static_cast<uint32_t>(CAT_CLASS_COUNT));
+    }
+    RETURN_IF_ERROR(check_region("unk runs", u.runs_offset, u.class_count, 
sizeof(WordIdRun),
+                                 kHdrEnd, size));
+    RETURN_IF_ERROR(check_region("unk entries", u.entries_offset, 
u.entries_count,
+                                 sizeof(WordEntry), kHdrEnd, size));
+    RETURN_IF_ERROR(
+            check_region("unk features", u.features_offset, u.features_bytes, 
1, kHdrEnd, size));
+    _unk_runs = reinterpret_cast<const WordIdRun*>(p + u.runs_offset);
+    _unk_runs_count = u.class_count;
+    _unk_entries = reinterpret_cast<const WordEntry*>(p + u.entries_offset);
+    _unk_entries_count = u.entries_count;
+    _unk_features = p + u.features_offset;
+    _unk_features_bytes = u.features_bytes;
+    return Status::OK();
+}
+
+Status KuromojiDictionary::validate_ranges() const {

Review Comment:
   There are still malformed dictionary shapes that pass this validation and 
crash or read unrelated metadata later: `system.bin` can have `trie_bytes == 
0`, leaving the Darts array unset before `common_prefix_search()` dereferences 
it; `chardef.bin` can store category bytes >= `CAT_CLASS_COUNT`, which 
`is_invoke()`/`is_group()` use to index `_defs`; and an unknown dictionary can 
leave zero-count runs, after which the Viterbi safety net emits `known=false, 
word_id=0` and the tokenizer calls `unknown_word(0)`. Please reject these 
artifacts during load/build, with tests that mutate each case to prove load 
fails instead of installing a crashable dictionary.



##########
regression-test/suites/inverted_index_p0/analyzer/test_japanese_analyzer.groovy:
##########
@@ -0,0 +1,80 @@
+// 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_japanese_analyzer", "p0") {
+    def tableName = "test_japanese_analyzer"
+
+    def backendId_to_backendIP = [:]
+    def backendId_to_backendHttpPort = [:]
+    getBackendIpHttpPort(backendId_to_backendIP, backendId_to_backendHttpPort)
+    def set_be_config = { key, value ->
+        for (String backend_id : backendId_to_backendIP.keySet()) {
+            update_be_config(backendId_to_backendIP.get(backend_id),
+                             backendId_to_backendHttpPort.get(backend_id), 
key, value)
+        }
+    }
+
+    sql "DROP TABLE IF EXISTS ${tableName}"
+    // kuromoji is disabled by default; enable it for this test.
+    set_be_config("enable_kuromoji_analyzer", "true")
+    try {
+        sql """
+          CREATE TABLE ${tableName} (
+            `id` int(11) NULL COMMENT "",
+            `content` text NULL COMMENT "",
+            INDEX content_idx (`content`) USING INVERTED PROPERTIES("parser" = 
"kuromoji", "parser_mode" = "search") COMMENT '',
+          ) ENGINE=OLAP
+          DUPLICATE KEY(`id`)
+          COMMENT "OLAP"
+          DISTRIBUTED BY RANDOM BUCKETS 1
+          PROPERTIES (
+            "replication_allocation" = "tag.location.default: 1"
+          );
+        """
+
+        sql """ INSERT INTO ${tableName} VALUES (1, "東京都に住んでいます"); """
+        sql """ INSERT INTO ${tableName} VALUES (2, "私は寿司が好きです"); """
+        sql """ INSERT INTO ${tableName} VALUES (3, "Apache Doris は高速です"); """
+        sql "sync"
+
+        // The kuromoji IPADIC dictionary ships with the package (built by the
+        // kuromoji_dict target), so this exercises real morphological 
analysis --
+        // The assertions below cover the real.
+        // Search mode decomposes the compound 東京都 into 東京 + 都, so a 東京 query
+        // matches row 1 (a single-character 東 query would NOT, unlike a 
unigram split).
+        def tokyo = sql """ SELECT id FROM ${tableName} WHERE content MATCH 
'東京' ORDER BY id; """

Review Comment:
   This new regression is checking deterministic SQL results with manual 
assertions and also uses `def tableName` plus a final table drop. The repo 
regression-test contract asks ordinary single-table tests to hardcode the table 
name, keep the pre-test drop, preserve the table after the run, and record 
deterministic results with `qt_`/`order_qt_` plus the generated `.out` file 
instead of handwritten assertions. Please convert these result checks to the 
normal regression format so the Kuromoji coverage is durable in CI output.



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