Files
msime-windows/engine/quanpin/quanpin_dictionary.cpp
T
fanlusky 57dd0fc2f3 fix(engine,server): 整句来源标签只挂在整句联想候选上
候选窗按 CandidateSource 挂〔Trigram〕/〔Unigram〕,但 Generated/Fallback 同时被
原样上屏兜底、英文模式原文、日期、Unicode、假名、译文义项等合成候选借用,导致
输入 E 等特殊模式前缀时原样候选被误标为〔Unigram〕。

WordItem 新增 sentence_association 标记,只由 Google 解码器整句(全拼、双拼)与
kenlm 词格整句置位;Server 挂标签时同时要求来源与标记。source 本身不变,落库与
学习策略不受影响。
2026-09-27 08:25:51 +08:00

1462 lines
60 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "fuzzy_pinyin.h"
#include "quanpin_dictionary.h"
#include "../user_dictionary/user_dictionary_journal.h"
#include "../common/helpcode_utils.h"
#include "quanpin_query.h"
#include "quanpin_utils.h"
#include "../shuangpin/shuangpin_utils.h"
#include <algorithm>
#include <climits>
#include <cstring>
#include <fmt/format.h>
#include <unordered_set>
#include <utf8/cpp17.h>
namespace
{
constexpr size_t kSparsePinyinFallbackThreshold = 8;
constexpr size_t kSyllableGraphPathLimit = 32;
constexpr size_t kMaxSyllablesForMultipleSegmentations = 4;
constexpr int kAlternativeSegmentationCandidateLimit = 128;
constexpr size_t kBestAlternativeSegmentationMaxIndex = 1;
// 保护位只对「排在首页之外」的备选读音生效。默认 page_size 是 6,所以自然排序已经
// 进了前 6 的候选一律按权重原样呈现——用户调频写的就是权重,再钉一次等于把刚调出来
// 的名次覆盖掉。page_size 可配(3..9),这里取默认值:调大页长最多是少提升一两个
// 本来就看得见的词,不会把该提升的漏掉。
constexpr size_t kAlternativeSegmentationFirstPageSize = 6;
// 备选切分保护位的词频门槛:备选读音的最佳词权重达到主切分首位的 1/RATIO 以上
// 才配保护位。跨表权重不可直接比(单字是语料计数、词组是小尺度词权),这个量级
// 判断只负责把「真歧义」和「罕见重码」分开:xian -> 西安(55K) 对 先(1.66M),
// 比值 3.3% 仍提升;xie -> 西鄂(6) 对 些(3.75M) 被拒,jiang -> 激昂(23.7K) 对
// 将(2.63M) 同样被拒。
constexpr std::int64_t kAlternativeSegmentationPromotionRatio = 100;
bool is_alpha_vk(ImeKeyCode vk)
{
return vk >= 'A' && vk <= 'Z';
}
std::string remove_delimiters(const std::string &segmented)
{
std::string normalized = segmented;
normalized.erase(std::remove(normalized.begin(), normalized.end(), '\''), normalized.end());
return normalized;
}
// 正字法别名归一:把非标准 ü 拼写改写成词库标准键,输出即进入查询管线的权威拼写
// (查库键、pinyin_segmentation_、调频/造词键因此全部落在标准拼法上)。标准拼音约定:
// ü 保留鱼眼的音节(n/l 系)键写 v(nv/nve/lv/lve),省略鱼眼的(j/q/x/y 系)键写 u
// (ju/jue/qu/.../yue)。nu/lu 是真实音节(怒/路),所以 n/l 系只按段精确匹配 nue/lue,
// 绝不改写 nv/lv/nu/lu。
quanpin::Segments normalize_umlaut_aliases(quanpin::Segments segments)
{
for (auto &segment : segments)
{
if (segment.size() == 3 && segment[1] == 'u' && segment[2] == 'e' && (segment[0] == 'n' || segment[0] == 'l'))
{
// nue/lue → nve/lve:词库正键用 v,混拼 u 是它的别名。
segment[1] = 'v';
}
else if (segment.size() >= 2 && segment[1] == 'v' &&
(segment[0] == 'j' || segment[0] == 'q' || segment[0] == 'x' || segment[0] == 'y'))
{
// j/q/x/y 系没有保留鱼眼的合法音节,v 一律是 u 的别名。
segment[1] = 'u';
}
}
return segments;
}
std::string series_cache_key(const std::string &raw_input, const std::string &segmentation)
{
const char *prefix = raw_input.find('\'') == std::string::npos ? "A:" : "M:";
return prefix + (segmentation.empty() ? raw_input : segmentation);
}
// Folds letters for autocorrect comparisons: lowercases and strips manual
// delimiters. v and u must compare as distinct letters — the ü-style alias
// rewrite is a first-class marking source (the trailing star badge), so
// 'nve' (primary segmentation) differing from typed 'nue' is exactly what
// produces the corrected_from label. The old contract ("aliases never look
// rewritten, hence never marked") was flipped by product decision.
std::string fold_autocorrect_letters(const std::string &text)
{
std::string folded;
folded.reserve(text.size());
for (const char ch : text)
{
if (ch == '\'')
{
continue;
}
folded.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
}
return folded;
}
// How many ranked correction cuts feed the query pipeline: the primary cut
// becomes the query key and the rest ride along as alternative segmentations
// for query-time disambiguation (patent CN 101133411 B M3). k=9 is set by the
// phase-4/5 evaluation data: dropped-initial keys such as uan have 10-15
// equally-weighted deletion targets (cuan/duan/guan/.../zuan), and k=3 cut the
// list by table order before reaching the correct reading (quan'li for uanli).
// k=9 lifts R@1/R@3 across the deletion and mixed models with no p95 change.
constexpr std::size_t kAutocorrectCutKBest = 9;
quanpin::Segments cut_syllables(const quanpin::AutocorrectCut &cut)
{
quanpin::Segments syllables;
syllables.reserve(cut.segments.size());
for (const auto &segment : cut.segments)
{
syllables.push_back(segment.syllable);
}
return syllables;
}
SeriesQueryResolution resolve_series_query(const std::string &raw_input, const quanpin::Segments &segments,
unsigned autocorrect_types)
{
SeriesQueryResolution result;
// Guard order matters: the jianpin-shape predicate runs before autocorrect_cut
// so a guarded input never pays for the BFS. Both guards express "the user did
// not mistype" and either one disables the rewrite entirely. The base
// segmentation is deliberately NOT part of the gate: the BFS works on the raw
// letters, so an empty base cut (nothing segmentable, e.g. a dropped initial
// such as uanli for quan'li) is often exactly the input that needs correction.
// With no correction reading the search returns nothing and the plain path is
// kept unchanged.
const bool eligible = autocorrect_types != 0 && raw_input.find('\'') == std::string::npos &&
!quanpin::has_only_complete_pinyin_segments(segments) &&
!quanpin::looks_like_syllable_with_jianpin_tail(raw_input);
if (eligible)
{
// Fill corrected_segments and the two alternative tiers from cost-ranked
// cuts, optionally appending a trailing jianpin segment (used by the
// head-correction + jianpin-tail composition below). cuts are ranked
// cheapest-first (edge count, then weight): the primary defines the cost
// tier, same-cost readings frequency-compete with it, costlier readings
// stay behind it (see the two vectors' contracts in the header).
const auto populate_from_cuts = [&](const std::vector<quanpin::AutocorrectCut> &cuts,
const std::string &jianpin_tail) {
const auto to_segments = [&](const quanpin::AutocorrectCut &cut) {
quanpin::Segments segs = cut_syllables(cut);
if (!jianpin_tail.empty())
{
segs.push_back(jianpin_tail);
}
return segs;
};
const auto &primary_cut = cuts.front();
result.corrected_segments = to_segments(primary_cut);
for (std::size_t i = 1; i < cuts.size(); ++i)
{
if (cuts[i].same_cost_as(primary_cut))
{
result.alternative_corrected_cuts.push_back(to_segments(cuts[i]));
}
else
{
result.costlier_corrected_cuts.push_back(to_segments(cuts[i]));
}
}
};
const auto cuts = quanpin::autocorrect_cut_kbest(raw_input, autocorrect_types, kAutocorrectCutKBest);
if (!cuts.empty())
{
result.corrected_input = true;
populate_from_cuts(cuts, "");
}
else
{
// Compose a correction with a trailing jianpin tail. The k-best search
// only reaches the end when every segment is a complete syllable, so an
// input like "hauzh" (hau typo + zh jianpin) or "hauz" fails outright.
// Retry on the head with the trailing incomplete-syllable prefix removed;
// query_series then handles the tail exactly as it does for the correctly
// spelled "huazh" (hua + zh). Shortest tail first, so the correction
// explains as much of the input as possible.
//
// Neighbor corrections are excluded from the head: they carry the widest
// false-positive surface (largest table), and this path already relaxes
// the "whole input is a legal cut" constraint by trusting a speculative
// jianpin boundary. Stacking the two turns deletion-shaped input into
// noise (e.g. "shng" -> "sun" + "g" -> 笋干). Structural typos
// (transposition / deletion / insertion) are confident enough to compose.
const unsigned head_types = autocorrect_types & ~quanpin::kAutocorrectNeighbor;
const auto &prefixes = quanpin::prefix_pinyin_set();
const auto &intact = quanpin::intact_pinyin_set();
for (std::size_t tail_len = 1; head_types != 0 && tail_len <= 2 && tail_len < raw_input.size(); ++tail_len)
{
const std::string tail = raw_input.substr(raw_input.size() - tail_len);
// A jianpin tail is an incomplete syllable: a valid pinyin prefix
// that is not itself a complete syllable (e.g. "zh", "z", "h").
if (prefixes.count(tail) == 0 || intact.count(tail) != 0)
{
continue;
}
const std::string head = raw_input.substr(0, raw_input.size() - tail_len);
const auto head_cuts = quanpin::autocorrect_cut_kbest(head, head_types, kAutocorrectCutKBest);
if (head_cuts.empty())
{
continue;
}
result.corrected_input = true;
populate_from_cuts(head_cuts, tail);
break;
}
}
}
// Both branches rebuild the segmentation string from segments: they already
// carry the canonical (alias-normalised) spelling, while a caller-passed
// explicit string could keep the alias spelling — the query would succeed
// yet nothing would look rewritten, so alias inputs with manual delimiters
// ("nue'hao") could never get marked. Delimiters sit on syllable boundaries,
// so split/join round-trips them unchanged ("nu'e" stays "nu'e").
result.segmentation = result.corrected_input ? quanpin::join_segments(result.corrected_segments)
: (segments.empty() ? raw_input : quanpin::join_segments(segments));
result.cache_key = (result.corrected_input ? "C:" : "") + series_cache_key(raw_input, result.segmentation);
return result;
}
std::string escape_sql_text(std::string text)
{
size_t pos = 0;
while ((pos = text.find('\'', pos)) != std::string::npos)
{
text.insert(pos, 1, '\'');
pos += 2;
}
return text;
}
} // namespace
QuanpinDictionary::QuanpinDictionary(std::string db_path, metasequoia::RuntimePaths paths)
: cache_(128), series_cache_(128), segmentation_cache_(128), resolution_cache_(128), paths_(std::move(paths)),
decoder_(paths_.resource(metasequoia::assets::pinyin_model),
paths_.user(metasequoia::assets::pinyin_user_dictionary)),
language_model_(&ngram::shared_language_model(paths_.resource(metasequoia::assets::language_model))),
neural_desktop_model_(neural::shared_sentence_model(
metasequoia::path_to_utf8(paths_.resource(metasequoia::assets::neural_model_desktop)))),
neural_keyboard_model_(neural::shared_sentence_model(
metasequoia::path_to_utf8(paths_.resource(metasequoia::assets::neural_model_keyboard)))),
db_path_(db_path.empty() ? metasequoia::path_to_utf8(paths_.dictionary(metasequoia::assets::main_dictionary))
: std::move(db_path))
{
const int exit = sqlite3_open(db_path_.c_str(), &db_);
if (exit != SQLITE_OK)
{
(void)0;
}
quanpin::warm_up(db_, statement_cache_);
reset_cache_if_database_changed();
}
QuanpinDictionary::~QuanpinDictionary()
{
for (auto &[sql, stmt] : statement_cache_)
{
if (stmt != nullptr)
{
sqlite3_finalize(stmt);
}
}
if (db_ != nullptr)
{
sqlite3_close(db_);
}
}
std::vector<WordItem> QuanpinDictionary::query_exact(const std::string &raw_input, const std::string &segmentation,
unsigned autocorrect_types)
{
if (raw_input.empty())
{
current_candidate_list_.clear();
return {};
}
pinyin_sequence_ = raw_input;
const auto segments = resolve_segments(raw_input, segmentation);
// Typing autocorrection: when the spelling is not a legal pinyin
// combination (the correction cut already fell back to greedy), try to
// rewrite the whole string into legal syllables. The corrected
// segmentation becomes the primary key so that selection and weight
// updates land on the right dictionary entries, and the original
// (garbage-leaning) candidates stay behind as a fallback tail.
// Memoize the k-best correction search: it is the expensive part of every
// keystroke, yet a pure function of this input tuple, so a re-typed or
// backspaced prefix should reuse it instead of re-running the k=9 beam.
const std::string resolution_key = std::to_string(autocorrect_types) + '\x1f' + raw_input + '\x1f' + segmentation;
SeriesQueryResolution resolution;
if (const auto cached_resolution = resolution_cache_.get(resolution_key))
{
resolution = cached_resolution.value();
}
else
{
resolution = resolve_series_query(raw_input, segments, autocorrect_types);
resolution_cache_.insert(resolution_key, resolution);
}
pinyin_segmentation_ = resolution.segmentation;
// The alternative readings must be published even on the cache-hit path:
// mark_autocorrect_candidates runs after every query, cached or not.
pinyin_alternative_segmentations_.clear();
for (const auto &alternative : resolution.alternative_corrected_cuts)
{
pinyin_alternative_segmentations_.push_back(quanpin::join_segments(alternative));
}
// Costlier readings are still corrections, so they must be marked as such
// even though they rank below the primary tier (see the tail append below).
for (const auto &alternative : resolution.costlier_corrected_cuts)
{
pinyin_alternative_segmentations_.push_back(quanpin::join_segments(alternative));
}
// Autocorrected results get their own cache slot so they never leak the
// fallback tail into plain (correct) spellings sharing the same key.
if (series_cache_.get(resolution.cache_key))
{
reset_cache_if_database_changed();
if (const auto cached = series_cache_.get(resolution.cache_key))
{
current_candidate_list_ = cached.value();
return current_candidate_list_;
}
}
std::vector<quanpin::Segments> alternative_segmentations;
std::unordered_set<std::string> seen_segmentations = {pinyin_segmentation_};
// Keep costlier readings out of the frequency-competing merge tier; they are
// appended after it below so dictionary frequency never lifts a dearer
// correction above the cheaper primary. Seeding "seen" here also blocks the
// greedy correction paths from re-introducing them into the merge tier.
for (const auto &costlier : resolution.costlier_corrected_cuts)
{
seen_segmentations.insert(quanpin::join_segments(costlier));
}
const auto append_alternative = [&](const quanpin::Segments &candidate) {
const std::string key = quanpin::join_segments(candidate);
if (!key.empty() && seen_segmentations.insert(key).second &&
alternative_segmentations.size() < kSyllableGraphPathLimit)
{
alternative_segmentations.push_back(candidate);
}
};
// Correction-mode segmentation is part of autocorrection and must follow the
// same switch. query() defaults autocorrect_types to 0, so running this
// unconditionally changed the default behaviour for every caller: a misspelled
// input such as "sahng" started offering the corrected candidate even with
// autocorrection explicitly turned off.
if (autocorrect_types != 0)
{
// Correction alternatives first: they explain the letters the user
// actually typed, so they outrank the phonetic-shape alias readings that
// follow (the alias layer rewrites legal-looking spellings regardless).
for (const auto &candidate : resolution.alternative_corrected_cuts)
{
append_alternative(candidate);
}
const auto correction_paths = quanpin::cut_pinyin_by_mode(raw_input, "correction");
for (const auto &candidate : correction_paths)
{
append_alternative(candidate);
}
}
if (raw_input.find('\'') == std::string::npos && segments.size() <= kMaxSyllablesForMultipleSegmentations &&
quanpin::has_only_complete_pinyin_segments(segments))
{
const auto complete_paths = quanpin::enumerate_complete_segmentations(quanpin::build_syllable_graph(raw_input),
kSyllableGraphPathLimit);
for (const auto &candidate : complete_paths)
{
append_alternative(candidate);
}
}
std::vector<WordItem> result;
if (resolution.corrected_input)
{
result = query_series(raw_input, pinyin_segmentation_, resolution.corrected_segments);
const std::string fallback_segmentation =
segmentation.empty() ? quanpin::join_segments(segments) : segmentation;
append_unique_words(result, query_series(raw_input, fallback_segmentation, segments));
// Query-time disambiguation (patent M3): the alternative readings of the
// same typo compete with the primary cut under dictionary word
// frequency; the best one keeps a protected slot near the top.
if (!alternative_segmentations.empty())
{
result = merge_alternative_segmentations(raw_input, pinyin_segmentation_, resolution.corrected_segments,
alternative_segmentations, std::move(result));
}
// Costlier readings sit below the whole primary cost tier: append them
// last so a high-frequency dearer correction (gau -> gai, weight 13)
// can never precede the cheaper one (gau -> gua, weight 10).
for (const auto &costlier : resolution.costlier_corrected_cuts)
{
append_unique_words(result, query_series(raw_input, quanpin::join_segments(costlier), costlier));
}
}
else
{
result = query_series(raw_input, pinyin_segmentation_, segments);
if (!alternative_segmentations.empty())
{
result = merge_alternative_segmentations(raw_input, pinyin_segmentation_, segments,
alternative_segmentations, std::move(result));
}
}
series_cache_.insert(resolution.cache_key, result);
current_candidate_list_ = result;
return current_candidate_list_;
}
std::optional<WordItem> QuanpinDictionary::find_candidate(const std::string &key, const std::string &value)
{
const std::string table = quanpin::build_table_name(quanpin::split_segments(key));
if (!db_ || table.empty())
return std::nullopt;
sqlite3_stmt *stmt = nullptr;
const std::string sql = "SELECT weight FROM \"" + table + "\" WHERE key=?1 AND value=?2 LIMIT 1";
if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
return std::nullopt;
std::unique_ptr<sqlite3_stmt, decltype(&sqlite3_finalize)> guard(stmt, sqlite3_finalize);
if (sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK ||
sqlite3_bind_text(stmt, 2, value.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK ||
sqlite3_step(stmt) != SQLITE_ROW)
return std::nullopt;
return WordItem(key, value, sqlite3_column_int64(stmt, 0), CandidateSource::Database, key);
}
bool QuanpinDictionary::expand_initial_candidates(const std::string &code, std::vector<WordItem> &candidates)
{
if (code.size() != 1)
{
return false;
}
const auto is_limited_initial = [&](const WordItem &item) {
return item.source == CandidateSource::Database && item.pinyin == code;
};
const size_t limited_count =
static_cast<size_t>(std::count_if(candidates.begin(), candidates.end(), is_limited_initial));
constexpr size_t kInitialCandidateLimit = 24;
if (limited_count != kInitialCandidateLimit)
{
return false;
}
auto expanded = query_initial(code, INT_MAX);
if (expanded.size() <= limited_count)
{
return false;
}
for (auto &item : expanded)
{
item.canonical_pinyin = item.pinyin;
item.pinyin = code;
}
std::vector<WordItem> merged;
merged.reserve(candidates.size() - limited_count + expanded.size());
bool inserted = false;
for (auto &item : candidates)
{
if (is_limited_initial(item))
{
if (!inserted)
{
merged.insert(merged.end(), expanded.begin(), expanded.end());
inserted = true;
}
continue;
}
merged.push_back(std::move(item));
}
candidates = std::move(merged);
cache_.insert(code, expanded);
series_cache_.insert(series_cache_key(pinyin_sequence_, pinyin_segmentation_), candidates);
current_candidate_list_ = candidates;
return true;
}
std::vector<WordItem> QuanpinDictionary::query_series(const std::string &raw_input, const std::string &segmentation,
const quanpin::Segments &segments)
{
if (segments.empty())
{
return query_single_path(raw_input, segmentation, segments);
}
std::vector<WordItem> result;
for (size_t count = segments.size(); count > 0; --count)
{
quanpin::Segments partial_segments(segments.begin(), segments.begin() + static_cast<std::ptrdiff_t>(count));
const std::string partial_segmentation = quanpin::join_segments(partial_segments);
const std::string partial_input = remove_delimiters(partial_segmentation);
auto partial_result = query_single_path(partial_input, partial_segmentation, partial_segments);
result.insert(result.end(), partial_result.begin(), partial_result.end());
}
if (segments.size() >= 2 && quanpin::has_only_complete_pinyin_segments(segments))
{
// 两条整句来源各出一句:词格(kenlm 三元模型打分)在前,Google 解码器在后。
// 词格换成 sc.lm 之后整体比 Google 那条准,所以由它占首位;Google 那条保留,
// 它在词格覆盖不到的输入上仍然有用。两边都只出一句,免得近似重复的整句把
// 候选页挤满。
//
// 两条都插在 generated_sentence_insert_position 给的位置上,也就是开头那串
// 「整串拼音精确命中词库」的候选之后:整句是猜出来的,不该压过词库里真有的
// 短语。相对次序仍由插入点决定——merge_lattice_candidates 算出的位置就是这条
// Fallback 所在的下标,词格随后落在它之前。
//
// Google 解码器自己重新切分,这正是它的价值所在(词格覆盖不到的输入靠它),
// 所以默认喂裸串。但手动分隔符是用户明确表达的切分意图,去掉之后 nu'e 会被
// 它读成 nüe 而出「虐」。撇号本来就是它认的音节分隔符(双拼那条一直是连着
// 撇号传的),手打分隔符时原样传下去即可。
//
// 不管走哪条,送进去之前都要把 ü 换成它认的写法:它的音节表里只有 nue/lue,
// nve 会被拆成 nv + e。见 quanpin::to_google_spelling。
// Google 整句(Fallback):仅在开关打开时才解码、才出候选。
if (sentence_association_.google)
{
const std::string google_input =
raw_input.find('\'') != std::string::npos
? quanpin::to_google_spelling(raw_input)
: remove_delimiters(quanpin::to_google_spelling(segmentation.empty() ? raw_input : segmentation));
const std::string google_sentence = search_sentence_from_ime_engine(google_input);
if (!google_sentence.empty())
{
const auto duplicate = std::find_if(result.begin(), result.end(),
[&](const WordItem &item) { return item.word == google_sentence; });
if (duplicate == result.end())
{
// 整句 fallback 必须带上 canonical quanpin,否则以它结尾的造词无法落库:
// update_creating_word_progress 依赖 canonical_pinyin 才能拼出完整读音。
// segmentation 为空时保持为空,交由既有逻辑判定为不可落库。
const size_t insert_at = quanpin::generated_sentence_insert_position(result, segments);
WordItem sentence(segmentation.empty() ? raw_input : segmentation, google_sentence, 1,
CandidateSource::Fallback, segmentation);
sentence.sentence_association = true;
result.insert(result.begin() + static_cast<std::ptrdiff_t>(insert_at), std::move(sentence));
}
}
}
// 词格给 Trigram 候选和神经模型共用:只开神经时仍在内部解出 n-best 供它重排,
// 但不把词格自己的首选显示出来。神经模型不自己造句,只在这批路径中选一句。
std::vector<quanpin::SourcedLatticeReranker> neural_rerankers;
if (sentence_association_.neural_keyboard && neural_keyboard_model_ != nullptr)
{
neural_rerankers.push_back({quanpin::make_neural_reranker(neural_keyboard_model_, rescoring_context_),
CandidateSource::NeuralKeyboard});
}
if (sentence_association_.neural_desktop && neural_desktop_model_ != nullptr)
{
neural_rerankers.push_back({quanpin::make_neural_reranker(neural_desktop_model_, rescoring_context_),
CandidateSource::NeuralDesktop});
}
quanpin::WordLatticeOptions lattice_options;
// 神经模型和去重补位都需要完整候选池;否则只解最优的一条。
const bool needs_alternatives = !neural_rerankers.empty() || (sentence_association_.word_lattice &&
sentence_association_.show_next_on_duplicate);
lattice_options.nbest = needs_alternatives ? static_cast<int>(neural::RerankOptions{}.max_paths) : 1;
lattice_options.include_lattice_best = sentence_association_.word_lattice;
lattice_options.show_next_on_duplicate = sentence_association_.show_next_on_duplicate;
lattice_options.language_model = language_model_;
if (sentence_association_.word_lattice || !neural_rerankers.empty())
{
quanpin::merge_lattice_candidates(
result, segments, quanpin::make_lattice_db_lookup(db_, statement_cache_, lattice_options.span_limit),
segmentation.empty() ? raw_input : segmentation, lattice_options, neural_rerankers);
}
}
if (result.size() < kSparsePinyinFallbackThreshold)
{
result = append_sparse_pinyin_fallbacks(segments, std::move(result));
}
return result;
}
std::vector<WordItem> QuanpinDictionary::query_single_path(const std::string &raw_input,
const std::string &segmentation,
const quanpin::Segments &segments)
{
const std::string cache_key = segmentation.empty() ? raw_input : segmentation;
if (auto cached = cache_.get(cache_key))
{
return cached.value();
}
std::vector<WordItem> result = query_database(segments, segmentation);
result = append_ime_fallback(raw_input, segmentation, std::move(result));
cache_.insert(cache_key, result);
return result;
}
quanpin::Segments QuanpinDictionary::resolve_segments(const std::string &raw_input, const std::string &segmentation)
{
// 单一归一咽喉点:显式 segmentation 与自动切分两条路径都过这里,进入查询
// 管线的 segments 由此保证已归一恰一次。查库、调频、造词各调用点因此
// 不必各自处理别名。
quanpin::Segments segments =
segmentation.empty() ? get_or_compute_segments(raw_input) : quanpin::split_segments(segmentation);
return normalize_umlaut_aliases(std::move(segments));
}
quanpin::Segments QuanpinDictionary::get_or_compute_segments(const std::string &raw_input)
{
if (auto cached = segmentation_cache_.get(raw_input))
{
return cached.value();
}
const auto cuts = quanpin::cut_pinyin_by_mode(raw_input, "correction");
const auto segments = cuts.empty() ? quanpin::Segments{} : cuts.front();
segmentation_cache_.insert(raw_input, segments);
return segments;
}
int QuanpinDictionary::handleVkCode(ImeKeyCode vk, ImeModifierMask modifiers_down, ImeCharacter wch)
{
(void)modifiers_down;
if (vk == ImeKey::Backspace)
{
if (!pinyin_sequence_.empty())
{
pinyin_sequence_.pop_back();
}
}
else if (vk == ImeKey::Escape || vk == ImeKey::Return || vk == ImeKey::Space)
{
reset_state();
return OK;
}
else if (vk == ImeKey::Apostrophe)
{
pinyin_sequence_.push_back('\'');
}
else if (is_alpha_vk(vk))
{
if (wch >= u'A' && wch <= u'Z')
{
pinyin_sequence_.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(wch))));
}
else if (wch >= u'a' && wch <= u'z')
{
pinyin_sequence_.push_back(static_cast<char>(wch));
}
else
{
pinyin_sequence_.push_back(static_cast<char>(vk + ('a' - 'A')));
}
}
query(pinyin_sequence_);
return OK;
}
std::vector<WordItem> QuanpinDictionary::query_database(const quanpin::Segments &segments,
const std::string &segmentation)
{
if (db_ == nullptr)
{
return {};
}
try
{
if (segments.size() == 1 && segments.front().size() == 1)
{
constexpr int kInitialCandidateLimit = 24;
auto result = query_initial(segments.front(), kInitialCandidateLimit);
const std::string matched_code = segmentation.empty() ? segments.front() : segmentation;
for (auto &item : result)
{
item.canonical_pinyin = item.pinyin;
item.pinyin = matched_code;
}
return result;
}
// segments 已在 resolve_segments 归一,直接按标准键查库。
const auto flat_items = quanpin::query_segments_keyed_flat(segments, db_, statement_cache_, INT_MAX);
std::vector<WordItem> result;
result.reserve(flat_items.size());
const std::string code = segmentation.empty() ? quanpin::join_segments(segments) : segmentation;
for (const auto &item : flat_items)
{
result.emplace_back(code, item.value, item.weight, CandidateSource::Database, item.key);
}
return result;
}
catch (const std::exception &)
{
(void)0;
return {};
}
}
std::vector<WordItem> QuanpinDictionary::query_initial(const std::string &code, int limit)
{
if (db_ == nullptr || code.size() != 1)
{
return {};
}
const auto rows = quanpin::query_initial(db_, code, limit);
std::vector<WordItem> result;
result.reserve(rows.size());
for (const auto &item : rows)
{
result.emplace_back(item.key, item.value, item.weight, CandidateSource::Database, item.key);
}
return result;
}
std::vector<WordItem> QuanpinDictionary::merge_alternative_segmentations(
const std::string &raw_input, const std::string &primary_segmentation, const quanpin::Segments &primary_segments,
const std::vector<quanpin::Segments> &alternative_segmentations, std::vector<WordItem> result)
{
const auto alternative_items = quanpin::query_exact_segmentations_keyed_flat(
alternative_segmentations, db_, statement_cache_, kAlternativeSegmentationCandidateLimit);
if (alternative_items.empty())
{
return result;
}
const auto primary_full = query_single_path(raw_input, primary_segmentation, primary_segments);
std::vector<WordItem> alternative_full;
alternative_full.reserve(alternative_items.size());
for (const auto &item : alternative_items)
{
alternative_full.emplace_back(item.key, item.value, item.weight, CandidateSource::Database, item.key);
}
std::vector<WordItem> merged_full = primary_full;
merged_full.insert(merged_full.end(), alternative_full.begin(), alternative_full.end());
std::stable_sort(merged_full.begin(), merged_full.end(),
[](const WordItem &lhs, const WordItem &rhs) { return lhs.weight > rhs.weight; });
std::unordered_set<std::string> seen_full_words;
merged_full.erase(std::remove_if(merged_full.begin(), merged_full.end(),
[&](const WordItem &item) { return !seen_full_words.insert(item.word).second; }),
merged_full.end());
// Weights rank candidates reliably within one pinyin key, but are not directly comparable across
// different segmentations. Keep the best alternative interpretation visible without letting every
// segmentation occupy a protected slot on the first page -- but only when the best alternative
// word is not dwarfed by the primary reading's top candidate, otherwise the slot goes to noise
// like 西鄂 for "xie" instead of a reading the user might actually have meant.
const std::int64_t primary_top_weight = primary_full.empty() ? 0 : primary_full.front().weight;
const bool promote_alternative =
static_cast<std::int64_t>(alternative_items.front().weight) * kAlternativeSegmentationPromotionRatio >=
primary_top_weight;
const std::string &best_alternative_word = alternative_items.front().value;
const auto best_alternative = std::find_if(merged_full.begin(), merged_full.end(), [&](const WordItem &item) {
return item.word == best_alternative_word;
});
// 这个保护位只负责「把首页外的备选读音拉进首页」,不负责给它排座次。已经排进首页的
// 候选一律不动:它的位置是权重排出来的,而用户调频写的就是权重,再钉一次等于把调频
// 的结果覆盖掉。吉安 就是这么被弹到第 2 位的——调频按 promote 把它放到 index 4,
// 它因此成了 ji'an 组里权重最高的词、过了上面的门槛,于是又被拽到 index 1,用户看到
// 的是「选一次就跳到第二」。西安(自然位置 16,在首页外)不受影响,照旧进 index 1。
const size_t best_alternative_index =
best_alternative == merged_full.end()
? 0
: static_cast<size_t>(std::distance(merged_full.begin(), best_alternative));
if (promote_alternative && best_alternative != merged_full.end() &&
best_alternative_index >= kAlternativeSegmentationFirstPageSize)
{
WordItem promoted = std::move(*best_alternative);
merged_full.erase(best_alternative);
merged_full.insert(merged_full.begin() + static_cast<std::ptrdiff_t>(kBestAlternativeSegmentationMaxIndex),
std::move(promoted));
}
std::vector<WordItem> merged = std::move(merged_full);
const size_t primary_full_count = primary_full.size() < result.size() ? primary_full.size() : result.size();
std::vector<WordItem> remaining(result.begin() + static_cast<std::ptrdiff_t>(primary_full_count), result.end());
append_unique_words(merged, remaining);
return merged;
}
std::vector<WordItem> QuanpinDictionary::append_ime_fallback(const std::string &raw_input,
const std::string &segmentation,
std::vector<WordItem> result)
{
if (!result.empty())
{
return result;
}
// 这条兜底整句也来自 Google 解码器,随「Google 整句联想」开关一起关闭。
if (!sentence_association_.google)
{
return result;
}
const std::string normalized = remove_delimiters(segmentation.empty() ? raw_input : segmentation);
const std::string sentence = search_sentence_from_ime_engine(normalized);
if (sentence.empty())
{
return result;
}
const auto exists =
std::find_if(result.begin(), result.end(), [&](const WordItem &item) { return item.word == sentence; });
if (exists == result.end())
{
// 同上,整句 fallback 需要 canonical quanpin 才能参与造词落库。
result.emplace_back(segmentation.empty() ? raw_input : segmentation, sentence, 1, CandidateSource::Fallback,
segmentation);
result.back().sentence_association = true;
}
return result;
}
std::vector<WordItem> QuanpinDictionary::append_sparse_pinyin_fallbacks(const quanpin::Segments &segments,
std::vector<WordItem> result)
{
for (const auto &fallback_segments : quanpin::sparse_pinyin_fallback_segments(segments))
{
if (fallback_segments.empty())
{
continue;
}
const std::string fallback_segmentation = quanpin::join_segments(fallback_segments);
const std::string fallback_input = remove_delimiters(fallback_segmentation);
const auto fallback_result = query_single_path(fallback_input, fallback_segmentation, fallback_segments);
append_unique_words(result, fallback_result);
}
return result;
}
void QuanpinDictionary::append_unique_words(std::vector<WordItem> &result, const std::vector<WordItem> &extra)
{
for (const auto &item : extra)
{
const auto exists = std::find_if(result.begin(), result.end(),
[&](const WordItem &existing) { return existing.word == item.word; });
if (exists == result.end())
{
result.push_back(item);
}
}
}
void QuanpinDictionary::mark_autocorrect_candidates(std::vector<WordItem> &candidates, const std::string &raw_input)
{
// A candidate comes from a corrected interpretation exactly when its code
// letters equal any correction cut's letters (primary or ranked
// alternative) while those differ from the typed letters. The letters-only
// comparison alone would also sweep up prefix candidates (keneng -> ke,
// single-letter jianpin expansions) that the user spelled correctly; both
// rules together keep those unmarked. The ü-style alias rewrite (nue->nve,
// jv->ju) is a first-class marking source: it runs in resolve_segments
// regardless of the autocorrect switches, so an alias-rewritten query marks
// its candidates with the typed spelling, while a standard spelling exits
// at the all-equal gate because every set equals the typed letters.
std::vector<std::string> corrected_letter_sets;
corrected_letter_sets.reserve(1 + pinyin_alternative_segmentations_.size());
corrected_letter_sets.push_back(fold_autocorrect_letters(pinyin_segmentation_));
for (const auto &alternative : pinyin_alternative_segmentations_)
{
corrected_letter_sets.push_back(fold_autocorrect_letters(alternative));
}
corrected_letter_sets.erase(std::remove_if(corrected_letter_sets.begin(), corrected_letter_sets.end(),
[](const std::string &letters) { return letters.empty(); }),
corrected_letter_sets.end());
const std::string raw_letters = fold_autocorrect_letters(raw_input);
if (corrected_letter_sets.empty() ||
std::all_of(corrected_letter_sets.begin(), corrected_letter_sets.end(),
[&](const std::string &letters) { return letters == raw_letters; }))
{
return;
}
for (auto &item : candidates)
{
if (!item.corrected_from.empty())
{
continue;
}
const std::string item_letters = fold_autocorrect_letters(item.pinyin);
// Match only against readings that actually differ from the typed
// letters. An alternative cut can fold back to exactly raw_letters
// (letters removed by the fold but rewritten by the cut); matching that
// set would stamp corrected_from onto a candidate the user spelled
// correctly. The all-equal gate above only guards the case where EVERY
// set equals raw_letters, so this per-set filter is needed.
if (std::any_of(corrected_letter_sets.begin(), corrected_letter_sets.end(),
[&](const std::string &letters) { return letters != raw_letters && item_letters == letters; }))
{
item.corrected_from = raw_letters;
}
}
}
int QuanpinDictionary::create_word(std::string pinyin, std::string word)
{
pinyin = remove_delimiters(pinyin);
const auto cuts = quanpin::cut_pinyin_by_mode(pinyin, "correction");
if (cuts.empty())
{
return ERROR_CODE;
}
pinyin = quanpin::join_segments(cuts.front());
const std::string jp = quanpin::segments_to_jianpin(cuts.front());
if (!do_validate(pinyin, jp, word))
{
return ERROR_CODE;
}
if (check_data(build_sql_for_checking_word(pinyin, word)))
{
return OK;
}
if (insert_data(build_sql_for_inserting_word(pinyin, jp, word)) != OK)
{
return ERROR_CODE;
}
(void)user_dictionary::record_user_insert(metasequoia::path_to_utf8(paths_.user(metasequoia::assets::user_journal)),
user_dictionary::DictionaryKind::Pinyin, pinyin, word, 10000);
reset_cache();
return OK;
}
int QuanpinDictionary::create_word_from_canonical_pinyin(std::string pinyin, std::string word)
{
const auto segments = quanpin::split_segments(pinyin);
const size_t han_count = HelpcodeUtils::count_han_chars(word);
if (segments.empty() || segments.size() != han_count ||
std::any_of(segments.begin(), segments.end(), [](const std::string &segment) {
return segment.empty() || !quanpin::is_complete_pinyin_input(segment);
}))
{
return ERROR_CODE;
}
pinyin = quanpin::join_segments(segments);
const std::string jp = quanpin::segments_to_jianpin(segments);
// 不再走 do_validate:这里的音节边界由调用方(canonical 键)显式给定,上面已按
// split_segments 校验过段数、逐段完整性,jp 也是每段一字母、长度必然等于字数。
// do_validate 会先 remove_delimiters 再用贪心 correction 重切,把 qi'e'huan 断成
// qie'huan(段数 3→2),从而误杀这条合法整句——正是 AI / 云联想选中却入不了库的根因。
if (check_data(build_sql_for_checking_word(pinyin, word)))
{
return OK;
}
if (insert_data(build_sql_for_inserting_word(pinyin, jp, word)) != OK)
{
return ERROR_CODE;
}
(void)user_dictionary::record_user_insert(metasequoia::path_to_utf8(paths_.user(metasequoia::assets::user_journal)),
user_dictionary::DictionaryKind::Pinyin, pinyin, word, 10000);
reset_cache();
return OK;
}
int QuanpinDictionary::update_weight_by_word(std::string word)
{
return update_weight_by_pinyin_and_word(remove_delimiters(pinyin_segmentation_), std::move(word));
}
int QuanpinDictionary::update_weight_by_pinyin_and_word(std::string pinyin, std::string word)
{
pinyin = remove_delimiters(pinyin);
const auto cuts = quanpin::cut_pinyin_by_mode(pinyin, "correction");
if (cuts.empty())
return ERROR_CODE;
auto segments = cuts.front();
// 别名拼写(nue/lue 等词库不存在的键)不得承载用户调频数据:改写为标准键,
// 保证权重更新落在标准字典行上(PRD R3)。
segments = normalize_umlaut_aliases(std::move(segments));
const size_t han_count = HelpcodeUtils::count_han_chars(word);
if (segments.size() > han_count)
segments.resize(han_count);
const std::string normalized = quanpin::join_segments(segments);
if (update_data(build_sql_for_updating_word(normalized, word)) != OK)
{
return ERROR_CODE;
}
(void)user_dictionary::record_pinyin_upsert_from_database(
db_path_, normalized, word, metasequoia::path_to_utf8(paths_.user(metasequoia::assets::user_journal)));
reset_cache();
return OK;
}
int QuanpinDictionary::delete_by_pinyin_and_word(std::string pinyin, std::string word)
{
pinyin = remove_delimiters(pinyin);
const auto cuts = quanpin::cut_pinyin_by_mode(pinyin, "correction");
if (cuts.empty())
return ERROR_CODE;
const std::string normalized = quanpin::join_segments(cuts.front());
if (!user_dictionary::delete_dictionary_candidate(
db_path_, metasequoia::path_to_utf8(paths_.user(metasequoia::assets::user_journal)),
user_dictionary::DictionaryKind::Pinyin, normalized, word))
return ERROR_CODE;
reset_cache();
return OK;
}
int QuanpinDictionary::insert_word_to_series_cache(const std::string &pinyin, const std::string &word,
CandidateSource source)
{
if (pinyin.empty() || word.empty())
{
return ERROR_CODE;
}
const auto cuts = quanpin::cut_pinyin_by_mode(pinyin, "correction");
const std::string segmentation = cuts.empty() ? pinyin : quanpin::join_segments(cuts.front());
const std::string cache_key = series_cache_key(pinyin, segmentation);
return insert_word_to_series_cache_key(cache_key, pinyin, word, source);
}
int QuanpinDictionary::insert_word_to_series_cache(const std::string &raw_input, const std::string &segmentation,
unsigned autocorrect_types, const std::string &word,
CandidateSource source)
{
if (raw_input.empty() || word.empty())
{
return ERROR_CODE;
}
const auto segments = resolve_segments(raw_input, segmentation);
const auto resolution = resolve_series_query(raw_input, segments, autocorrect_types);
return insert_word_to_series_cache_key(resolution.cache_key, raw_input, word, source);
}
int QuanpinDictionary::insert_word_to_series_cache_key(const std::string &cache_key, const std::string &pinyin,
const std::string &word, CandidateSource source)
{
auto list = series_cache_.get(cache_key).value_or(std::vector<WordItem>{});
// Keep at most one cloud/AI suggestion in the series cache for this key.
if (source == CandidateSource::AiSuggestion || source == CandidateSource::CloudSuggestion)
{
list.erase(
std::remove_if(list.begin(), list.end(), [source](const WordItem &item) { return item.source == source; }),
list.end());
}
const auto exists = std::find_if(list.begin(), list.end(), [&](const WordItem &item) { return item.word == word; });
if (exists == list.end())
{
if (list.empty())
{
list.emplace_back(pinyin, word, 1, source);
}
else
{
const size_t index = source == CandidateSource::AiSuggestion ? std::min<size_t>(2, list.size()) : 1;
list.insert(list.begin() + index, WordItem(pinyin, word, 1, source));
}
}
if (source == CandidateSource::CloudSuggestion)
{
const auto ai = std::find_if(list.begin(), list.end(),
[](const WordItem &item) { return item.source == CandidateSource::AiSuggestion; });
if (ai != list.end())
{
WordItem ai_item = std::move(*ai);
list.erase(ai);
list.insert(list.begin() + std::min<size_t>(2, list.size()), std::move(ai_item));
}
}
series_cache_.insert(cache_key, list);
return OK;
}
std::string QuanpinDictionary::search_sentence_from_ime_engine(const std::string &user_pinyin)
{
return decoder_.sentence(user_pinyin);
}
void QuanpinDictionary::reset_state()
{
pinyin_sequence_.clear();
pinyin_segmentation_.clear();
pinyin_alternative_segmentations_.clear();
current_candidate_list_.clear();
}
void QuanpinDictionary::reset_cache()
{
cache_.clear();
series_cache_.clear();
segmentation_cache_.clear();
}
void QuanpinDictionary::set_sentence_association(const SentenceAssociationOptions &options)
{
if (sentence_association_ == options)
{
return;
}
sentence_association_ = options;
// 开关变了,之前缓存的候选列表按旧开关算出,必须清掉,否则打开/关闭后要到缓存过期
// 才见效。整句候选都进这几个缓存,见 query_series / query_single_path。
reset_cache();
}
void QuanpinDictionary::set_rescoring_context(const std::string &context)
{
if (rescoring_context_ == context)
{
return;
}
rescoring_context_ = context;
// 上文变了,缓存里的顺序是按旧上文重排出来的,同 set_sentence_association。
reset_cache();
}
void QuanpinDictionary::reset_cache_if_database_changed()
{
if (db_ == nullptr)
{
return;
}
sqlite3_stmt *statement = nullptr;
if (sqlite3_prepare_v2(db_, "PRAGMA data_version", -1, &statement, nullptr) != SQLITE_OK)
{
return;
}
if (sqlite3_step(statement) != SQLITE_ROW)
{
sqlite3_finalize(statement);
return;
}
const sqlite3_int64 current_version = sqlite3_column_int64(statement, 0);
sqlite3_finalize(statement);
if (data_version_ >= 0 && current_version != data_version_)
{
reset_cache();
}
data_version_ = current_version;
}
std::vector<std::string> QuanpinDictionary::select_data(const std::string &sql_str)
{
std::vector<std::string> candidate_list;
if (db_ == nullptr)
{
return candidate_list;
}
sqlite3_stmt *stmt = nullptr;
if (sqlite3_prepare_v2(db_, sql_str.c_str(), -1, &stmt, 0) != SQLITE_OK)
{
(void)0;
return candidate_list;
}
while (sqlite3_step(stmt) == SQLITE_ROW)
{
candidate_list.push_back(std::string(reinterpret_cast<const char *>(sqlite3_column_text(stmt, 2))));
}
sqlite3_finalize(stmt);
return candidate_list;
}
std::vector<WordItem> QuanpinDictionary::select_complete_data(const std::string &sql_str)
{
std::vector<WordItem> candidate_list;
if (db_ == nullptr)
{
return candidate_list;
}
sqlite3_stmt *stmt = nullptr;
if (sqlite3_prepare_v2(db_, sql_str.c_str(), -1, &stmt, 0) != SQLITE_OK)
{
(void)0;
return candidate_list;
}
while (sqlite3_step(stmt) == SQLITE_ROW)
{
candidate_list.emplace_back(std::string(reinterpret_cast<const char *>(sqlite3_column_text(stmt, 0))),
std::string(reinterpret_cast<const char *>(sqlite3_column_text(stmt, 2))),
sqlite3_column_int64(stmt, 3), CandidateSource::Database,
std::string(reinterpret_cast<const char *>(sqlite3_column_text(stmt, 0))));
}
sqlite3_finalize(stmt);
return candidate_list;
}
int QuanpinDictionary::check_data(const std::string &sql_str)
{
if (db_ == nullptr)
{
return false;
}
sqlite3_stmt *stmt = nullptr;
if (sqlite3_prepare_v2(db_, sql_str.c_str(), -1, &stmt, 0) != SQLITE_OK)
{
(void)0;
return false;
}
const bool exists = sqlite3_step(stmt) == SQLITE_ROW;
sqlite3_finalize(stmt);
return exists;
}
int QuanpinDictionary::insert_data(const std::string &sql_str)
{
if (db_ == nullptr)
{
return ERROR_CODE;
}
sqlite3_stmt *stmt = nullptr;
if (sqlite3_prepare_v2(db_, sql_str.c_str(), -1, &stmt, 0) != SQLITE_OK)
{
(void)0;
return ERROR_CODE;
}
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return ok ? OK : ERROR_CODE;
}
int QuanpinDictionary::update_data(const std::string &sql_str)
{
if (db_ == nullptr)
{
return ERROR_CODE;
}
sqlite3_stmt *stmt = nullptr;
if (sqlite3_prepare_v2(db_, sql_str.c_str(), -1, &stmt, 0) != SQLITE_OK)
{
(void)0;
return ERROR_CODE;
}
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return ok ? OK : ERROR_CODE;
}
int QuanpinDictionary::delete_data(const std::string &sql_str)
{
if (db_ == nullptr)
{
return ERROR_CODE;
}
sqlite3_stmt *stmt = nullptr;
if (sqlite3_prepare_v2(db_, sql_str.c_str(), -1, &stmt, 0) != SQLITE_OK)
{
(void)0;
return ERROR_CODE;
}
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return ok ? OK : ERROR_CODE;
}
std::string QuanpinDictionary::build_sql_for_creating_word(const std::string &pinyin)
{
const auto cuts = quanpin::cut_pinyin_by_mode(pinyin, "correction");
if (cuts.empty())
{
return "";
}
std::string sql;
for (size_t i = 1; i <= cuts.front().size(); ++i)
{
std::vector<std::string> partial(cuts.front().begin(), cuts.front().begin() + i);
const std::string key = quanpin::join_segments(partial);
const std::string table = quanpin::build_table_name(partial);
const std::string each =
fmt::format("select * from(select * from {} where key = '{}' order by weight desc)", table, key);
sql = sql.empty() ? each : each + " union all " + sql;
}
return sql;
}
std::string QuanpinDictionary::build_sql_for_checking_word(const std::string &key, const std::string &value)
{
const auto cuts = quanpin::cut_pinyin_by_mode(key, "correction");
if (cuts.empty())
{
return "";
}
const std::string table = quanpin::build_table_name(cuts.front());
return fmt::format("select 1 from {} where key = '{}' and value = '{}';", table, escape_sql_text(key),
escape_sql_text(value));
}
std::string QuanpinDictionary::build_sql_for_inserting_word(const std::string &key, const std::string &jp,
const std::string &value)
{
const auto cuts = quanpin::cut_pinyin_by_mode(key, "correction");
if (cuts.empty())
{
return "";
}
const std::string table = quanpin::build_table_name(cuts.front());
return fmt::format("insert into {} (key, jp, value, weight) values ('{}', '{}', '{}', '{}');", table,
escape_sql_text(key), escape_sql_text(jp), escape_sql_text(value), 10000);
}
std::string QuanpinDictionary::build_sql_for_updating_word(const std::string &word)
{
return build_sql_for_updating_word(remove_delimiters(pinyin_segmentation_), word);
}
std::string QuanpinDictionary::build_sql_for_updating_word(std::string pinyin, const std::string &word)
{
pinyin = remove_delimiters(pinyin);
const auto cuts = quanpin::cut_pinyin_by_mode(pinyin, "correction");
if (cuts.empty())
{
return "";
}
size_t han_cnt = HelpcodeUtils::count_han_chars(word);
auto segments = cuts.front();
if (segments.size() > han_cnt)
{
segments.resize(han_cnt);
}
pinyin = quanpin::join_segments(segments);
const std::string jp = quanpin::segments_to_jianpin(segments);
if (!do_validate(pinyin, jp, word))
{
return "";
}
const std::string table = quanpin::build_table_name(segments);
return fmt::format("update {0} set weight = ( select MAX(weight) + 1 from {0} AS sub where sub.key = '{1}') "
"where key = '{1}' and value = '{2}';",
table, escape_sql_text(pinyin), escape_sql_text(word));
}
std::string QuanpinDictionary::build_sql_for_deleting_word(std::string pinyin, const std::string &word)
{
pinyin = remove_delimiters(pinyin);
const auto cuts = quanpin::cut_pinyin_by_mode(pinyin, "correction");
if (cuts.empty())
{
return "";
}
const std::string normalized = quanpin::join_segments(cuts.front());
const std::string jp = quanpin::segments_to_jianpin(cuts.front());
if (!do_validate(normalized, jp, word))
{
return "";
}
return fmt::format("delete from {} where key = '{}' and value = '{}';", quanpin::build_table_name(cuts.front()),
escape_sql_text(normalized), escape_sql_text(word));
}
bool QuanpinDictionary::do_validate(const std::string &key, const std::string &jp, const std::string &value)
{
const std::string pure_key = remove_delimiters(key);
if (pure_key.empty())
{
return false;
}
const size_t han_count = HelpcodeUtils::count_han_chars(value);
if (jp.size() != han_count)
{
return false;
}
const auto cuts = quanpin::cut_pinyin_by_mode(pure_key, "correction");
if (cuts.empty())
{
return false;
}
return cuts.front().size() == han_count;
}
std::vector<WordItem> QuanpinDictionary::fuzzy_candidates(const std::string &segmentation,
metasequoia::FuzzyPinyinOptions options)
{
std::vector<WordItem> result;
if (!options.rules || !db_)
return result;
reset_cache_if_database_changed();
const auto cache_key = "fuzzy:" + std::to_string(options.rules) + ":" + segmentation;
if (const auto cached = series_cache_.get(cache_key))
return *cached;
const auto segments = quanpin::split_segments(segmentation);
std::size_t budget = 128;
for (std::size_t count = segments.size(); count > 0 && budget > 1; --count)
{
const quanpin::Segments prefix(segments.begin(), segments.begin() + count);
const auto paths = quanpin::fuzzy_segmentations(prefix, options, std::min<std::size_t>(64, budget));
if (paths.empty())
continue;
budget -= paths.size();
const auto rows = quanpin::query_exact_segmentations_keyed_flat(paths, db_, statement_cache_, 128);
for (const auto &row : rows)
{
WordItem item(quanpin::join_segments(prefix), row.value, row.weight, CandidateSource::Database, row.key);
item.fuzzy = true;
result.push_back(std::move(item));
}
}
series_cache_.insert(cache_key, result);
return result;
}
std::vector<WordItem> QuanpinDictionary::query(const std::string &raw_input, const std::string &segmentation,
unsigned autocorrect, metasequoia::FuzzyPinyinOptions fuzzy)
{
auto result = query_exact(raw_input, segmentation, autocorrect);
if (fuzzy.rules && !raw_input.empty())
{
// Keep ordinary cache slots free of preference-specific candidates.
const auto typed =
segmentation.empty() ? quanpin::join_segments(resolve_segments(raw_input, segmentation)) : segmentation;
append_unique_words(result, fuzzy_candidates(typed, fuzzy));
// Specificity is how many typed letters a candidate accounts for, not the raw
// length of its stored key: alternative cuts ("xi'e" for "xie", "you'di'an"
// for "you'dian") and fuzzy variants carry apostrophes, so comparing pinyin
// size() verbatim lifted rarer re-segmentations above the exact reading.
const auto matched_letters = [](const WordItem &item) {
size_t letters = 0;
for (const char ch : item.pinyin)
{
letters += ch != '\'';
}
return letters;
};
std::stable_sort(result.begin(), result.end(),
[&](const WordItem &a, const WordItem &b) { return matched_letters(a) > matched_letters(b); });
}
// Labeling runs after every mutation (including the fuzzy merge) so the
// returned list and the published candidate list always agree.
mark_autocorrect_candidates(result, raw_input);
current_candidate_list_ = result;
return result;
}