一方代码在 /W4 下已零警告。剩余警告全部来自上游副本 (googlepinyinime-rev/、vendor/opencc/ 及其捆绑的 marisa), 按 AGENTS.md 保留上游原格式,有意不改。 - data_path.h、test_pinyin.cpp: _wgetenv 改 _wdupenv_s,返回值 用 optional 包一层,由 unique_ptr 负责 free - quanpin_dictionary.cpp: 去掉条件表达式内的赋值。外层查询原本 只用其真假,reset_cache_if_database_changed() 可能使其失效, 故保留重取 - build_sql_for_checking_word: 删掉从未使用的 jp 形参 - 未使用的 catch 形参、int/size_type 符号不匹配 - wav_writer.h: 形参 sample_rate 遮蔽同名命名空间常量,改名 requested_sample_rate;默认值改为直接引用该常量,去掉魔数 - pinyin_file_io.h: _wfopen/_wopen 改 _wfsopen/_wsopen_s 最后一项没有用微软推荐的 _wfopen_s: 它打开的文件不可共享,而这是 词库文件,Server、TSF DLL 和设置进程会并发读同一份。_SH_DENYNO 正是被弃用调用原本的共享语义,因此只消除警告,不改变行为。 _wsopen_s 的 pmode 传 0,唯一调用方 userdict.cpp 用的是 O_WRONLY, 不带 _O_CREAT。
1167 lines
42 KiB
C++
1167 lines
42 KiB
C++
#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;
|
|
|
|
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;
|
|
}
|
|
|
|
quanpin::Segments normalize_umlaut_aliases(quanpin::Segments segments)
|
|
{
|
|
for (auto &segment : segments)
|
|
{
|
|
if (segment.size() >= 2 && segment[1] == 'v' &&
|
|
(segment[0] == 'j' || segment[0] == 'q' || segment[0] == 'x' || segment[0] == 'y'))
|
|
{
|
|
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, and maps the ü-style 'v' spelling onto 'u' on both sides. The
|
|
// jv/nv normalisation is not a correction, so it must never make the primary
|
|
// segmentation look rewritten (jv displays as typed and stays unmarked).
|
|
std::string fold_autocorrect_letters(const std::string &text)
|
|
{
|
|
std::string folded;
|
|
folded.reserve(text.size());
|
|
for (const char ch : text)
|
|
{
|
|
if (ch == '\'')
|
|
{
|
|
continue;
|
|
}
|
|
const char lower = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
|
folded.push_back(lower == 'v' ? 'u' : lower);
|
|
}
|
|
return folded;
|
|
}
|
|
|
|
struct SeriesQueryResolution
|
|
{
|
|
std::string segmentation;
|
|
std::string cache_key;
|
|
quanpin::Segments corrected_segments;
|
|
bool corrected_input = false;
|
|
};
|
|
|
|
SeriesQueryResolution resolve_series_query(const std::string &raw_input, const std::string &segmentation,
|
|
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.
|
|
result.corrected_input =
|
|
autocorrect_types != 0 && !segments.empty() && raw_input.find('\'') == std::string::npos &&
|
|
!quanpin::has_only_complete_pinyin_segments(segments) &&
|
|
!quanpin::looks_like_syllable_with_jianpin_tail(raw_input) &&
|
|
!(result.corrected_segments = quanpin::autocorrect_cut(raw_input, autocorrect_types)).empty();
|
|
result.segmentation =
|
|
result.corrected_input
|
|
? quanpin::join_segments(result.corrected_segments)
|
|
: (segmentation.empty() ? (segments.empty() ? raw_input : quanpin::join_segments(segments)) : segmentation);
|
|
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), paths_(std::move(paths)),
|
|
decoder_(paths_.resource(metasequoia::assets::pinyin_model),
|
|
paths_.user(metasequoia::assets::pinyin_user_dictionary)),
|
|
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.
|
|
const auto resolution = resolve_series_query(raw_input, segmentation, segments, autocorrect_types);
|
|
pinyin_segmentation_ = resolution.segmentation;
|
|
|
|
// 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_};
|
|
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)
|
|
{
|
|
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));
|
|
}
|
|
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() >= 3 && quanpin::has_only_complete_pinyin_segments(segments))
|
|
{
|
|
// Keep one local Google-Pinyin sentence as the primary whole-sentence
|
|
// suggestion. The lattice is a secondary source; it should add only
|
|
// its best path rather than filling the candidate page with near-
|
|
// duplicate low-quality sentences.
|
|
const std::string normalized = remove_delimiters(segmentation.empty() ? raw_input : segmentation);
|
|
const std::string google_sentence = search_sentence_from_ime_engine(normalized);
|
|
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 为空时保持为空,交由既有逻辑判定为不可落库。
|
|
result.insert(result.begin(), WordItem(segmentation.empty() ? raw_input : segmentation, google_sentence,
|
|
1, CandidateSource::Fallback, segmentation));
|
|
}
|
|
|
|
quanpin::WordLatticeOptions lattice_options;
|
|
lattice_options.nbest = 1;
|
|
quanpin::merge_lattice_candidates(result, segments,
|
|
quanpin::make_lattice_db_lookup(db_, statement_cache_,
|
|
quanpin::QuerySource::Quanpin,
|
|
lattice_options.span_limit),
|
|
segmentation.empty() ? raw_input : segmentation, lattice_options);
|
|
if (!google_sentence.empty())
|
|
{
|
|
const auto google = std::find_if(result.begin(), result.end(), [&](const WordItem &item) {
|
|
return item.word == google_sentence && item.source == CandidateSource::Fallback;
|
|
});
|
|
if (google != result.end() && google != result.begin())
|
|
{
|
|
WordItem preferred = std::move(*google);
|
|
result.erase(google);
|
|
result.insert(result.begin(), std::move(preferred));
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (!segmentation.empty())
|
|
{
|
|
return quanpin::split_segments(segmentation);
|
|
}
|
|
|
|
return get_or_compute_segments(raw_input);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
const auto lookup_segments = normalize_umlaut_aliases(segments);
|
|
const auto flat_items = quanpin::query_segments_keyed_flat(lookup_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.
|
|
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;
|
|
});
|
|
if (best_alternative != merged_full.end() &&
|
|
static_cast<size_t>(std::distance(merged_full.begin(), best_alternative)) >
|
|
kBestAlternativeSegmentationMaxIndex)
|
|
{
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
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 the corrected interpretation exactly when its code
|
|
// letters equal the primary segmentation letters while those differ from the
|
|
// typed letters. The first condition alone would also sweep up prefix
|
|
// candidates (keneng -> ke, single-letter jianpin expansions) that the user
|
|
// spelled correctly; both rules together keep those unmarked. Deliberately
|
|
// switch-independent: the scheme alias layer rewrites letters regardless of
|
|
// the autocorrect switches, so an alias-corrected candidate is labelled as
|
|
// such even with autocorrection off.
|
|
const std::string primary_letters = fold_autocorrect_letters(pinyin_segmentation_);
|
|
const std::string raw_letters = fold_autocorrect_letters(raw_input);
|
|
if (primary_letters.empty() || primary_letters == raw_letters)
|
|
{
|
|
return;
|
|
}
|
|
for (auto &item : candidates)
|
|
{
|
|
if (!item.corrected_from.empty())
|
|
{
|
|
continue;
|
|
}
|
|
if (fold_autocorrect_letters(item.pinyin) == primary_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);
|
|
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::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();
|
|
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, segmentation, 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();
|
|
current_candidate_list_.clear();
|
|
}
|
|
|
|
void QuanpinDictionary::reset_cache()
|
|
{
|
|
cache_.clear();
|
|
series_cache_.clear();
|
|
segmentation_cache_.clear();
|
|
}
|
|
|
|
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));
|
|
std::stable_sort(result.begin(), result.end(),
|
|
[](const auto &a, const auto &b) { return a.pinyin.size() > b.pinyin.size(); });
|
|
}
|
|
// 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;
|
|
}
|