nishant94 commented on code in PR #64667: URL: https://github.com/apache/doris/pull/64667#discussion_r3595289326
########## 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: Updated ########## 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: Fixed -- 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]
