Skip to content

Commit ed0d168

Browse files
committed
perf(dict): 简拼查询改为精确长度匹配,消除短简拼卡顿
LookupAbbrev 旧实现用 PrefixCollect 收集整棵 code* 子树,短简拼 (如 sf)需扫描数千叶子 / 上万词条再排序截断,造成 50~100ms 卡顿, 且会把更长简拼词(sf→sfg 三字)当噪声召回,不符合"N 声母 = N 字"语义。 改为 ExactMatch 精确匹配,与 binformat DictReader.LookupAbbrev 行为对齐。 新增 TestWdatReader_LookupAbbrevExactNoPrefix 锁定精确匹配语义。
1 parent 18efb8d commit ed0d168

2 files changed

Lines changed: 48 additions & 24 deletions

File tree

wind_input/internal/dict/datformat/dat_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,43 @@ func TestWdatReader_RoundTrip(t *testing.T) {
320320
}
321321
}
322322

323+
// TestWdatReader_LookupAbbrevExactNoPrefix 验证简拼查询为精确长度匹配,不展开
324+
// code* 前缀子树:LookupAbbrev("nh") 只返回 abbrev=="nh" 的词,不召回更长简拼
325+
// "nhh" 的词。锁定性能修复后的语义(旧 PrefixCollect 实现下本用例会返回 2 条而失败)。
326+
func TestWdatReader_LookupAbbrevExactNoPrefix(t *testing.T) {
327+
w := NewWdatWriter()
328+
w.AddCode("nihao", []WdatEntry{{Text: "你好", Weight: 200}})
329+
w.AddAbbrev("nh", []WdatEntry{{Text: "你好", Weight: 200}})
330+
w.AddAbbrev("nhh", []WdatEntry{{Text: "你好哈", Weight: 100}})
331+
332+
path := writeTestWdat(t, w)
333+
r, err := OpenWdat(path)
334+
if err != nil {
335+
t.Fatalf("OpenWdat: %v", err)
336+
}
337+
defer r.Close()
338+
339+
// "nh" 只精确命中两字词,不展开到 "nhh"
340+
cands := r.LookupAbbrev("nh", 0)
341+
if len(cands) != 1 {
342+
t.Fatalf("LookupAbbrev(nh): 精确匹配应只返回 1 条,got %d", len(cands))
343+
}
344+
if cands[0].Text != "你好" {
345+
t.Errorf("LookupAbbrev(nh)[0] = %q, want 你好", cands[0].Text)
346+
}
347+
348+
// 更长简拼仍能被自身精确命中
349+
cands = r.LookupAbbrev("nhh", 0)
350+
if len(cands) != 1 || cands[0].Text != "你好哈" {
351+
t.Fatalf("LookupAbbrev(nhh): want [你好哈],got %v", cands)
352+
}
353+
354+
// 不存在的简拼返回空
355+
if cands = r.LookupAbbrev("nx", 0); len(cands) != 0 {
356+
t.Errorf("LookupAbbrev(nx): want 0, got %d", len(cands))
357+
}
358+
}
359+
323360
func TestWdatReader_PrefixCursor(t *testing.T) {
324361
w := NewWdatWriter()
325362
codes := []string{"sa", "sai", "san", "sang", "she", "shi", "shou", "si", "song", "su"}

wind_input/internal/dict/datformat/reader.go

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -285,38 +285,25 @@ func (r *WdatReader) hotPrefixSlice(b byte, limit int) []candidate.Candidate {
285285
return out
286286
}
287287

288-
// LookupAbbrev 简拼查找
288+
// LookupAbbrev 简拼查找(精确长度匹配)。
289+
//
290+
// 简拼 code 的声母数即目标词字数(如 "nh"=2 声母→两字词),因此只做 ExactMatch,
291+
// 不展开 code* 前缀子树。旧实现用 PrefixCollect(code, 0) 收集整棵子树的全部叶子,
292+
// 对短简拼(如 "sf")会扫描数千叶子 / 上万词条再排序截断,造成 50~100ms 卡顿;
293+
// 且把 "sf" 召回 "sfg"(三字) 等更长简拼词,既是噪声也不符合主流"N 声母 = N 字"语义。
294+
// 与 binformat DictReader.LookupAbbrev 的精确匹配行为对齐。
289295
func (r *WdatReader) LookupAbbrev(code string, limit int) []candidate.Candidate {
290296
if !r.hasAbbrev {
291297
return nil
292298
}
293299
dat := r.abbrevDAT()
294-
leafIndices := dat.PrefixCollect(code, 0)
295-
296-
// 也尝试精确匹配
297-
if leafIdx, found := dat.ExactMatch(code); found {
298-
// 去重:精确匹配的 leafIdx 可能已在 PrefixCollect 中
299-
has := false
300-
for _, idx := range leafIndices {
301-
if idx == leafIdx {
302-
has = true
303-
break
304-
}
305-
}
306-
if !has {
307-
leafIndices = append([]uint32{leafIdx}, leafIndices...)
308-
}
309-
}
310-
311-
if len(leafIndices) == 0 {
300+
leafIdx, found := dat.ExactMatch(code)
301+
if !found {
312302
return nil
313303
}
314304

315-
var all []candidate.Candidate
316-
for _, leafIdx := range leafIndices {
317-
leaf := r.readLeaf(r.abbrevLeafBase, leafIdx)
318-
all = r.appendEntries(all, r.abbrevEntryBase, leaf, code)
319-
}
305+
leaf := r.readLeaf(r.abbrevLeafBase, leafIdx)
306+
all := r.appendEntries(nil, r.abbrevEntryBase, leaf, code)
320307

321308
sort.Slice(all, func(i, j int) bool {
322309
return candidate.Better(all[i], all[j])

0 commit comments

Comments
 (0)