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


##########
be/CMakeLists.txt:
##########
@@ -976,6 +996,49 @@ if (BUILD_META_TOOL OR BUILD_INDEX_TOOL)
     add_subdirectory(${SRC_DIR}/tools)
 endif()
 
+if (NOT MAKE_TEST)
+    # Offline generator: compiles the UTF-8 mecab-ipadic source into binary 
files.
+    add_executable(kuromoji_build_dict EXCLUDE_FROM_ALL 
${SRC_DIR}/tools/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})
+
+    set(KUROMOJI_IPADIC_SRC 
"${THIRDPARTY_DIR}/share/mecab-ipadic-2.7.0-20250920"
+        CACHE PATH "UTF-8 mecab-ipadic source directory used to generate the 
kuromoji dictionary")
+    set(KUROMOJI_DICT_OUT "${BASE_DIR}/dict/kuromoji")
+    file(GLOB KUROMOJI_IPADIC_SRC_FILES CONFIGURE_DEPENDS
+         "${KUROMOJI_IPADIC_SRC}/*.csv"
+         "${KUROMOJI_IPADIC_SRC}/*.def")
+    get_filename_component(KUROMOJI_LIBJVM_DIR "${LIB_JVM}" DIRECTORY)
+    set(KUROMOJI_DYLD_PATH "${KUROMOJI_LIBJVM_DIR}")
+    set(KUROMOJI_LD_PATH "${KUROMOJI_LIBJVM_DIR}")
+    if (NOT "$ENV{DYLD_LIBRARY_PATH}" STREQUAL "")
+        set(KUROMOJI_DYLD_PATH 
"${KUROMOJI_LIBJVM_DIR}:$ENV{DYLD_LIBRARY_PATH}")
+    endif()
+    if (NOT "$ENV{LD_LIBRARY_PATH}" STREQUAL "")
+        set(KUROMOJI_LD_PATH "${KUROMOJI_LIBJVM_DIR}:$ENV{LD_LIBRARY_PATH}")
+    endif()
+    add_custom_command(

Review Comment:
   [Major] Publish these four outputs as one atomic generation. The converter 
truncates and writes `system.bin` first, then `matrix.bin`, `chardef.bin`, and 
`unkdict.bin`; if it is interrupted over an older complete set, the first file 
can be new while the existing siblings remain old. With Unix Makefiles, CMake's 
secondary-output rules then `touch_nocreate` those stale siblings without 
rerunning the converter, and the install-time existence checks accept the mixed 
set. (A truly missing sibling is regenerated; an existing stale one is not.) 
Generate into temporaries and atomically publish a validated set before writing 
one completion stamp/generation marker, with an interruption test under Make.



##########
be/src/storage/index/inverted/analyzer/kuromoji/dict/kuromoji_dictionary.cpp:
##########
@@ -0,0 +1,325 @@
+// 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");
+    }
+    if (s.trie_bytes == 0) {
+        return Status::Corruption("kuromoji dict: system.bin has an empty 
trie");
+    }
+    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;
+    // trie_bytes is non-zero and 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));

Review Comment:
   [Major] Validate the Darts structure before publishing this mmap. A nonempty 
four-byte trie passes the current checks, but `set_array()` does not use its 
size during search and `commonPrefixSearch()` follows unit offsets without 
bounds checks; a one-unit trie can therefore read beyond the mapping on the 
first input byte. Even a structurally traversable trie can store a terminal 
value beyond `_runs_count`, which `run_for_value()` silently turns into an 
empty run and changes known words into OOV tokens. Reject invalid 
block/root/transition shapes and terminal values at load time (or use a 
size-aware search), with corrupt-trie mutations covering both cases.



##########
be/src/storage/index/inverted/analyzer/kuromoji/dict/kuromoji_dictionary.cpp:
##########
@@ -0,0 +1,325 @@
+// 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");
+    }
+    if (s.trie_bytes == 0) {
+        return Status::Corruption("kuromoji dict: system.bin has an empty 
trie");
+    }
+    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;
+    // trie_bytes is non-zero and 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 {
+    // Every run must reference a valid [entry_start, entry_start + count) 
slice.
+    auto check_runs = [](const WordIdRun* runs, uint64_t run_count, uint64_t 
entries_count,
+                         const char* what) -> Status {
+        for (uint64_t i = 0; i < run_count; ++i) {
+            if (static_cast<uint64_t>(runs[i].entry_start) + runs[i].count > 
entries_count) {
+                return Status::Corruption(
+                        "kuromoji dict: {} run {} references entries past the 
end", what, i);
+            }
+        }
+        return Status::OK();
+    };
+    // Every entry's context ids must index the connection matrix (used 
directly
+    // as offsets into _cells at query time).
+    auto check_entries = [this](const WordEntry* entries, uint64_t count,
+                                const char* what) -> Status {
+        for (uint64_t i = 0; i < count; ++i) {
+            const WordEntry& e = entries[i];
+            if (e.left_id < 0 || static_cast<uint32_t>(e.left_id) >= 
_backward_size ||
+                e.right_id < 0 || static_cast<uint32_t>(e.right_id) >= 
_forward_size) {
+                return Status::Corruption("kuromoji dict: {} entry {} has 
out-of-range context id",
+                                          what, i);
+            }
+        }
+        return Status::OK();
+    };
+    RETURN_IF_ERROR(check_runs(_runs, _runs_count, _entries_count, "system"));
+    RETURN_IF_ERROR(check_entries(_entries, _entries_count, "system"));
+    RETURN_IF_ERROR(check_runs(_unk_runs, _unk_runs_count, _unk_entries_count, 
"unk"));
+    RETURN_IF_ERROR(check_entries(_unk_entries, _unk_entries_count, "unk"));
+    for (uint32_t cp = 0; cp < 0x10000; ++cp) {
+        if (_catmap[cp] >= CAT_CLASS_COUNT) {
+            return Status::Corruption(
+                    "kuromoji dict: chardef catmap has out-of-range category 
{} at code point {}",
+                    static_cast<uint32_t>(_catmap[cp]), cp);
+        }
+    }
+    if (_unk_entries_count == 0) {
+        return Status::Corruption("kuromoji dict: unknown dictionary has no 
entries");
+    }
+    return Status::OK();
+}
+
+Status KuromojiDictionary::load(const std::string& dir, 
std::unique_ptr<KuromojiDictionary>* out) {
+    auto dict = std::make_unique<KuromojiDictionary>();
+    RETURN_IF_ERROR(dict->map_system(dir + "/system.bin"));
+    RETURN_IF_ERROR(dict->map_matrix(dir + "/matrix.bin"));
+    RETURN_IF_ERROR(dict->map_chardef(dir + "/chardef.bin"));
+    RETURN_IF_ERROR(dict->map_unkdict(dir + "/unkdict.bin"));
+    // Cross-file checks need every file mapped (entries vs. matrix bounds).
+    RETURN_IF_ERROR(dict->validate_ranges());
+    *out = std::move(dict);
+    return Status::OK();
+}
+
+const KuromojiDictionary* KuromojiDictionary::get_or_load(const std::string& 
dir) {
+    static std::mutex mu;
+    static std::map<std::string, std::unique_ptr<KuromojiDictionary>> cache;

Review Comment:
   [Major] Put the successful dictionary mappings under Doris memory/cache 
accounting. All four files are mapped directly, then this function-static map 
retains them behind raw pointers for the process lifetime. Resident pages do 
count toward process RSS and clean file-backed pages remain kernel-reclaimable, 
but none of these bytes belongs to a MemTracker or registered cache, so Doris 
reports them as untracked and CacheManager cannot attribute or explicitly unmap 
them under pressure. The matrix alone is about 3.3 MiB, before the trie, 
entries, and feature blobs. Give the mappings an explicit global owner/tracker 
and an analyzer-safe handle/lifetime policy with defined reclamation behavior.



##########
be/src/storage/index/inverted/analyzer/kuromoji/kuromoji_normalize.h:
##########
@@ -0,0 +1,68 @@
+// 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.
+
+#pragma once
+
+#include <cstdint>
+#include <string>
+#include <string_view>
+
+namespace doris::segment_v2::inverted_index::kuromoji {
+
+// CJK width folding (a subset of Lucene's CJKWidthFilter): full-width ASCII
+// variants U+FF01..U+FF5E -> basic-latin U+0021..U+007E, and the ideographic
+// space U+3000 -> ' '. So ABC123 -> ABC123. Everything else is preserved
+// byte-for-byte. (Half-width katakana -> full-width composition is a TODO.)

Review Comment:
   [Major] Normalize half-width katakana before building the Viterbi lattice. 
The tokenizer currently segments the original bytes, and this later filter 
explicitly preserves U+FF66..U+FF9F, so half-width `カタカナ` follows the OOV path 
and emits a different term from the known full-width `カタカナ`; `ガ` likewise 
remains separate code points instead of composing to `ガ`. A row indexed with 
one common width variant therefore cannot be found with the other. Fold 
half-width katakana plus dakuten/handakuten before segmentation (or otherwise 
guarantee identical segmentation and terms), and add TOKENIZE plus persisted 
index/query tests in both directions.



##########
build.sh:
##########
@@ -568,6 +568,21 @@ update_submodule() {
     fi
 }
 
+if [[ "${CLEAN}" -eq 1 && "${BUILD_BE}" -eq 0 && "${BUILD_FE}" -eq 0 && 
${BUILD_CLOUD} -eq 0 ]]; then
+    clean_gensrc
+    clean_be
+    clean_fe
+    exit 0
+fi
+
+if [[ "${BUILD_BE}" -eq 1 ]]; then
+    
MECAB_IPADIC_DIR="${DORIS_THIRDPARTY}/installed/share/mecab-ipadic-2.7.0-20250920"
+    if [[ ! -d "${MECAB_IPADIC_DIR}" ]]; then

Review Comment:
   [Major] Do not treat directory existence as proof that IPADIC staging 
completed. `build_mecab_ipadic` removes the final directory and copies directly 
into that same path; a failed/interrupted copy leaves `-d` true, so the next 
build skips restaging. If the three `.def` files and any CSV subset arrived, 
the converter accepts it (`lexicon_rows > 0`) and packages a structurally valid 
dictionary with permanently missing terms. Publish a validated versioned 
inventory/completion marker via an atomic rename, and make this check require 
that marker rather than only the directory.



##########
build.sh:
##########
@@ -568,6 +568,21 @@ update_submodule() {
     fi
 }
 
+if [[ "${CLEAN}" -eq 1 && "${BUILD_BE}" -eq 0 && "${BUILD_FE}" -eq 0 && 
${BUILD_CLOUD} -eq 0 ]]; then
+    clean_gensrc
+    clean_be
+    clean_fe
+    exit 0
+fi
+
+if [[ "${BUILD_BE}" -eq 1 ]]; then

Review Comment:
   [Major] Normalize `--compile-bench` into BE mode before this staging 
decision. The option initially sets only `COMPILE_BENCH=1`, so an otherwise 
valid pre-PR third-party cache reaches this block with `BUILD_BE=0` and never 
stages IPADIC. Only later does the script force `BUILD_BE=1`; it then 
configures `MAKE_TEST=OFF` and builds `ALL`, where `kuromoji_dict` fails on the 
missing source directory. Move the mode normalization ahead of all third-party 
requirement checks (or include compile-bench explicitly) and cover an old cache 
without the new share directory.



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