ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

DeepSeek Harness 中文检索为什么搜不到「消耗」两个字?—— unicode61 缺陷 + trigram + LIKE 回退的完整修复

DeepSeek Harness 中文检索为什么搜不到「消耗」两个字?—— unicode61 缺陷 + trigram + LIKE 回退的完整修复 DeepSeek Harness 中文检索为什么搜不到「消耗」两个字—— unicode61 缺陷 trigram LIKE 回退的完整修复️标签#DeepSeek-Harness #FTS5 #中文全文检索 #trigram #SQLite关键词DeepSeek Harness 中文检索, FTS5 unicode61, trigram tokenizer, LIKE 回退, sessionQuery, CJK 分词摘要DeepSeek Harness 的会话全文检索sessionQuery用 SQLite FTS5 的unicode61tokenizer对连续汉字不切词——整段中文被当成一个 token且查询被整体包成单短语。结果是Token消耗、索引优化这类中文子串查询必然 0 命中。本文从源码机制讲清楚为什么给出可复现的 node:sqlite 验证并分享一套trigram 双表 1–2 字 LIKE 回退的完整修复含通配符转义与 snippet 处理。https://github.com/QIANLING-0831/dsh-memory-plus1. 现象中文子串搜索全部落空在 DSH 里搜一句旧会话里的中文比如文档索引优化减少Token消耗的句子查询Token消耗→0 命中查询索引优化→0 命中只有把完整整句原样打出来才命中英文/代码没问题中文一塌糊涂。这不是 DSH 特有的 bug而是 FTS5unicode61tokenizer 对 CJK 的固有行为 查询方式叠加的结果。2. 机制为什么必然 0 命中unicode61按 Unicode 类别把字母/数字当作词内字符对中文汉字同样如此。于是连续汉字在分词后是一个 token——索引优化减少Token消耗的句子整个是一坨被索引成一个 token索引优化减少token消耗的句子 ← 单个 tokenunicode61 视角而session-query的查询侧quoteFtsData把用户输入整体包成一个短语functionquoteFtsData(query){return${query.replaceAll(\,\\)};}短语查询要求短语的 token 序列 文档的 token 序列所以索引优化要等于整段 token 才命中——永远不相等于是0 命中。3. 可复现node:sqlite无需 DSHimport{DatabaseSync}fromnode:sqlite;constdbnewDatabaseSync(:memory:);db.exec(CREATE VIRTUAL TABLE u USING fts5(text, tokenizeunicode61));db.exec(CREATE VIRTUAL TABLE t USING fts5(text, tokenizetrigram));db.exec(INSERT INTO u(text) VALUES (索引优化减少Token消耗的句子));db.exec(INSERT INTO t(text) VALUES (索引优化减少Token消耗的句子));constn(sql,...a)db.prepare(sql).get(...a).c;console.log(unicode61 Token消耗 :,n(SELECT count(*) c FROM u WHERE u MATCH ?,Token消耗));console.log(unicode61 索引优化 :,n(SELECT count(*) c FROM u WHERE u MATCH ?,索引优化));console.log(trigram Token消耗 :,n(SELECT count(*) c FROM t WHERE t MATCH ?,Token消耗));console.log(trigram 索引优化 :,n(SELECT count(*) c FROM t WHERE t MATCH ?,索引优化));console.log(trigram 消耗(2字) :,n(SELECT count(*) c FROM t WHERE t MATCH ?,消耗));console.log(trigram 优(1字) :,n(SELECT count(*) c FROM t WHERE t MATCH ?,优));console.log(LIKE 消耗 :,n(SELECT count(*) c FROM t WHERE text LIKE ? ESCAPE \\,%消耗%));结果与 DSH 同一 SQLite 引擎查询unicode61上游trigramLIKE 回退Token消耗中英混合0✅ 命中—索引优化0✅ 命中—完整整句✅唯一方式✅—消耗/索引2 字00✅ 命中优1 字00✅ 命中关键点来了trigram 也不完美——它只索引 ≥3 字符的连续子串所以消耗、索引、优这类 1–2 字中文查询在 trigram 表上照样 0 命中。1–2 字中文恰是高频查询形态必须回退。4. 完整修复三级路由 LIKE 回退我做在开源插件dsh-session-query-sqlite-cjk里思路是双表双 tokenizer 按查询内容路由// 路由规则functionqueryMatchMode(query){if(!containsCjk(query))returnunicode61;// 纯 ASCII → 原表行为与上游一致returnArray.from(query).length3?like:trigram;// CJK3 字走 LIKE否则 trigram}含 CJK 且总长 ≥ 3→ trigram 表Token消耗的 CJK 部分只有 2 字但整体 ≥3 字符ke消是合法 trigram直接命中含 CJK 且总长 3→LIKE %词%回退ESCAPE \转义%/_/\纯 ASCII→ 原 unicode61 表英文/代码检索行为与上游完全一致零回归。4.1 LIKE 回退的三个细节通配符转义LIKE %词%里%/_是通配符查询里若含这些字符必须转义否则完%会误中完成constpattern%${query.replace(/[\\%_]/g,(ch)\\${ch})}%;// SQL: WHERE text LIKE ? ESCAPE \snippet 定位trigram 表走 FTS5highlight()自动标记命中LIKE 表没有高亮需用instr/substr手工把首个命中位置包上标记字符DSH 用\u{FDD0}/\u{FDD1}作标记这样 snippet 仍能居中定位CASEWHENinstr(text,?)0THENsubstr(text,1,instr(text,?)-1)||?||substr(text,instr(text,?),length(?))||?||substr(text,instr(text,?)length(?))ELSEtextENDASmarked_textmatch_count排序用highlight计数靠标记字符LIKE 表改按字节差统计全部出现次数(length(CAST(textASBLOB))-length(CAST(replace(text,?,)ASBLOB)))/?4.2 别忘了 FTS5 的坑LIKE 不能用裸表名WHERE 表名 LIKE ?不生效FTS5 的裸表名语法只配MATCH用必须写列名-- 错WHERE live_docs_cjk LIKE ? → 0 命中-- 对WHERE ld.text LIKE ? ESCAPE \ → 命中5. 若想合入上游三处集成点DERIVED_USER_TABLES白名单新增 FTS5 表名persisted_docs_cjk等必须同步进白名单否则已有库下次打开会被assertDerivedUserTables判为 unrecognized 拒开SCHEMA_VERSION递增加表/换 tokenizer 是不兼容变更老库靠版本不一致就地 reset 重建查询分支复用现有 persisted live 的UNION ALL第三个分支按查询路由替换 MATCH 表达式即可。另注trigram 的case_sensitive默认关闭与 unicode61 的大小写折叠一致保持默认即不回归英文/代码检索。6. 测试12 个单测trigram 命中、中英混合命中、ASCII 回退、1 字/2 字 LIKE 回退、通配符转义、短查询无命中、会话级检索、持久化会话双分支检索、无命中场景依赖包dsh-memory-index8 单测回归通过。仓库https://github.com/QIANLING-0831/dsh-memory-plus packages/dsh-session-query-sqlite-cjkMIT欢迎试用、提 issue。
返回列表