blob: c0bd185995203517603e440129bf9e992eecddcd [file]
// 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.
// Offline tool: compile a UTF-8 mecab-ipadic source directory into the four
// kuromoji .bin files consumed by KuromojiDictionary.
// usage: kuromoji_build_dict <ipadic_src_dir> <out_dir>
// Built on demand via `ninja kuromoji_dict`; never linked into doris_be.
#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <functional>
#include <sstream>
#include <string>
#include <string_view>
#include <system_error>
#include <unordered_map>
#include <utility>
#include <vector>
#include "common/status.h"
#include "storage/index/inverted/analyzer/kuromoji/dict/kuromoji_dictionary_builder.h"
#include "storage/index/inverted/analyzer/kuromoji/dict/kuromoji_ipadic_parser.h"
namespace fs = std::filesystem;
using namespace doris::segment_v2::inverted_index::kuromoji;
using doris::Status;
namespace {
bool read_file(const std::string& path, std::string* out) {
std::ifstream in(path, std::ios::binary);
if (!in) {
std::fprintf(stderr, "cannot open %s\n", path.c_str());
return false;
}
std::ostringstream ss;
ss << in.rdbuf();
*out = ss.str();
return true;
}
void for_each_line(const std::string& content, const std::function<void(std::string_view)>& fn) {
std::size_t i = 0;
while (i < content.size()) {
auto nl = content.find('\n', i);
if (nl == std::string::npos) {
nl = content.size();
}
std::string_view line(content.data() + i, nl - i);
if (!line.empty() && line.back() == '\r') {
line.remove_suffix(1);
}
if (!line.empty()) {
fn(line);
}
i = nl + 1;
}
}
} // namespace
int main(int argc, char** argv) {
if (argc < 3) {
std::fprintf(stderr, "usage: %s <ipadic_src_dir> <out_dir>\n", argv[0]);
return 2;
}
const std::string src = argv[1];
const std::string out = argv[2];
std::error_code ec;
if (!fs::is_directory(src, ec)) {
std::fprintf(stderr,
"kuromoji dict source directory not found: %s\n"
"stage the mecab-ipadic thirdparty package before building the dictionary.\n",
src.c_str());
return 1;
}
fs::create_directories(out, ec);
// --- system dictionary: group all *.csv lexicon rows by surface (homographs) ---
std::vector<std::string> csv_paths;
for (const auto& entry : fs::directory_iterator(src)) {
if (entry.is_regular_file() && entry.path().extension() == ".csv") {
csv_paths.push_back(entry.path().string());
}
}
std::sort(csv_paths.begin(), csv_paths.end());
std::unordered_map<std::string, std::vector<BuilderWord>> by_surface;
std::size_t lexicon_rows = 0;
for (const auto& path : csv_paths) {
std::string content;
if (!read_file(path, &content)) {
return 1;
}
Status line_st = Status::OK();
for_each_line(content, [&](std::string_view line) {
if (!line_st.ok()) {
return;
}
std::string surface;
BuilderWord w;
line_st = parse_lexicon_line(line, &surface, &w);
if (!line_st.ok()) {
return;
}
by_surface[surface].push_back(std::move(w));
++lexicon_rows;
});
if (!line_st.ok()) {
std::fprintf(stderr, "lexicon parse failed in %s: %s\n", path.c_str(),
line_st.to_string().c_str());
return 1;
}
}
if (lexicon_rows == 0) {
std::fprintf(stderr, "no lexicon rows parsed from %s; source missing or truncated?\n",
src.c_str());
return 1;
}
SystemDictInput sys;
sys.surfaces.reserve(by_surface.size());
for (auto& kv : by_surface) {
std::sort(kv.second.begin(), kv.second.end(),
[](const BuilderWord& a, const BuilderWord& b) {
if (a.word_cost != b.word_cost) {
return a.word_cost < b.word_cost;
}
if (a.left_id != b.left_id) {
return a.left_id < b.left_id;
}
if (a.right_id != b.right_id) {
return a.right_id < b.right_id;
}
return a.feature < b.feature;
});
sys.surfaces.emplace_back(kv.first, std::move(kv.second));
}
if (Status st = KuromojiDictionaryBuilder::write_system(out + "/system.bin.tmp", sys);
!st.ok()) {
std::fprintf(stderr, "write_system failed: %s\n", st.to_string().c_str());
return 1;
}
// --- connection cost matrix ---
std::string matrix_txt;
MatrixInput matrix;
if (!read_file(src + "/matrix.def", &matrix_txt) ||
!parse_matrix_def(matrix_txt, &matrix).ok() ||
!KuromojiDictionaryBuilder::write_matrix(out + "/matrix.bin.tmp", matrix).ok()) {
std::fprintf(stderr, "matrix.def build failed\n");
return 1;
}
// --- character definitions ---
std::string char_txt;
CharDefInput chardef;
if (!read_file(src + "/char.def", &char_txt) || !parse_char_def(char_txt, &chardef).ok() ||
!KuromojiDictionaryBuilder::write_chardef(out + "/chardef.bin.tmp", chardef).ok()) {
std::fprintf(stderr, "char.def build failed\n");
return 1;
}
// --- unknown-word dictionary ---
std::string unk_txt;
UnkDictInput unk;
if (!read_file(src + "/unk.def", &unk_txt) || !parse_unk_def(unk_txt, &unk).ok() ||
!KuromojiDictionaryBuilder::write_unkdict(out + "/unkdict.bin.tmp", unk).ok()) {
std::fprintf(stderr, "unk.def build failed\n");
return 1;
}
for (const char* name : {"system.bin", "matrix.bin", "chardef.bin", "unkdict.bin"}) {
std::error_code rec;
fs::rename(out + "/" + name + ".tmp", out + "/" + name, rec);
if (rec) {
std::fprintf(stderr, "failed to publish %s: %s\n", name, rec.message().c_str());
return 1;
}
}
std::fprintf(stderr,
"kuromoji dict built: %zu surfaces (%zu lexicon rows), matrix %ux%u -> %s\n",
sys.surfaces.size(), lexicon_rows, matrix.forward_size, matrix.backward_size,
out.c_str());
return 0;
}