tai'zhuan'qian'le 出「大赚钱了」:大 也读 tai,词库里这一行权重 4719,占 tai 的 0.47%,已有的 reading_prior(看 P(字|拼音),1% 免罚线)只扣 0.33,翻不过语言模型 对「大赚」约 1.6 的偏好。换个角度看,大 读 tai 只占它全部权重的 0.14%——这才是 「这个字几乎不这么读」,语言模型只认汉字,这一维要由读音补上。 词格跨度查询对单字行加一项 P(读音|字) 惩罚:占比低于 10% 的部分按 log10 扣分, 上限 2.5。长 chang、重 chong、行 hang 这类常见第二读音不扣;大 tai/dai、会 kuai、 都 du、给 ji 扣 1.5~2。只罚单字,词本身就是读音的证据(会计、成都、大夫)。多音字 各读音的权重和在第一次整句联想时扫一遍单字表载入,全拼与双拼(含直接辅助码)共用。 自建 111 句多音字评测从 79 提到 82,无回归;5% 档翻不过「大赚钱了」,20% 与 10% 结果相同,取保守的一边。test_engine_smoke 加固定权重的断言,句子评测集补两条。
1441 lines
50 KiB
C++
1441 lines
50 KiB
C++
#include "../contracts/assets/assets.h"
|
||
#include "../contracts/dictionary/format.h"
|
||
#include "quanpin_query.h"
|
||
|
||
#include "fuzzy_pinyin.h"
|
||
#include "quanpin_utils.h"
|
||
#include "../shuangpin/shuangpin_utils.h"
|
||
#include <algorithm>
|
||
#include <climits>
|
||
#include <cmath>
|
||
#include <map>
|
||
#include <sqlite3.h>
|
||
#include <stdexcept>
|
||
#include <string>
|
||
#include <unordered_map>
|
||
#include <unordered_set>
|
||
|
||
namespace quanpin
|
||
{
|
||
namespace
|
||
{
|
||
|
||
std::vector<std::string> split(const std::string &text, char delimiter)
|
||
{
|
||
std::vector<std::string> parts;
|
||
size_t start = 0;
|
||
while (true)
|
||
{
|
||
const size_t pos = text.find(delimiter, start);
|
||
if (pos == std::string::npos)
|
||
{
|
||
parts.push_back(text.substr(start));
|
||
return parts;
|
||
}
|
||
parts.push_back(text.substr(start, pos - start));
|
||
start = pos + 1;
|
||
}
|
||
}
|
||
|
||
constexpr size_t kCorrectionPathLimit = 32;
|
||
|
||
using CorrectionAliases = std::unordered_map<std::string, std::vector<std::string>>;
|
||
|
||
// 非纠错的拼写归一:ü 归一(jv -> ju)。它不属于手误纠正,永远生效——关掉纠错开关
|
||
// 也要把 jv 当 ju 查库(jv 本身不是合法音节,靠这条归一才能切出来)。n/l 系的
|
||
// nue/lue -> nve/lve 由 normalize_umlaut_aliases 在段级完成,不进这张表。
|
||
const CorrectionAliases &pinyin_normalization_aliases()
|
||
{
|
||
static const CorrectionAliases kAliases = [] {
|
||
CorrectionAliases aliases;
|
||
aliases["jv"].push_back("ju");
|
||
return aliases;
|
||
}();
|
||
return kAliases;
|
||
}
|
||
|
||
// 手误别名:换位形状(aing/uang/ian/uan/iao/agn/egn/on)、多一个 h、漏一个 g,外加
|
||
// 上面那条 ü 归一。属纠错,按掩码门控(开关关掉时不参与切分)。
|
||
const CorrectionAliases &pinyin_correction_aliases()
|
||
{
|
||
static const CorrectionAliases kAliases = [] {
|
||
CorrectionAliases aliases;
|
||
const auto add_alias = [&](const std::string &typed, const std::string &canonical) {
|
||
auto &canonical_syllables = aliases[typed];
|
||
if (std::find(canonical_syllables.begin(), canonical_syllables.end(), canonical) ==
|
||
canonical_syllables.end())
|
||
{
|
||
canonical_syllables.push_back(canonical);
|
||
}
|
||
};
|
||
const auto add_suffix_aliases = [&](const std::string &canonical_suffix, const std::string &typed_suffix) {
|
||
for (const auto &syllable : intact_pinyin_list())
|
||
{
|
||
if (syllable.size() < canonical_suffix.size() ||
|
||
syllable.compare(syllable.size() - canonical_suffix.size(), canonical_suffix.size(),
|
||
canonical_suffix) != 0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
const std::string prefix = syllable.substr(0, syllable.size() - canonical_suffix.size());
|
||
add_alias(prefix + typed_suffix, syllable);
|
||
}
|
||
};
|
||
|
||
add_suffix_aliases("iang", "aing");
|
||
add_suffix_aliases("uang", "aung");
|
||
add_suffix_aliases("ian", "ain");
|
||
add_suffix_aliases("uan", "aun");
|
||
add_suffix_aliases("iao", "aio");
|
||
add_suffix_aliases("ing", "ihng");
|
||
add_suffix_aliases("ang", "agn");
|
||
add_suffix_aliases("eng", "egn");
|
||
add_alias("egn", "eng");
|
||
// ü 归一也留在这张表里,纠错开着时一张表就够;关掉时只查 pinyin_normalization_aliases。
|
||
add_alias("jv", "ju");
|
||
// Missing final g before the next syllable: zhonguo -> zhong guo.
|
||
add_suffix_aliases("ong", "on");
|
||
|
||
// Prefer a transposed h (ahng -> hang), while retaining the extra-h
|
||
// interpretation as an alternative (ahng -> ang).
|
||
add_suffix_aliases("hang", "ahng");
|
||
add_suffix_aliases("ang", "ahng");
|
||
|
||
// Prefer a transposed h (cehng -> cheng), while retaining the extra-h
|
||
// interpretation as an alternative (cehng -> ceng).
|
||
add_suffix_aliases("heng", "ehng");
|
||
add_suffix_aliases("eng", "ehng");
|
||
return aliases;
|
||
}();
|
||
return kAliases;
|
||
}
|
||
|
||
std::vector<Segments> cut_one_piece_with_corrections(const std::string &pinyin, bool include_correction_aliases)
|
||
{
|
||
struct RankedPath
|
||
{
|
||
Segments segments;
|
||
std::vector<size_t> typed_lengths;
|
||
std::vector<size_t> correction_ranks;
|
||
};
|
||
|
||
const auto &valid_pinyin = intact_pinyin_set();
|
||
// include=false keeps only the ü normalization (jv -> ju); the hand-mistake
|
||
// aliases are part of autocorrection and follow its switch.
|
||
const auto &aliases = include_correction_aliases ? pinyin_correction_aliases() : pinyin_normalization_aliases();
|
||
std::unordered_map<size_t, std::vector<RankedPath>> memo;
|
||
|
||
const auto solve = [&](auto &&self, size_t index) -> std::vector<RankedPath> {
|
||
if (index == pinyin.size())
|
||
{
|
||
return {RankedPath{}};
|
||
}
|
||
if (const auto found = memo.find(index); found != memo.end())
|
||
{
|
||
return found->second;
|
||
}
|
||
|
||
std::vector<RankedPath> paths;
|
||
for (size_t end = pinyin.size(); end > index; --end)
|
||
{
|
||
const std::string typed = pinyin.substr(index, end - index);
|
||
std::vector<std::string> canonical_syllables;
|
||
if (const auto alias = aliases.find(typed); alias != aliases.end())
|
||
{
|
||
canonical_syllables = alias->second;
|
||
}
|
||
else if (valid_pinyin.find(typed) != valid_pinyin.end())
|
||
{
|
||
canonical_syllables.push_back(typed);
|
||
}
|
||
else
|
||
{
|
||
continue;
|
||
}
|
||
|
||
const auto suffix_paths = self(self, end);
|
||
for (size_t canonical_index = 0; canonical_index < canonical_syllables.size(); ++canonical_index)
|
||
{
|
||
for (const auto &suffix : suffix_paths)
|
||
{
|
||
RankedPath candidate;
|
||
candidate.segments.reserve(1 + suffix.segments.size());
|
||
candidate.segments.push_back(canonical_syllables[canonical_index]);
|
||
candidate.segments.insert(candidate.segments.end(), suffix.segments.begin(), suffix.segments.end());
|
||
candidate.typed_lengths.reserve(1 + suffix.typed_lengths.size());
|
||
candidate.typed_lengths.push_back(end - index);
|
||
candidate.typed_lengths.insert(candidate.typed_lengths.end(), suffix.typed_lengths.begin(),
|
||
suffix.typed_lengths.end());
|
||
candidate.correction_ranks.reserve(1 + suffix.correction_ranks.size());
|
||
candidate.correction_ranks.push_back(canonical_index);
|
||
candidate.correction_ranks.insert(candidate.correction_ranks.end(), suffix.correction_ranks.begin(),
|
||
suffix.correction_ranks.end());
|
||
paths.push_back(std::move(candidate));
|
||
}
|
||
}
|
||
}
|
||
|
||
std::stable_sort(paths.begin(), paths.end(), [](const RankedPath &lhs, const RankedPath &rhs) {
|
||
if (lhs.segments.size() != rhs.segments.size())
|
||
{
|
||
return lhs.segments.size() < rhs.segments.size();
|
||
}
|
||
if (lhs.typed_lengths != rhs.typed_lengths)
|
||
{
|
||
return std::lexicographical_compare(lhs.typed_lengths.begin(), lhs.typed_lengths.end(),
|
||
rhs.typed_lengths.begin(), rhs.typed_lengths.end());
|
||
}
|
||
return std::lexicographical_compare(lhs.correction_ranks.begin(), lhs.correction_ranks.end(),
|
||
rhs.correction_ranks.begin(), rhs.correction_ranks.end());
|
||
});
|
||
paths.erase(
|
||
std::unique(paths.begin(), paths.end(),
|
||
[](const RankedPath &lhs, const RankedPath &rhs) { return lhs.segments == rhs.segments; }),
|
||
paths.end());
|
||
if (!paths.empty())
|
||
{
|
||
const size_t minimum_segments = paths.front().segments.size();
|
||
paths.erase(std::find_if(paths.begin(), paths.end(),
|
||
[&](const RankedPath &path) { return path.segments.size() != minimum_segments; }),
|
||
paths.end());
|
||
}
|
||
if (paths.size() > kCorrectionPathLimit)
|
||
{
|
||
paths.resize(kCorrectionPathLimit);
|
||
}
|
||
memo.emplace(index, paths);
|
||
return paths;
|
||
};
|
||
|
||
const auto ranked_paths = solve(solve, 0);
|
||
std::vector<Segments> paths;
|
||
paths.reserve(ranked_paths.size());
|
||
for (const auto &path : ranked_paths)
|
||
{
|
||
paths.push_back(path.segments);
|
||
}
|
||
return paths;
|
||
}
|
||
|
||
std::vector<Segments> cut_pinyin_with_corrections(const std::string &pinyin, bool include_correction_aliases)
|
||
{
|
||
std::vector<Segments> merged_paths = {Segments{}};
|
||
for (const auto &part : split(pinyin, '\''))
|
||
{
|
||
const auto part_paths = cut_one_piece_with_corrections(part, include_correction_aliases);
|
||
if (part_paths.empty())
|
||
{
|
||
return {};
|
||
}
|
||
|
||
std::vector<Segments> combined;
|
||
for (const auto &merged : merged_paths)
|
||
{
|
||
for (const auto &part_path : part_paths)
|
||
{
|
||
Segments candidate = merged;
|
||
candidate.insert(candidate.end(), part_path.begin(), part_path.end());
|
||
combined.push_back(std::move(candidate));
|
||
if (combined.size() == kCorrectionPathLimit)
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
if (combined.size() == kCorrectionPathLimit)
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
merged_paths = std::move(combined);
|
||
}
|
||
return merged_paths;
|
||
}
|
||
|
||
std::string build_table_name_impl(const Segments &segments)
|
||
{
|
||
if (segments.empty() || segments.front().empty())
|
||
{
|
||
return "";
|
||
}
|
||
return metasequoia::dictionary_format::quanpin_table(segments.size(), segments.front().front());
|
||
}
|
||
|
||
std::string segments_to_jianpin_impl(const Segments &segments)
|
||
{
|
||
std::string jp;
|
||
for (const auto &segment : segments)
|
||
{
|
||
if (!segment.empty())
|
||
{
|
||
jp.push_back(segment.front());
|
||
}
|
||
}
|
||
return jp;
|
||
}
|
||
|
||
std::string build_key_like_pattern(const Segments &segments)
|
||
{
|
||
Segments parts;
|
||
for (size_t i = 0; i < segments.size(); ++i)
|
||
{
|
||
const bool is_last = (i + 1 == segments.size());
|
||
if (is_last || segments[i].size() == 1)
|
||
{
|
||
parts.push_back(segments[i] + "%");
|
||
}
|
||
else
|
||
{
|
||
parts.push_back(segments[i]);
|
||
}
|
||
}
|
||
return join_segments(parts);
|
||
}
|
||
|
||
std::string build_key_prefix_upper_bound(const std::string &prefix)
|
||
{
|
||
return prefix + "{";
|
||
}
|
||
|
||
bool is_pure_jianpin(const Segments &segments)
|
||
{
|
||
return std::all_of(segments.begin(), segments.end(),
|
||
[](const std::string &segment) { return segment.size() == 1; });
|
||
}
|
||
|
||
bool is_shuangpin_initial_token(const std::string &segment)
|
||
{
|
||
return segment == "zh" || segment == "ch" || segment == "sh";
|
||
}
|
||
|
||
bool needs_mixed_jianpin_query(const Segments &segments, QuerySource source)
|
||
{
|
||
if (segments.size() <= 1)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return std::any_of(segments.begin(), segments.end() - 1, [source](const std::string &segment) {
|
||
if (segment.size() == 1)
|
||
{
|
||
return true;
|
||
}
|
||
return source == QuerySource::Shuangpin && is_shuangpin_initial_token(segment);
|
||
});
|
||
}
|
||
|
||
std::string extract_initial_token(const std::string &segment)
|
||
{
|
||
if (segment.size() >= 2)
|
||
{
|
||
const auto prefix = segment.substr(0, 2);
|
||
if (prefix == "zh" || prefix == "ch" || prefix == "sh")
|
||
{
|
||
return prefix;
|
||
}
|
||
}
|
||
return segment.empty() ? "" : segment.substr(0, 1);
|
||
}
|
||
|
||
bool matches_mixed_segments(const std::string &key, const Segments &segments, QuerySource source)
|
||
{
|
||
const auto key_segments = split(key, '\'');
|
||
if (key_segments.size() != segments.size())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
for (size_t i = 0; i < segments.size(); ++i)
|
||
{
|
||
const auto &expected = segments[i];
|
||
const auto &actual = key_segments[i];
|
||
|
||
if (expected.empty() || actual.empty())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const bool is_strict_shuangpin_initial =
|
||
source == QuerySource::Shuangpin && is_shuangpin_initial_token(expected);
|
||
if (expected.size() == 1 || is_strict_shuangpin_initial)
|
||
{
|
||
if (source == QuerySource::Shuangpin)
|
||
{
|
||
if (extract_initial_token(actual) != expected)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
else if (actual.front() != expected.front())
|
||
{
|
||
return false;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (actual != expected)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
int build_mixed_jianpin_scan_limit(int limit)
|
||
{
|
||
if (limit > INT_MAX / 16)
|
||
{
|
||
return INT_MAX;
|
||
}
|
||
return std::max(limit * 16, 128);
|
||
}
|
||
|
||
bool can_match_exact_key(const Segments &segments)
|
||
{
|
||
if (segments.empty())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const auto &valid_pinyin = intact_pinyin_set();
|
||
return std::all_of(segments.begin(), segments.end(),
|
||
[&](const std::string &segment) { return valid_pinyin.find(segment) != valid_pinyin.end(); });
|
||
}
|
||
|
||
class SqliteDb
|
||
{
|
||
public:
|
||
explicit SqliteDb(const std::string &db_path)
|
||
{
|
||
if (sqlite3_open(db_path.c_str(), &db_) != SQLITE_OK)
|
||
{
|
||
const std::string message = db_ != nullptr ? sqlite3_errmsg(db_) : "sqlite open failed";
|
||
if (db_ != nullptr)
|
||
{
|
||
sqlite3_close(db_);
|
||
db_ = nullptr;
|
||
}
|
||
throw std::runtime_error(message);
|
||
}
|
||
// See QuanpinDictionary's constructor: a query must wait out a commit, not fail.
|
||
sqlite3_busy_timeout(db_, kDictionaryBusyTimeoutMs);
|
||
}
|
||
|
||
~SqliteDb()
|
||
{
|
||
if (db_ != nullptr)
|
||
{
|
||
sqlite3_close(db_);
|
||
}
|
||
}
|
||
|
||
sqlite3 *get() const
|
||
{
|
||
return db_;
|
||
}
|
||
|
||
private:
|
||
sqlite3 *db_ = nullptr;
|
||
};
|
||
|
||
std::vector<QueryItem> run_query(sqlite3 *db, const std::string &sql, const std::string &value, int limit)
|
||
{
|
||
sqlite3_stmt *stmt = nullptr;
|
||
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
||
{
|
||
return {};
|
||
}
|
||
|
||
sqlite3_bind_text(stmt, 1, value.c_str(), -1, SQLITE_TRANSIENT);
|
||
sqlite3_bind_int(stmt, 2, limit);
|
||
|
||
std::vector<QueryItem> rows;
|
||
while (sqlite3_step(stmt) == SQLITE_ROW)
|
||
{
|
||
const unsigned char *text = sqlite3_column_text(stmt, 0);
|
||
const std::int64_t weight = sqlite3_column_int64(stmt, 1);
|
||
rows.emplace_back(text == nullptr ? "" : reinterpret_cast<const char *>(text), weight);
|
||
}
|
||
sqlite3_finalize(stmt);
|
||
return rows;
|
||
}
|
||
|
||
std::vector<QueryItem> run_query(sqlite3 *db, std::unordered_map<std::string, sqlite3_stmt *> &statement_cache,
|
||
const std::string &sql, const std::string &value, int limit)
|
||
{
|
||
sqlite3_stmt *stmt = nullptr;
|
||
const auto found = statement_cache.find(sql);
|
||
if (found != statement_cache.end())
|
||
{
|
||
stmt = found->second;
|
||
sqlite3_reset(stmt);
|
||
sqlite3_clear_bindings(stmt);
|
||
}
|
||
else
|
||
{
|
||
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
||
{
|
||
return {};
|
||
}
|
||
statement_cache.emplace(sql, stmt);
|
||
}
|
||
|
||
sqlite3_bind_text(stmt, 1, value.c_str(), -1, SQLITE_TRANSIENT);
|
||
sqlite3_bind_int(stmt, 2, limit);
|
||
|
||
std::vector<QueryItem> rows;
|
||
while (sqlite3_step(stmt) == SQLITE_ROW)
|
||
{
|
||
const unsigned char *text = sqlite3_column_text(stmt, 0);
|
||
const std::int64_t weight = sqlite3_column_int64(stmt, 1);
|
||
rows.emplace_back(text == nullptr ? "" : reinterpret_cast<const char *>(text), weight);
|
||
}
|
||
sqlite3_reset(stmt);
|
||
sqlite3_clear_bindings(stmt);
|
||
return rows;
|
||
}
|
||
|
||
std::vector<QueryItem> run_query(sqlite3 *db, const std::string &sql, const std::string &lower_bound,
|
||
const std::string &upper_bound, int limit)
|
||
{
|
||
sqlite3_stmt *stmt = nullptr;
|
||
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
||
{
|
||
return {};
|
||
}
|
||
|
||
sqlite3_bind_text(stmt, 1, lower_bound.c_str(), -1, SQLITE_TRANSIENT);
|
||
sqlite3_bind_text(stmt, 2, upper_bound.c_str(), -1, SQLITE_TRANSIENT);
|
||
sqlite3_bind_int(stmt, 3, limit);
|
||
|
||
std::vector<QueryItem> rows;
|
||
while (sqlite3_step(stmt) == SQLITE_ROW)
|
||
{
|
||
const unsigned char *text = sqlite3_column_text(stmt, 0);
|
||
const std::int64_t weight = sqlite3_column_int64(stmt, 1);
|
||
rows.emplace_back(text == nullptr ? "" : reinterpret_cast<const char *>(text), weight);
|
||
}
|
||
sqlite3_finalize(stmt);
|
||
return rows;
|
||
}
|
||
|
||
std::vector<QueryItem> run_query(sqlite3 *db, std::unordered_map<std::string, sqlite3_stmt *> &statement_cache,
|
||
const std::string &sql, const std::string &lower_bound, const std::string &upper_bound,
|
||
int limit)
|
||
{
|
||
sqlite3_stmt *stmt = nullptr;
|
||
const auto found = statement_cache.find(sql);
|
||
if (found != statement_cache.end())
|
||
{
|
||
stmt = found->second;
|
||
sqlite3_reset(stmt);
|
||
sqlite3_clear_bindings(stmt);
|
||
}
|
||
else
|
||
{
|
||
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
||
{
|
||
return {};
|
||
}
|
||
statement_cache.emplace(sql, stmt);
|
||
}
|
||
|
||
sqlite3_bind_text(stmt, 1, lower_bound.c_str(), -1, SQLITE_TRANSIENT);
|
||
sqlite3_bind_text(stmt, 2, upper_bound.c_str(), -1, SQLITE_TRANSIENT);
|
||
sqlite3_bind_int(stmt, 3, limit);
|
||
|
||
std::vector<QueryItem> rows;
|
||
while (sqlite3_step(stmt) == SQLITE_ROW)
|
||
{
|
||
const unsigned char *text = sqlite3_column_text(stmt, 0);
|
||
const std::int64_t weight = sqlite3_column_int64(stmt, 1);
|
||
rows.emplace_back(text == nullptr ? "" : reinterpret_cast<const char *>(text), weight);
|
||
}
|
||
sqlite3_reset(stmt);
|
||
sqlite3_clear_bindings(stmt);
|
||
return rows;
|
||
}
|
||
|
||
std::vector<KeyedQueryItem> run_keyed_query(sqlite3 *db, const std::string &sql, const std::string &value, int limit)
|
||
{
|
||
sqlite3_stmt *stmt = nullptr;
|
||
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
||
{
|
||
return {};
|
||
}
|
||
|
||
sqlite3_bind_text(stmt, 1, value.c_str(), -1, SQLITE_TRANSIENT);
|
||
sqlite3_bind_int(stmt, 2, limit);
|
||
|
||
std::vector<KeyedQueryItem> rows;
|
||
while (sqlite3_step(stmt) == SQLITE_ROW)
|
||
{
|
||
const unsigned char *key = sqlite3_column_text(stmt, 0);
|
||
const unsigned char *value_text = sqlite3_column_text(stmt, 1);
|
||
const std::int64_t weight = sqlite3_column_int64(stmt, 2);
|
||
rows.push_back(KeyedQueryItem{
|
||
key == nullptr ? "" : reinterpret_cast<const char *>(key),
|
||
value_text == nullptr ? "" : reinterpret_cast<const char *>(value_text),
|
||
weight,
|
||
});
|
||
}
|
||
sqlite3_finalize(stmt);
|
||
return rows;
|
||
}
|
||
|
||
std::vector<KeyedQueryItem> run_keyed_query(sqlite3 *db,
|
||
std::unordered_map<std::string, sqlite3_stmt *> &statement_cache,
|
||
const std::string &sql, const std::string &value, int limit)
|
||
{
|
||
sqlite3_stmt *stmt = nullptr;
|
||
const auto found = statement_cache.find(sql);
|
||
if (found != statement_cache.end())
|
||
{
|
||
stmt = found->second;
|
||
sqlite3_reset(stmt);
|
||
sqlite3_clear_bindings(stmt);
|
||
}
|
||
else
|
||
{
|
||
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
||
{
|
||
return {};
|
||
}
|
||
statement_cache.emplace(sql, stmt);
|
||
}
|
||
|
||
sqlite3_bind_text(stmt, 1, value.c_str(), -1, SQLITE_TRANSIENT);
|
||
sqlite3_bind_int(stmt, 2, limit);
|
||
|
||
std::vector<KeyedQueryItem> rows;
|
||
while (sqlite3_step(stmt) == SQLITE_ROW)
|
||
{
|
||
const unsigned char *key = sqlite3_column_text(stmt, 0);
|
||
const unsigned char *value_text = sqlite3_column_text(stmt, 1);
|
||
const std::int64_t weight = sqlite3_column_int64(stmt, 2);
|
||
rows.push_back(KeyedQueryItem{
|
||
key == nullptr ? "" : reinterpret_cast<const char *>(key),
|
||
value_text == nullptr ? "" : reinterpret_cast<const char *>(value_text),
|
||
weight,
|
||
});
|
||
}
|
||
sqlite3_reset(stmt);
|
||
sqlite3_clear_bindings(stmt);
|
||
return rows;
|
||
}
|
||
|
||
std::vector<KeyedQueryItem> run_keyed_query(sqlite3 *db, const std::string &sql, const std::string &lower_bound,
|
||
const std::string &upper_bound, int limit)
|
||
{
|
||
sqlite3_stmt *stmt = nullptr;
|
||
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
||
{
|
||
return {};
|
||
}
|
||
|
||
sqlite3_bind_text(stmt, 1, lower_bound.c_str(), -1, SQLITE_TRANSIENT);
|
||
sqlite3_bind_text(stmt, 2, upper_bound.c_str(), -1, SQLITE_TRANSIENT);
|
||
sqlite3_bind_int(stmt, 3, limit);
|
||
|
||
std::vector<KeyedQueryItem> rows;
|
||
while (sqlite3_step(stmt) == SQLITE_ROW)
|
||
{
|
||
const unsigned char *key = sqlite3_column_text(stmt, 0);
|
||
const unsigned char *value_text = sqlite3_column_text(stmt, 1);
|
||
rows.push_back(KeyedQueryItem{
|
||
key == nullptr ? "" : reinterpret_cast<const char *>(key),
|
||
value_text == nullptr ? "" : reinterpret_cast<const char *>(value_text),
|
||
sqlite3_column_int64(stmt, 2),
|
||
});
|
||
}
|
||
sqlite3_finalize(stmt);
|
||
return rows;
|
||
}
|
||
|
||
std::vector<KeyedQueryItem> run_keyed_query(sqlite3 *db,
|
||
std::unordered_map<std::string, sqlite3_stmt *> &statement_cache,
|
||
const std::string &sql, const std::string &lower_bound,
|
||
const std::string &upper_bound, int limit)
|
||
{
|
||
sqlite3_stmt *stmt = nullptr;
|
||
const auto found = statement_cache.find(sql);
|
||
if (found != statement_cache.end())
|
||
{
|
||
stmt = found->second;
|
||
sqlite3_reset(stmt);
|
||
sqlite3_clear_bindings(stmt);
|
||
}
|
||
else
|
||
{
|
||
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
||
{
|
||
return {};
|
||
}
|
||
statement_cache.emplace(sql, stmt);
|
||
}
|
||
|
||
sqlite3_bind_text(stmt, 1, lower_bound.c_str(), -1, SQLITE_TRANSIENT);
|
||
sqlite3_bind_text(stmt, 2, upper_bound.c_str(), -1, SQLITE_TRANSIENT);
|
||
sqlite3_bind_int(stmt, 3, limit);
|
||
|
||
std::vector<KeyedQueryItem> rows;
|
||
while (sqlite3_step(stmt) == SQLITE_ROW)
|
||
{
|
||
const unsigned char *key = sqlite3_column_text(stmt, 0);
|
||
const unsigned char *value_text = sqlite3_column_text(stmt, 1);
|
||
rows.push_back(KeyedQueryItem{
|
||
key == nullptr ? "" : reinterpret_cast<const char *>(key),
|
||
value_text == nullptr ? "" : reinterpret_cast<const char *>(value_text),
|
||
sqlite3_column_int64(stmt, 2),
|
||
});
|
||
}
|
||
sqlite3_reset(stmt);
|
||
sqlite3_clear_bindings(stmt);
|
||
return rows;
|
||
}
|
||
|
||
std::vector<KeyedQueryItem> run_keyed_batch_query(sqlite3 *db,
|
||
std::unordered_map<std::string, sqlite3_stmt *> &statement_cache,
|
||
const std::string &table, const std::vector<std::string> &keys,
|
||
int limit)
|
||
{
|
||
if (db == nullptr || table.empty() || keys.empty() || limit <= 0)
|
||
{
|
||
return {};
|
||
}
|
||
|
||
std::string placeholders;
|
||
for (size_t index = 0; index < keys.size(); ++index)
|
||
{
|
||
if (index > 0)
|
||
{
|
||
placeholders += ',';
|
||
}
|
||
placeholders += '?';
|
||
}
|
||
const std::string sql = "SELECT \"key\", \"value\", \"weight\" FROM \"" + table + "\" WHERE \"key\" IN (" +
|
||
placeholders + ") ORDER BY \"weight\" DESC LIMIT ?";
|
||
|
||
sqlite3_stmt *stmt = nullptr;
|
||
const auto found = statement_cache.find(sql);
|
||
if (found != statement_cache.end())
|
||
{
|
||
stmt = found->second;
|
||
sqlite3_reset(stmt);
|
||
sqlite3_clear_bindings(stmt);
|
||
}
|
||
else
|
||
{
|
||
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
||
{
|
||
return {};
|
||
}
|
||
statement_cache.emplace(sql, stmt);
|
||
}
|
||
|
||
for (size_t index = 0; index < keys.size(); ++index)
|
||
{
|
||
sqlite3_bind_text(stmt, static_cast<int>(index + 1), keys[index].c_str(), -1, SQLITE_TRANSIENT);
|
||
}
|
||
sqlite3_bind_int(stmt, static_cast<int>(keys.size() + 1), limit);
|
||
|
||
std::vector<KeyedQueryItem> rows;
|
||
while (sqlite3_step(stmt) == SQLITE_ROW)
|
||
{
|
||
const unsigned char *key = sqlite3_column_text(stmt, 0);
|
||
const unsigned char *value_text = sqlite3_column_text(stmt, 1);
|
||
rows.push_back(KeyedQueryItem{
|
||
key == nullptr ? "" : reinterpret_cast<const char *>(key),
|
||
value_text == nullptr ? "" : reinterpret_cast<const char *>(value_text),
|
||
sqlite3_column_int64(stmt, 2),
|
||
});
|
||
}
|
||
sqlite3_reset(stmt);
|
||
sqlite3_clear_bindings(stmt);
|
||
return rows;
|
||
}
|
||
|
||
std::vector<KeyedQueryItem> filter_mixed_jianpin_rows(const std::vector<KeyedQueryItem> &rows, const Segments &segments,
|
||
int limit, QuerySource source)
|
||
{
|
||
std::vector<KeyedQueryItem> matched;
|
||
matched.reserve(static_cast<size_t>(std::min(limit, static_cast<int>(rows.size()))));
|
||
for (const auto &row : rows)
|
||
{
|
||
if (!matches_mixed_segments(row.key, segments, source))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
matched.push_back(row);
|
||
if (static_cast<int>(matched.size()) >= limit)
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
return matched;
|
||
}
|
||
|
||
std::vector<QueryItem> without_keys(const std::vector<KeyedQueryItem> &rows)
|
||
{
|
||
std::vector<QueryItem> result;
|
||
result.reserve(rows.size());
|
||
for (const auto &row : rows)
|
||
{
|
||
result.emplace_back(row.value, row.weight);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
void deduplicate_keyed_items_by_value(std::vector<KeyedQueryItem> &items)
|
||
{
|
||
std::unordered_set<std::string> seen;
|
||
items.erase(std::remove_if(items.begin(), items.end(),
|
||
[&](const KeyedQueryItem &item) { return !seen.insert(item.value).second; }),
|
||
items.end());
|
||
}
|
||
|
||
std::vector<KeyedQueryItem> query_single_cut_keyed(sqlite3 *db, const Segments &segments, int limit, QuerySource source)
|
||
{
|
||
const auto table = build_table_name_impl(segments);
|
||
if (table.empty())
|
||
{
|
||
return {};
|
||
}
|
||
|
||
const auto key = join_segments(segments);
|
||
const auto jp = segments_to_jianpin_impl(segments);
|
||
const auto mixed_query_limit = build_mixed_jianpin_scan_limit(limit);
|
||
const auto needs_mixed_query = needs_mixed_jianpin_query(segments, source);
|
||
const auto key_prefix_pattern = build_key_like_pattern(segments);
|
||
const auto key_prefix = key_prefix_pattern.substr(0, key_prefix_pattern.size() - 1);
|
||
const auto key_prefix_upper_bound = build_key_prefix_upper_bound(key_prefix);
|
||
|
||
const auto exact_sql = "SELECT \"key\", \"value\", \"weight\" FROM \"" + table +
|
||
"\" WHERE \"key\" = ? ORDER BY \"weight\" DESC LIMIT ?";
|
||
std::vector<KeyedQueryItem> rows;
|
||
if (can_match_exact_key(segments))
|
||
{
|
||
rows = run_keyed_query(db, exact_sql, key, limit);
|
||
if (!rows.empty())
|
||
{
|
||
return rows;
|
||
}
|
||
}
|
||
|
||
const auto prefix_sql = "SELECT \"key\", \"value\", \"weight\" FROM \"" + table +
|
||
"\" WHERE \"key\" >= ? AND \"key\" < ? ORDER BY \"weight\" DESC LIMIT ?";
|
||
rows = run_keyed_query(db, prefix_sql, key_prefix, key_prefix_upper_bound, limit);
|
||
if (!rows.empty())
|
||
{
|
||
return rows;
|
||
}
|
||
|
||
if (needs_mixed_query)
|
||
{
|
||
const auto mixed_sql = "SELECT \"key\", \"value\", \"weight\" FROM \"" + table +
|
||
"\" WHERE \"jp\" = ? ORDER BY \"weight\" DESC LIMIT ?";
|
||
rows =
|
||
filter_mixed_jianpin_rows(run_keyed_query(db, mixed_sql, jp, mixed_query_limit), segments, limit, source);
|
||
if (!rows.empty())
|
||
{
|
||
return rows;
|
||
}
|
||
}
|
||
|
||
if (!is_pure_jianpin(segments))
|
||
{
|
||
return {};
|
||
}
|
||
|
||
const auto jp_sql = "SELECT \"key\", \"value\", \"weight\" FROM \"" + table +
|
||
"\" WHERE \"jp\" = ? ORDER BY \"weight\" DESC LIMIT ?";
|
||
return run_keyed_query(db, jp_sql, jp, limit);
|
||
}
|
||
|
||
std::vector<KeyedQueryItem> query_single_cut_keyed(sqlite3 *db,
|
||
std::unordered_map<std::string, sqlite3_stmt *> &statement_cache,
|
||
const Segments &segments, int limit, QuerySource source)
|
||
{
|
||
const auto table = build_table_name_impl(segments);
|
||
if (table.empty())
|
||
{
|
||
return {};
|
||
}
|
||
|
||
const auto key = join_segments(segments);
|
||
const auto jp = segments_to_jianpin_impl(segments);
|
||
const auto mixed_query_limit = build_mixed_jianpin_scan_limit(limit);
|
||
const auto needs_mixed_query = needs_mixed_jianpin_query(segments, source);
|
||
const auto key_prefix_pattern = build_key_like_pattern(segments);
|
||
const auto key_prefix = key_prefix_pattern.substr(0, key_prefix_pattern.size() - 1);
|
||
const auto key_prefix_upper_bound = build_key_prefix_upper_bound(key_prefix);
|
||
|
||
const auto exact_sql = "SELECT \"key\", \"value\", \"weight\" FROM \"" + table +
|
||
"\" WHERE \"key\" = ? ORDER BY \"weight\" DESC LIMIT ?";
|
||
std::vector<KeyedQueryItem> rows;
|
||
if (can_match_exact_key(segments))
|
||
{
|
||
rows = run_keyed_query(db, statement_cache, exact_sql, key, limit);
|
||
if (!rows.empty())
|
||
{
|
||
return rows;
|
||
}
|
||
}
|
||
|
||
const auto prefix_sql = "SELECT \"key\", \"value\", \"weight\" FROM \"" + table +
|
||
"\" WHERE \"key\" >= ? AND \"key\" < ? ORDER BY \"weight\" DESC LIMIT ?";
|
||
rows = run_keyed_query(db, statement_cache, prefix_sql, key_prefix, key_prefix_upper_bound, limit);
|
||
if (!rows.empty())
|
||
{
|
||
return rows;
|
||
}
|
||
|
||
if (needs_mixed_query)
|
||
{
|
||
const auto mixed_sql = "SELECT \"key\", \"value\", \"weight\" FROM \"" + table +
|
||
"\" WHERE \"jp\" = ? ORDER BY \"weight\" DESC LIMIT ?";
|
||
rows = filter_mixed_jianpin_rows(run_keyed_query(db, statement_cache, mixed_sql, jp, mixed_query_limit),
|
||
segments, limit, source);
|
||
if (!rows.empty())
|
||
{
|
||
return rows;
|
||
}
|
||
}
|
||
|
||
if (!is_pure_jianpin(segments))
|
||
{
|
||
return {};
|
||
}
|
||
|
||
const auto jp_sql = "SELECT \"key\", \"value\", \"weight\" FROM \"" + table +
|
||
"\" WHERE \"jp\" = ? ORDER BY \"weight\" DESC LIMIT ?";
|
||
return run_keyed_query(db, statement_cache, jp_sql, jp, limit);
|
||
}
|
||
|
||
std::vector<QueryItem> query_single_cut(sqlite3 *db, const Segments &segments, int limit, QuerySource source)
|
||
{
|
||
return without_keys(query_single_cut_keyed(db, segments, limit, source));
|
||
}
|
||
|
||
std::vector<QueryItem> query_single_cut(sqlite3 *db, std::unordered_map<std::string, sqlite3_stmt *> &statement_cache,
|
||
const Segments &segments, int limit, QuerySource source)
|
||
{
|
||
return without_keys(query_single_cut_keyed(db, statement_cache, segments, limit, source));
|
||
}
|
||
|
||
} // namespace
|
||
|
||
Segments cut_pinyin_greedy(const std::string &pinyin, bool intact_only)
|
||
{
|
||
if (pinyin.empty())
|
||
{
|
||
return {};
|
||
}
|
||
|
||
if (pinyin.find('\'') == std::string::npos)
|
||
{
|
||
return cut_one_piece_min_segments(pinyin, intact_only);
|
||
}
|
||
|
||
Segments merged;
|
||
for (const auto &part : split(pinyin, '\''))
|
||
{
|
||
auto cut = cut_one_piece_min_segments(part, intact_only);
|
||
if (cut.empty())
|
||
{
|
||
if (intact_only && !part.empty())
|
||
{
|
||
return {};
|
||
}
|
||
if (!part.empty())
|
||
{
|
||
merged.push_back(part);
|
||
}
|
||
continue;
|
||
}
|
||
merged.insert(merged.end(), cut.begin(), cut.end());
|
||
}
|
||
return merged;
|
||
}
|
||
|
||
std::vector<Segments> cut_pinyin_by_mode(const std::string &pinyin, const std::string &mode,
|
||
bool include_correction_aliases)
|
||
{
|
||
if (mode != "greedy" && mode != "correction")
|
||
{
|
||
throw std::invalid_argument("mode must be one of: greedy, correction");
|
||
}
|
||
|
||
if (mode == "greedy")
|
||
{
|
||
const auto greedy = cut_pinyin_greedy(pinyin, false);
|
||
if (!greedy.empty())
|
||
{
|
||
return {greedy};
|
||
}
|
||
return {};
|
||
}
|
||
|
||
const auto corrected_paths = cut_pinyin_with_corrections(pinyin, include_correction_aliases);
|
||
if (!corrected_paths.empty())
|
||
{
|
||
return corrected_paths;
|
||
}
|
||
|
||
const auto greedy = cut_pinyin_greedy(pinyin, false);
|
||
if (!greedy.empty())
|
||
{
|
||
return {greedy};
|
||
}
|
||
|
||
return {};
|
||
}
|
||
|
||
Segments split_segments(const std::string &segmentation)
|
||
{
|
||
if (segmentation.empty())
|
||
{
|
||
return {};
|
||
}
|
||
|
||
return split(segmentation, '\'');
|
||
}
|
||
|
||
std::string join_segments(const Segments &segments, const std::string &delimiter)
|
||
{
|
||
std::string joined;
|
||
for (size_t i = 0; i < segments.size(); ++i)
|
||
{
|
||
if (i > 0)
|
||
{
|
||
joined += delimiter;
|
||
}
|
||
joined += segments[i];
|
||
}
|
||
return joined;
|
||
}
|
||
|
||
std::string build_table_name(const Segments &segments)
|
||
{
|
||
return build_table_name_impl(segments);
|
||
}
|
||
|
||
std::string segments_to_jianpin(const Segments &segments)
|
||
{
|
||
return segments_to_jianpin_impl(segments);
|
||
}
|
||
|
||
std::string get_default_db_path()
|
||
{
|
||
return metasequoia::path_to_utf8(shuangpin::get_data_file_path(metasequoia::assets::main_dictionary));
|
||
}
|
||
|
||
void warm_up(sqlite3 *db, std::unordered_map<std::string, sqlite3_stmt *> &statement_cache)
|
||
{
|
||
(void)intact_pinyin_set();
|
||
(void)prefix_pinyin_set();
|
||
|
||
if (db == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Warm the most common single-syllable prefixes to hide first-query setup costs.
|
||
(void)query_single_cut(db, statement_cache, Segments{"n"}, 1, QuerySource::Quanpin);
|
||
(void)query_single_cut(db, statement_cache, Segments{"ni"}, 1, QuerySource::Quanpin);
|
||
}
|
||
|
||
std::vector<KeyedQueryItem> query_initial(sqlite3 *db, const std::string &prefix, int limit)
|
||
{
|
||
if (db == nullptr || prefix.empty() || prefix.front() < 'a' || prefix.front() > 'z' || limit <= 0)
|
||
{
|
||
return {};
|
||
}
|
||
|
||
const std::string table = "tbl_1_" + std::string(1, prefix.front());
|
||
const std::string sql = "SELECT \"key\", \"value\", \"weight\" FROM \"" + table +
|
||
"\" WHERE \"key\" >= ?1 AND \"key\" < ?2 ORDER BY \"weight\" DESC LIMIT ?3";
|
||
sqlite3_stmt *stmt = nullptr;
|
||
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
||
{
|
||
return {};
|
||
}
|
||
|
||
const std::string upper_bound = prefix + "{";
|
||
sqlite3_bind_text(stmt, 1, prefix.c_str(), -1, SQLITE_TRANSIENT);
|
||
sqlite3_bind_text(stmt, 2, upper_bound.c_str(), -1, SQLITE_TRANSIENT);
|
||
sqlite3_bind_int(stmt, 3, limit);
|
||
|
||
std::vector<KeyedQueryItem> rows;
|
||
while (sqlite3_step(stmt) == SQLITE_ROW)
|
||
{
|
||
const auto *key = reinterpret_cast<const char *>(sqlite3_column_text(stmt, 0));
|
||
const auto *value = reinterpret_cast<const char *>(sqlite3_column_text(stmt, 1));
|
||
rows.push_back(
|
||
KeyedQueryItem{key == nullptr ? "" : key, value == nullptr ? "" : value, sqlite3_column_int64(stmt, 2)});
|
||
}
|
||
sqlite3_finalize(stmt);
|
||
return rows;
|
||
}
|
||
|
||
QueryResult query_words(const std::string &pinyin, const std::string &db_path, const std::string &mode, int limit)
|
||
{
|
||
// 离线查询工具,不挂在任何会话的纠错开关上。
|
||
const auto cuts = cut_pinyin_by_mode(pinyin, mode, true);
|
||
QueryResult result{pinyin, mode, {}};
|
||
if (cuts.empty())
|
||
{
|
||
return result;
|
||
}
|
||
|
||
SqliteDb db(db_path);
|
||
for (const auto &segments : cuts)
|
||
{
|
||
result.results.push_back(QueryResultEntry{
|
||
segments,
|
||
join_segments(segments),
|
||
build_table_name(segments),
|
||
query_single_cut(db.get(), segments, limit, QuerySource::Quanpin),
|
||
});
|
||
}
|
||
return result;
|
||
}
|
||
|
||
QueryResult query_segments(const Segments &segments, const std::string &db_path, int limit, QuerySource source)
|
||
{
|
||
QueryResult result{join_segments(segments), "precut", {}};
|
||
if (segments.empty())
|
||
{
|
||
return result;
|
||
}
|
||
|
||
SqliteDb db(db_path);
|
||
result.results.push_back(QueryResultEntry{
|
||
segments,
|
||
join_segments(segments),
|
||
build_table_name(segments),
|
||
query_single_cut(db.get(), segments, limit, source),
|
||
});
|
||
return result;
|
||
}
|
||
|
||
std::vector<QueryItem> query_words_flat(const std::string &pinyin, const std::string &db_path, const std::string &mode,
|
||
int limit)
|
||
{
|
||
const auto result = query_words(pinyin, db_path, mode, limit);
|
||
std::vector<QueryItem> items;
|
||
std::unordered_set<std::string> seen;
|
||
for (const auto &entry : result.results)
|
||
{
|
||
for (const auto &item : entry.items)
|
||
{
|
||
if (seen.insert(item.first).second)
|
||
{
|
||
items.push_back(item);
|
||
}
|
||
}
|
||
}
|
||
|
||
std::sort(items.begin(), items.end(),
|
||
[](const QueryItem &lhs, const QueryItem &rhs) { return lhs.second > rhs.second; });
|
||
|
||
if (static_cast<int>(items.size()) > limit)
|
||
{
|
||
items.resize(static_cast<size_t>(limit));
|
||
}
|
||
return items;
|
||
}
|
||
|
||
std::vector<QueryItem> query_segments_flat(const Segments &segments, const std::string &db_path, int limit,
|
||
QuerySource source)
|
||
{
|
||
const auto result = query_segments(segments, db_path, limit, source);
|
||
std::vector<QueryItem> items;
|
||
std::unordered_set<std::string> seen;
|
||
for (const auto &entry : result.results)
|
||
{
|
||
for (const auto &item : entry.items)
|
||
{
|
||
if (seen.insert(item.first).second)
|
||
{
|
||
items.push_back(item);
|
||
}
|
||
}
|
||
}
|
||
|
||
std::sort(items.begin(), items.end(),
|
||
[](const QueryItem &lhs, const QueryItem &rhs) { return lhs.second > rhs.second; });
|
||
|
||
if (static_cast<int>(items.size()) > limit)
|
||
{
|
||
items.resize(static_cast<size_t>(limit));
|
||
}
|
||
return items;
|
||
}
|
||
|
||
std::vector<QueryItem> query_segments_flat(const Segments &segments, sqlite3 *db,
|
||
std::unordered_map<std::string, sqlite3_stmt *> &statement_cache, int limit,
|
||
QuerySource source)
|
||
{
|
||
if (db == nullptr || segments.empty())
|
||
{
|
||
return {};
|
||
}
|
||
|
||
std::vector<QueryItem> items;
|
||
std::unordered_set<std::string> seen;
|
||
for (const auto &item : query_single_cut(db, statement_cache, segments, limit, source))
|
||
{
|
||
if (seen.insert(item.first).second)
|
||
{
|
||
items.push_back(item);
|
||
}
|
||
}
|
||
|
||
std::sort(items.begin(), items.end(),
|
||
[](const QueryItem &lhs, const QueryItem &rhs) { return lhs.second > rhs.second; });
|
||
|
||
if (static_cast<int>(items.size()) > limit)
|
||
{
|
||
items.resize(static_cast<size_t>(limit));
|
||
}
|
||
return items;
|
||
}
|
||
|
||
std::vector<KeyedQueryItem> query_segments_keyed_flat(const Segments &segments, const std::string &db_path, int limit,
|
||
QuerySource source)
|
||
{
|
||
if (segments.empty())
|
||
{
|
||
return {};
|
||
}
|
||
|
||
SqliteDb db(db_path);
|
||
if (db.get() == nullptr)
|
||
{
|
||
return {};
|
||
}
|
||
|
||
auto items = query_single_cut_keyed(db.get(), segments, limit, source);
|
||
deduplicate_keyed_items_by_value(items);
|
||
std::sort(items.begin(), items.end(),
|
||
[](const KeyedQueryItem &lhs, const KeyedQueryItem &rhs) { return lhs.weight > rhs.weight; });
|
||
if (static_cast<int>(items.size()) > limit)
|
||
{
|
||
items.resize(static_cast<size_t>(limit));
|
||
}
|
||
return items;
|
||
}
|
||
|
||
std::vector<KeyedQueryItem> query_segments_keyed_flat(const Segments &segments, sqlite3 *db,
|
||
std::unordered_map<std::string, sqlite3_stmt *> &statement_cache,
|
||
int limit, QuerySource source)
|
||
{
|
||
if (db == nullptr || segments.empty())
|
||
{
|
||
return {};
|
||
}
|
||
|
||
auto items = query_single_cut_keyed(db, statement_cache, segments, limit, source);
|
||
deduplicate_keyed_items_by_value(items);
|
||
std::sort(items.begin(), items.end(),
|
||
[](const KeyedQueryItem &lhs, const KeyedQueryItem &rhs) { return lhs.weight > rhs.weight; });
|
||
if (static_cast<int>(items.size()) > limit)
|
||
{
|
||
items.resize(static_cast<size_t>(limit));
|
||
}
|
||
return items;
|
||
}
|
||
|
||
std::vector<KeyedQueryItem> query_exact_segmentations_keyed_flat(
|
||
const std::vector<Segments> &segmentations, sqlite3 *db,
|
||
std::unordered_map<std::string, sqlite3_stmt *> &statement_cache, int limit)
|
||
{
|
||
if (db == nullptr || segmentations.empty() || limit <= 0)
|
||
{
|
||
return {};
|
||
}
|
||
|
||
std::map<std::string, std::vector<std::string>> keys_by_table;
|
||
std::unordered_set<std::string> seen;
|
||
for (const auto &segments : segmentations)
|
||
{
|
||
if (!has_only_complete_pinyin_segments(segments))
|
||
{
|
||
continue;
|
||
}
|
||
const std::string table = build_table_name_impl(segments);
|
||
const std::string key = join_segments(segments);
|
||
if (table.empty() || key.empty() || !seen.insert(table + '\n' + key).second)
|
||
{
|
||
continue;
|
||
}
|
||
keys_by_table[table].push_back(key);
|
||
}
|
||
|
||
std::vector<KeyedQueryItem> result;
|
||
for (const auto &[table, keys] : keys_by_table)
|
||
{
|
||
auto rows = run_keyed_batch_query(db, statement_cache, table, keys, limit);
|
||
result.insert(result.end(), std::make_move_iterator(rows.begin()), std::make_move_iterator(rows.end()));
|
||
}
|
||
std::stable_sort(result.begin(), result.end(),
|
||
[](const KeyedQueryItem &lhs, const KeyedQueryItem &rhs) { return lhs.weight > rhs.weight; });
|
||
return result;
|
||
}
|
||
|
||
std::shared_ptr<const CharReadingTotals> load_char_reading_totals(sqlite3 *db)
|
||
{
|
||
auto totals = std::make_shared<CharReadingTotals>();
|
||
if (db == nullptr)
|
||
return totals;
|
||
|
||
std::vector<std::string> tables;
|
||
sqlite3_stmt *statement = nullptr;
|
||
if (sqlite3_prepare_v2(
|
||
db, "SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'tbl\\_1\\_%' ESCAPE '\\'", -1,
|
||
&statement, nullptr) != SQLITE_OK)
|
||
return totals;
|
||
while (sqlite3_step(statement) == SQLITE_ROW)
|
||
{
|
||
const auto *name = reinterpret_cast<const char *>(sqlite3_column_text(statement, 0));
|
||
if (name != nullptr)
|
||
tables.emplace_back(name);
|
||
}
|
||
sqlite3_finalize(statement);
|
||
|
||
// 先按字累计权重和读音数,再只留多音字:单读音的字占比恒为 1,存着只占内存。
|
||
std::unordered_map<std::string, std::pair<std::int64_t, int>> sums;
|
||
for (const auto &table : tables)
|
||
{
|
||
const std::string sql = "SELECT value, weight FROM " + table;
|
||
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &statement, nullptr) != SQLITE_OK)
|
||
continue;
|
||
while (sqlite3_step(statement) == SQLITE_ROW)
|
||
{
|
||
const auto *value = reinterpret_cast<const char *>(sqlite3_column_text(statement, 0));
|
||
if (value == nullptr)
|
||
continue;
|
||
auto &[sum, readings] = sums[value];
|
||
sum += (std::max)(sqlite3_column_int64(statement, 1), sqlite3_int64{0});
|
||
++readings;
|
||
}
|
||
sqlite3_finalize(statement);
|
||
}
|
||
for (const auto &[value, sum] : sums)
|
||
{
|
||
if (sum.second > 1)
|
||
totals->emplace(value, sum.first);
|
||
}
|
||
return totals;
|
||
}
|
||
|
||
namespace
|
||
{
|
||
|
||
// P(读音|字) 的免罚线与扣分上限(log10)。
|
||
//
|
||
// 已有的 reading_prior 看的是 P(字|拼音):这一行占该拼音全部行的权重比。它分得出权重为 0 的
|
||
// 「卷」读 gun,却分不出「大」读 tai——大 的 tai 行有 4719,占 tai 的 0.47%,只扣 0.33,翻不过
|
||
// 语言模型对「大赚」的偏好(差 1.6 左右),tai'zhuan'qian'le 于是出「大赚钱了」。同一行换个
|
||
// 角度看,大 的 da 行有 327 万,读 tai 的只占 0.14%:这才是「这个字几乎不这么读」。语言模型
|
||
// 只认汉字,这一维只能由读音补上(噪声信道里的 P(拼音|字))。
|
||
//
|
||
// 免罚线定在 10%:长 chang 42%、重 chong 20%、行 hang 15% 这类常见的第二读音一分不扣,
|
||
// 觉 jiao、还 huan(8%~9%)只扣零点零几,着 zhao/zhuo、地 di(3%~4%)扣 0.4~0.5。真正的
|
||
// 生僻读音——大 tai/dai、会 kuai、都 du、给 ji——都在 0.1%~0.3%,扣 1.5~2。了 liao(0.47%)
|
||
// 扣 1.3,「受不了」「去不了」仍靠语言模型的上下文赢下 料/聊。5% 那一档还差一点翻不过
|
||
// 「大赚钱了」,20% 在评测句上与 10% 结果相同,取保守的一边。上限只管权重为 0 的读音,那些
|
||
// reading_prior 已经扣过一大截。
|
||
//
|
||
// 只罚单字:多音字的少见读音落在词里(会计、成都、大夫)时整词就是读音的证据。没有同音
|
||
// 竞争的字(得 只有它一个读 dei)各条路径扣得一样多,排序不受影响。
|
||
constexpr double kRareCharReadingShare = 0.1;
|
||
constexpr double kRareCharReadingFloor = 2.5;
|
||
|
||
double rare_char_reading_penalty(const LatticeLexeme &lexeme, const CharReadingTotals *totals)
|
||
{
|
||
if (totals == nullptr)
|
||
return 0.0;
|
||
const auto found = totals->find(lexeme.value);
|
||
if (found == totals->end() || found->second <= 0)
|
||
return 0.0;
|
||
const double weight = lexeme.weight > 0 ? static_cast<double>(lexeme.weight) : 0.0;
|
||
const double share = (weight + 1.0) / (static_cast<double>(found->second) + 1.0);
|
||
if (share >= kRareCharReadingShare)
|
||
return 0.0;
|
||
return (std::min)(-std::log10(share / kRareCharReadingShare), kRareCharReadingFloor);
|
||
}
|
||
|
||
} // namespace
|
||
|
||
WordLatticeLookup make_lattice_db_lookup(sqlite3 *db, std::unordered_map<std::string, sqlite3_stmt *> &statement_cache,
|
||
int span_limit, metasequoia::FuzzyPinyinOptions fuzzy,
|
||
std::shared_ptr<const CharReadingTotals> char_totals)
|
||
{
|
||
return [db, &statement_cache, span_limit, fuzzy, char_totals = std::move(char_totals)](const Segments &span) {
|
||
// ü 的两种写法归一到词库存的那一种。intact_pinyin_list() 两种都认,可词库只存
|
||
// 一种:切出来的 jv、lue 这类音节一行都查不到,精确键查询查空,整个词格就断在
|
||
// 这里——wo'men'lue'de 解不出任何路径,长句联想直接消失。这不是模糊音降级,lue
|
||
// 和 lve 本来就是同一个读音的两种拼写,归一化之后仍然是精确匹配。
|
||
Segments normalized;
|
||
normalized.reserve(span.size());
|
||
for (const auto &syllable : span)
|
||
normalized.push_back(canonical_umlaut_spelling(syllable));
|
||
|
||
// 词格只收「键与跨度完全相等」的行。query_segments_keyed_flat 是带降级的
|
||
// 查询:精确键查不到就退到前缀区间扫描(key >= "gun'qi" AND key < …),
|
||
// gun'qi 于是捞出 gun'qiu 的「滚球」——拿这种行去造句,联想出来的短语读音
|
||
// 根本对不上用户敲的拼音。简拼那两级同理。走批量精确键查询既堵住这个口子,
|
||
// 也省掉了白扫的前缀区间。
|
||
std::vector<Segments> keys{normalized};
|
||
for (auto &variant : fuzzy_segmentations(normalized, fuzzy, kFuzzyLatticeVariantLimit))
|
||
{
|
||
for (auto &syllable : variant)
|
||
syllable = canonical_umlaut_spelling(std::move(syllable));
|
||
keys.push_back(std::move(variant));
|
||
}
|
||
// 模糊变体与敲出的读音同批查(同表一条 SQL)。LIMIT 按表计,放宽到每个键一份,
|
||
// 免得高频的模糊读音把敲出读音的行挤出去;折算之后再截回 span_limit。
|
||
const int limit = span_limit * static_cast<int>(keys.size());
|
||
const auto rows = query_exact_segmentations_keyed_flat(keys, db, statement_cache, limit);
|
||
std::vector<LatticeLexeme> lexemes;
|
||
lexemes.reserve(rows.size());
|
||
for (const auto &row : rows)
|
||
{
|
||
LatticeLexeme lexeme{row.key, row.value, row.weight};
|
||
if (span.size() == 1)
|
||
lexeme.penalty += rare_char_reading_penalty(lexeme, char_totals.get());
|
||
if (keys.size() > 1)
|
||
{
|
||
// 每个与敲出读音不同的音节扣一份分:一个词里错两个音,比只错一个更不可能。
|
||
const auto syllables = split_segments(row.key);
|
||
for (size_t i = 0; i < syllables.size() && i < normalized.size(); ++i)
|
||
lexeme.penalty += syllables[i] == normalized[i] ? 0.0 : kFuzzyLatticePenaltyLog;
|
||
}
|
||
lexemes.push_back(std::move(lexeme));
|
||
}
|
||
if (keys.size() > 1)
|
||
{
|
||
// 按折后权重排序再截断:与 build_graph 一样,参与解码的只是排在前面的 span_limit 行。
|
||
std::stable_sort(lexemes.begin(), lexemes.end(), [](const LatticeLexeme &a, const LatticeLexeme &b) {
|
||
return static_cast<double>(a.weight) * std::pow(10.0, -a.penalty) >
|
||
static_cast<double>(b.weight) * std::pow(10.0, -b.penalty);
|
||
});
|
||
// 同一个字两种读音都收(多音字),只留排在前面的那行。
|
||
std::unordered_set<std::string> seen;
|
||
lexemes.erase(
|
||
std::remove_if(lexemes.begin(), lexemes.end(),
|
||
[&](const LatticeLexeme &lexeme) { return !seen.insert(lexeme.value).second; }),
|
||
lexemes.end());
|
||
if (lexemes.size() > static_cast<size_t>(std::max(0, span_limit)))
|
||
lexemes.resize(static_cast<size_t>(std::max(0, span_limit)));
|
||
}
|
||
return lexemes;
|
||
};
|
||
}
|
||
|
||
} // namespace quanpin
|