From e1331ff1dc8124782258f3e6b2ba3b8a95ee2fd4 Mon Sep 17 00:00:00 2001 From: Jia Sui Date: Sat, 3 Oct 2026 22:16:35 +0800 Subject: [PATCH] =?UTF-8?q?fix(engine):=20=E6=81=A2=E5=A4=8D=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87=E9=87=8D=E6=8E=92=E7=9A=84=E3=80=8C=E6=95=B4?= =?UTF-8?q?=E9=94=AE=E5=9C=A8=E5=89=8D=E3=80=81=E5=89=8D=E7=BC=80=E5=9C=A8?= =?UTF-8?q?=E5=90=8E=E3=80=8D=E5=88=86=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 胜出切分接管后用 query_series(raw_input, winner_key, winner_segments) 取它的结果, 但该函数的 count 从整键递减到 1,返回的不只是整键行,还有首音节的全部单字;而 rest(班长、搬账这些同键位整词)在这之后才append。 结果是办证/辩证/整句/班/办/半/般…/班长——胜出切分的单字插到同键位整词之前,把它们 整组挤出首页,PR 描述里「同键位纠错读法整组下移」并不成立。 按 canonical_pinyin == winner_key 把前缀行摘出来,恢复 merge_alternative_segmentations 的既有分层(merged_full 在前、前缀作为 remaining 追加):整键块、rest、前缀块。分类 键选 canonical_pinyin 而非 pinyin——后者承载用户键入码(可能是简拼);整句与词格行 同样落在整键块内,因为 append_ime_fallback 用完整 segmentation、词格 select_distinct 用 path->key 填 canonical_pinyin。 补判别性用例:原 fixture 缺 tbl_1_b,前缀这条路根本没被走到。已实测——撤掉本修复 时该用例失败(Same-tier whole words must not be pushed behind the winner cut's single-character prefixes.),装回后通过。 --- engine/quanpin/quanpin_dictionary.cpp | 18 +++++- engine/tests/src/test_input_session.cpp | 79 +++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/engine/quanpin/quanpin_dictionary.cpp b/engine/quanpin/quanpin_dictionary.cpp index edccc5c7..ff2bfe77 100644 --- a/engine/quanpin/quanpin_dictionary.cpp +++ b/engine/quanpin/quanpin_dictionary.cpp @@ -513,8 +513,24 @@ std::vector QuanpinDictionary::query_exact(const std::string &raw_inpu std::vector rest = merge_alternative_segmentations(raw_input, pinyin_segmentation_, resolution.corrected_segments, remaining_alternatives, std::move(result)); - result = query_series(raw_input, winner_key, winner_segments); + // query_series 的 count 从整键递减到 1,返回的不只是胜出切分的整键行,还有它 + // 首音节的全部单字。整句与词格行跟整键行同键——append_ime_fallback 用完整 + // segmentation 填 canonical_pinyin,词格 select_distinct 用 path->key——所以 + // 按 canonical_pinyin == winner_key 能干净地把前缀行摘出去。 + // + // 不切开会破坏 merge_alternative_segmentations 的既有分层(merged_full 在前、 + // 前缀作为 remaining 追加):query_series(ban'zheng) 先出 办证/辩证/整句,再出 + // 班/办/半/般…,而班长、搬账这些同键位整词在 rest 里、排在单字之后,整组被挤 + // 出首页。这里恢复同一分层:整键块、rest、前缀块。 + std::vector winner_full; + std::vector winner_prefix; + for (auto &item : query_series(raw_input, winner_key, winner_segments)) + { + (item.canonical_pinyin == winner_key ? winner_full : winner_prefix).push_back(std::move(item)); + } + result = std::move(winner_full); append_unique_words(result, std::move(rest)); + append_unique_words(result, std::move(winner_prefix)); context_reordered = true; } } diff --git a/engine/tests/src/test_input_session.cpp b/engine/tests/src/test_input_session.cpp index 7a5e9c9e..ddb4366b 100644 --- a/engine/tests/src/test_input_session.cpp +++ b/engine/tests/src/test_input_session.cpp @@ -411,6 +411,83 @@ void run_autocorrect_context_ranking_tests(const std::filesystem::path &data_dir } } +// 审阅缺陷 D1(PR #592 行内评论):上下文重排接管后,胜出切分的单字前缀曾被插到 +// 同键位整词前面。query_series 的 count 从整键递减到 1,ban'zheng 的输出里除了 +// 办证/辩证/整句还带着首音节的单字;这些单字排在 rest(班长、搬账)之前,把同键位的 +// 其他整词整组挤出首页。原 fixture 没有 tbl_1_b,前缀这条路根本没被走到。 +// 这里补单字表,并直接断言「整键在前、前缀在后」这条分层不变量。 +void run_autocorrect_context_layering_tests(const std::filesystem::path &data_directory) +{ + const std::filesystem::path directory = data_directory / "autocorrect-context-layering"; + std::filesystem::create_directories(directory); + { + Database database(directory / "msime.db"); + database.execute("CREATE TABLE tbl_2_b(key TEXT,jp TEXT,value TEXT,weight INTEGER);" + "INSERT INTO tbl_2_b VALUES('ban''zheng', 'bz', '办证', 100000);" + "INSERT INTO tbl_2_b VALUES('ban''zheng', 'bz', '辩证', 10);" + "INSERT INTO tbl_2_b VALUES('ban''zhang', 'bz', '班长', 5000);" + "INSERT INTO tbl_2_b VALUES('ban''zhang', 'bz', '搬账', 4000);"); + // 单字表:query_series 递减到 count==1 时查它,前缀行由此产生。单字权重取 1e6 + // 量级(真实词库里单字是语料计数),确保它们不会被 append 阶段的去重或调频挪走。 + database.execute("CREATE TABLE tbl_1_b(key TEXT,jp TEXT,value TEXT,weight INTEGER);" + "INSERT INTO tbl_1_b VALUES('ban', 'b', '办', 1000000);" + "INSERT INTO tbl_1_b VALUES('ban', 'b', '半', 900000);"); + } + + metasequoia::RuntimePaths paths; + paths.resources = directory; + paths.user_data = directory; + paths.cache = directory; + paths.dictionaries = directory; + const unsigned both = quanpin::kAutocorrectTransposition | quanpin::kAutocorrectNeighbor; + SentenceAssociationOptions lattice_only; + lattice_only.word_lattice = true; + + metasequoia::InputSession session(SchemeType::Quanpin, both, true, true, true, paths); + session.set_sentence_association(lattice_only); + type(session, "banzhng"); + + const auto &items = session.candidates(); + const auto index_of = [&items](const std::string &word) { + for (size_t i = 0; i < items.size(); ++i) + { + if (items[i].word == word) + { + return i; + } + } + return items.size(); + }; + + // 先确认 fixture 真的同时产出了整键行与单字前缀行,否则下面的分层断言是空的。 + const size_t prefix_a = index_of("办"); + const size_t prefix_b = index_of("半"); + require(prefix_a != items.size() && prefix_b != items.size(), + "The layering fixture must surface single-character prefix rows for the winner cut."); + const size_t sibling = index_of("班长"); + require(sibling != items.size(), "The layering fixture must surface the same-tier whole word."); + + require(index_of("办证") < prefix_a, "The winner cut's whole word must precede its own single-character prefixes."); + require(sibling < prefix_a && sibling < prefix_b, + "Same-tier whole words must not be pushed behind the winner cut's single-character prefixes."); + + // 分层的完整形式:所有整键行都在任何前缀行之前。全拼键用 ' 分隔音节, + // 单音节键不含 '。 + const auto is_prefix_row = [&items](size_t i) { return items[i].canonical_pinyin.find('\'') == std::string::npos; }; + bool seen_prefix = false; + for (size_t i = 0; i < items.size(); ++i) + { + if (is_prefix_row(i)) + { + seen_prefix = true; + } + else + { + require(!seen_prefix, "Every whole-key row must precede every single-character prefix row."); + } + } +} + // 阶段 2 生成式纠错空间(任务 quanpin-autocorrect-generated-space):静态表 // 形状之外的单编辑手误由生成式索引兜底,权重落贵档(15)。隔离 fixture: // - shatg = shang 的 n→t(t 非邻键,远键替换) @@ -1407,6 +1484,8 @@ int run_test() run_umlaut_alias_session_tests(data_directory); run_caret_prefix_session_tests(data_directory); run_autocorrect_context_ranking_tests(data_directory); + run_autocorrect_context_layering_tests(data_directory); + run_autocorrect_generated_space_tests(data_directory); #endif