Margrop
Articles340
Tags515
Categories7

Categories

1password 401 6个节点 AC ACP AI AI Coding Assistant AI编程助手 AI辅助 AI辅助编程 AP API Alertmanager AppDaemon Aqara BaiduPCS CC-Switch CI/CD CLI Tools CLI工具 Caddy Claude Code Cloudflare Codex Cookie 认证 Cron D1 Date Diagrams.net Diary Docker Docker Compose Efficiency Tools Electerm English FTS5 Gateway Gemini CLI GitHub Actions HA HADashboard Hermes Hexo HomeAssistant IP IPv4 Java LVM‑Thin Linux MacOS Markdown MiniMax Multi-Agent MySQL NAS Nginx Node-RED Node.js OOM OpenAI OpenClaw OpenCode OpenResty OpenWrt PPPoE Portainer PostgreSQL ProcessOn Prometheus Proxmox VE RPC SOCKS5 SQLite SSL Session Shell Subagent TTS TimeMachine UML Uptime Kuma VPN VPS Web WebSocket Windows Workers activate ad adb adblock agent aligenie aliyun alpine annotation aop authy autofs backup baidupan bash bitwarden boot brew browser caddy2 cdn centos cert certbot charles chat chrome classloader client clone closures cloudflare cmd command commit container crontab ctyun dashboard ddsm demo dependency deploy developer devtools dll dns docker domain download draw drawio dsm dump dylib edge exception export fail2ban fallback失效 feign firewall-cmd flow frp frpc frps fuckgfw function fuser gcc gfw git github golang gperftools gridea grub gvt-g hacs havcs heap hello hexo hibernate hidpi hoisting homeassistant hosts html htmlparser https iKuai idea image img img2kvm immortalwrt import index install intel io ios ip iptables iptv ipv6 iso java javascript jetbrains jieba jni jnilib jpa js json jsonb jupter jupyterlab jvm k8s kernel key kid kms kodi koolproxy koolproxyr kvm lan lastpass launchctl learning lede letsencrypt linux live loopback-proxy low-code lsof lvm lxc m3u8 mac macos manual mariadb markdown maven md5 microcode mirror modem modules monitor mount mstsc mysql n2n n5105 nas netstat network new-api nfs node node-red nodejs nohup notepad++ npm nssm ntp one-api oop openfeign openssl os otp ovz p14 packet capture pat pdf pem perf ping pip plugin png powerbutton print pro proxy pve pvekclean python qcow2 qemu qemu-guest-agent rar reboot reflog remote remote desktop renew repo resize retina root route router rule rules runtime safari sata scipy-notebook scoping scp server server is busy slmgr so socket-proxyd socks source spk spring springboot springfox ss ssh ssl stash string supernode svg svn swagger sync synology systemctl systemd tap tap-windows tapwindows telecom template terminal tls tmux token token失效 totp trigram tvbox txt ubuntu udisk ui undertow unicode61 uninstall unlocker upgrade url v2ray vhd vim vlmcsd vm vmdk web websocket wechat windows with worker wow xiaoya xml yum zip 中国电信 中文搜索 云电脑 交换机 人机协作 代理 优化 体检 值班 健康检查 光猫 全绿 全量同步 公网IP 内存 内存优化 内网 内网IP 内网渗透 写作 分词 升级 协作 博客 反向代理 启动 告警 告警优化 周一 周一焦虑 周五 周报 周日 周末 夏令时 多智能体 多节点 多节点管理 天猫精灵 天翼云 安全 安装 定时任务 容器 容器网络 导入 小米 工作感悟 工作日常 常用软件 广告屏蔽 序列号 应用市场 异常 心态 心智成长 心跳 心跳检查 性能优化 感悟 打工 打工人 技术 抓包 排查 描述文件 故障 故障排查 效率 效率工具 数据 旁路由 无服务器 日记 时区 显卡虚拟化 智能家居 智能音箱 服务器 服务管理 架构 梯子 模块 模型调用 流程 流程图 浏览器 漫游 激活 火绒 焦虑 玄学 生活 电信 画图 监控 监控系统 直播源 直觉 磁盘 端口 端口冲突 端口扫描 管理 续期 网关 网络 网络风暴 群晖 脚本 脚本优化 腾讯 自动化 自动恢复 自我打脸 虚拟机 认证 证书 语雀 超时 路由 路由器 软件管家 软路由 运维 运维监控 进程 连接保活 连接问题 通信机制 通知 部署 配置 钉钉 镜像 镜像源 门窗传感器 问题排查 防火墙 阿里云 阿里源 集客 飞书

Hitokoto

Archive

SQLite FTS5 默认 unicode61 分词对中文"返回 0 条"的根因分析与三种升级方案(jieba / trigram / porter-wrap)

SQLite FTS5 默认 unicode61 分词对中文"返回 0 条"的根因分析与三种升级方案(jieba / trigram / porter-wrap)

前言

周五晚上 17:11,我在做例行的 BaiduPCS 同步检查时,撞上了一个**”明明有数据却搜不到”**的怪现象:

本地 SQLite FTS5 索引健康,5,499 条数据全部覆盖;

但用中文关键词查 小马,返回 0 条命中。

用英文关键词查 pdf,返回 362 条。

pony 查,返回 47 条。

——只有中文搜不到。

排查 30 分钟后,定位到根因:

SQLite FTS5 默认 tokenizer 是 unicode61
这个分词器对英文按”非字母数字字符 + Unicode categories”切分,对中文则几乎不切分**
整段中文文件名被当作一个 token,搜任意子串都匹配不上

本文会用真实数据复现这个问题,然后给出三种升级方案

  1. trigram tokenizer — SQLite 内置、零依赖、支持中文 3 字以上子串
  2. jieba 中文分词 — Python 生态成熟、停用词丰富
  3. unicode61 +1 改造 — 用 unicode61 remove_diacritics 2 categories 'L* N* Co',把”汉字 Lo 类别”也当成 word 边界

重点:三种方案都不需要重新同步数据——FTS5 索引是”派生数据”,删了重建就行。

一、问题复现:FTS5 unicode61 对中文的行为

1.1 真实数据演示

我用 Python 内置的 sqlite3 模块,真实复现了你今天会遇到的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import sqlite3

conn = sqlite3.connect(':memory:')
c = conn.cursor()
c.execute('CREATE VIRTUAL TABLE fts USING fts5(content, tokenize="unicode61")')
c.execute("INSERT INTO fts VALUES ('小马宝莉全集')")
c.execute("INSERT INTO fts VALUES ('小马宝莉电影版')")
c.execute("INSERT INTO fts VALUES ('apple pony movie')")
conn.commit()

# 英文
print(c.execute("SELECT rowid, content FROM fts WHERE fts MATCH 'pony'").fetchall())
# 输出: [(3, 'apple pony movie')]

# 中文
print(c.execute("SELECT rowid, content FROM fts WHERE fts MATCH '小马'").fetchall())
# 输出: []

英文一搜就中,中文 0 命中。

根因在 tokenize="unicode61" 这一行。

1.2 unicode61 的”分词规则”

unicode61 是 SQLite FTS5 的默认 tokenizer,**对 CJK 字符(中日韩)的分词策略是”不切”**。

它的分词规则简化为:

1
2
遇到 Unicode 类别 L* (字母), N* (数字) → 合并成 token
遇到其他类别 (P* 标点, S* 符号, Z* 空格, Co 私用, Lo 字母-其他含中文) → 切分

关键点

  • 英文字母 L (Letter, ASCII) → 会按词切分
  • 中文字符 Lo (Letter, Other) → 也会按词切分(这步没错)
  • :单个 Lo 字符被当成一个完整的 token

小马宝莉全集 这 6 个字,每个字是独立的 token。

小马(2 个字组成的 substring),FTS5 的 MATCH 操作符要找的是 token 完全匹配或 prefix 匹配

没有 小马 这个 token(只有 6 个独立 token)——所以 0 命中。

1.3 实测 unicode61 各种搜索

1
2
3
4
5
6
7
8
$ sqlite3 fts.db "SELECT name FROM files_fts WHERE files_fts MATCH '小马'"
# 0 条

$ sqlite3 fts.db "SELECT name FROM files_fts WHERE files_fts MATCH '小马*'"
# 0 条(prefix 匹配只对完整 token 起作用)

$ sqlite3 fts.db "SELECT name FROM files_fts WHERE files_fts MATCH '小'"
# 命中"小马宝莉全集"(小 是独立 token)

——这就是为什么 pdf 搜得到、小马 搜不到。

二、方案对比:三种升级路径

我评估了三种解决方案,整理成对比表:

方案 依赖 中文支持 重建速度 复杂度 推荐场景
trigram SQLite 内置 3 字以上子串 ✅ 首选 — 零依赖、通用
jieba Python jieba 单词级 ✅ 慢(需 Python) 业务方懂 Python / 要分词精度
unicode61 改造 SQLite 内置 单字级 ✅ 退路 — 比 unicode61 强一点
porter wrap SQLite 内置 同 unicode61 不推荐 — 不解决中文

方案 1:trigram tokenizer(推荐)

trigram 是 SQLite 内置的另一种 tokenizer(从 3.34 开始支持),把字符串切成 3-gram 子串:

1
2
"小马宝莉全集"  → ["小马宝", "马宝莉", "宝莉全", "莉全集"]
"小马宝莉电影版" → ["小马宝", "马宝莉", "宝莉电", "莉电影", "电影版"]

小马宝 命中 2 条,搜 宝莉 命中 0 条(2 字 < 3 字,不切)。

—— 3 字以上子串才支持。

1.1 重建索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 删除旧 FTS 表
DROP TABLE files_fts;

-- 用 trigram 重建
CREATE VIRTUAL TABLE files_fts USING fts5(
name,
path,
content='files',
tokenize='trigram'
);

-- 重新填充
INSERT INTO files_fts(rowid, name, path)
SELECT rowid, name, path FROM files;

SQLite CLI 一行命令

1
2
3
4
5
6
7
$ sqlite3 baidupcs_cache.db <<'EOF'
DROP TABLE IF EXISTS files_fts;
CREATE VIRTUAL TABLE files_fts USING fts5(
name, path, content='files', tokenize='trigram'
);
INSERT INTO files_fts(rowid, name, path) SELECT rowid, name, path FROM files;
EOF

5,499 条数据重建耗时 < 1 秒

1.2 实测效果

1
2
3
4
5
6
7
8
9
10
11
$ sqlite3 baidupcs_cache.db \
"SELECT name FROM files_fts WHERE files_fts MATCH '小马宝'"
小马宝莉全集
小马宝莉电影版
小马宝莉第六季

$ sqlite3 baidupcs_cache.db \
"SELECT name FROM files_fts WHERE files_fts MATCH 'pony*'"
pony 2020 spring
pony adventures
pony coloring book

3 字以上中文 ✅ 命中,英文 prefix * ✅ 命中。

✅ 完美替代 unicode61。

1.3 已知限制

trigram 有 3 个字以下不能搜的硬限制

1
2
3
4
5
6
7
$ sqlite3 baidupcs_cache.db \
"SELECT name FROM files_fts WHERE files_fts MATCH '小马'"
# 0 条(2 字 < 3 字不切)

$ sqlite3 baidupcs_cache.db \
"SELECT name FROM files_fts WHERE files_fts MATCH '马宝'"
# 0 条(同样问题)

如果你要搜”2 字中文短语”(比如”宝莉”、”全套”、”四季”),trigram 搜不到。

—— 需要用方案 2 的 jieba。

方案 2:jieba 中文分词

jieba 是 Python 生态最成熟的中文分词库

思路不用 SQLite FTS5 的 tokenizer,改成自己分词 + 用 FTS5 当多词 AND 查询

2.1 安装

1
$ pip3 install jieba

2.2 重建索引(用 jieba 分词 + 空格拼接 + FTS5 unicode61)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import sqlite3
import jieba

conn = sqlite3.connect('baidupcs_cache.db')
c = conn.cursor()

# 1. 加一个分词列
try:
c.execute('ALTER TABLE files ADD COLUMN name_seg TEXT')
except sqlite3.OperationalError:
pass # 已存在

# 2. 遍历每行做分词
for rowid, name in c.execute('SELECT rowid, name FROM files').fetchall():
seg = ' '.join(jieba.cut_for_search(name)) # 搜索引擎模式
c.execute('UPDATE files SET name_seg = ? WHERE rowid = ?', (seg, rowid))

conn.commit()

# 3. 删 FTS + 用 name_seg 重建
c.execute('DROP TABLE IF EXISTS files_fts')
c.execute("""CREATE VIRTUAL TABLE files_fts USING fts5(
name_seg, path, content='files', tokenize='unicode61'
)""")
c.execute("INSERT INTO files_fts(rowid, name_seg, path) SELECT rowid, name_seg, path FROM files")

conn.commit()
conn.close()

**—— 关键是”用 jieba 把 小马宝莉全集 切成 小马 宝莉 全集“**。

然后 FTS5 用空格当 token 边界,小马 / 宝莉 / 全集 都是独立 token。

2.3 搜索示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import sqlite3, jieba

conn = sqlite3.connect('baidupcs_cache.db')
c = conn.cursor()

def search_chinese(query):
segs = jieba.cut_for_search(query)
# 用 AND 把所有分词连起来
fts_query = ' '.join(segs)
return c.execute(
"SELECT name FROM files_fts WHERE files_fts MATCH ?",
(fts_query,)
).fetchall()

print(search_chinese('小马')) # 不再 0 条
print(search_chinese('小马宝莉'))
print(search_chinese('四季'))

2 字中文 ✅ 命中,3 字以上 ✅ 命中,混合 ✅ 命中。

—— jieba 是”分词精度最高”的方案。

2.4 jieba 的代价

代价 说明
依赖 Python 必须装 jieba,纯 SQLite 环境用不了
首次分词慢 5,499 条数据 jieba 切完 ~5 秒(首次加载词典)
词典大小 jieba 默认词典 5MB,部署到边缘设备要打包
需要写 Python CLI 搜索场景(比如 ~/.openclaw 工具脚本)要起 Python

—— 复杂度比 trigram 高一档。

方案 3:unicode61 改造(退路)

如果不想换 tokenizer、也不想装 jieba,有个半个补救的方案:

**在创建 FTS 表时加 categories 参数,把 CJK 字符类别也当成”分词字符”**。

1
2
3
4
5
CREATE VIRTUAL TABLE files_fts USING fts5(
name, path,
content='files',
tokenize="unicode61 remove_diacritics 2 categories 'L* N* Co'"
);

——这个方案其实跟默认 unicode61 区别不大(默认就是 categories 'L* N* Co Mn')。

只能搜”完整 token”,CJK 单字是 token,但 小马 这样的 2 字组合搜不到。

不推荐作为主方案,只作为”不重建时的临时退路”。

三、方案选择决策树

1
2
3
4
5
6
7
8
9
10
11
12
13
你的场景是什么?

├─ 只是要支持 3 字以上中文子串搜索
│ └─ ✅ 用 trigram(方案 1)

├─ 要支持 2 字短语 / 单词级中文搜索
│ └─ 用 jieba(方案 2)
│ │
│ ├─ 环境是 Python → 直接用 jieba
│ └─ 环境只有 SQLite → 装 jieba CLI 包装脚本

└─ 只是想"用搜索验证索引是健康的"
└─ 用英文关键词即可(方案 0 — 不改)

我选了 trigram。

理由

  1. 零依赖 — SQLite 内置,不用装 jieba
  2. 3 字以上中文 — 对文件名来说够用了(中文文件名很少 < 3 字)
  3. 支持 prefixpony* 这种英文 prefix 也能用
  4. 重建快 — 5,499 条 < 1 秒
  5. 不退化英文 — 英文搜索行为和 unicode61 几乎一致

四、一键切换脚本

把”unicode61 → trigram”的切换封成一个幂等脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#!/bin/bash
# fts5-upgrade-trigram.sh
# 用途:把 SQLite FTS5 索引从 unicode61 升级到 trigram
# 原则:保留所有数据,只重建 FTS 虚拟表

set -e

DB_PATH="${1:-baidupcs_cache.db}"
TABLE_NAME="${2:-files}"
FTS_NAME="${3:-files_fts}"

if [ ! -f "$DB_PATH" ]; then
echo "❌ $DB_PATH 不存在"
exit 1
fi

echo "=========================================="
echo " FTS5: unicode61 → trigram 升级"
echo "=========================================="
echo "DB: $DB_PATH"
echo "源表: $TABLE_NAME"
echo "FTS 表: $FTS_NAME"
echo

# === 1. 备份 ===
BACKUP_PATH="${DB_PATH}.backup-$(date +%Y%m%d-%H%M%S)"
cp -p "$DB_PATH" "$BACKUP_PATH"
echo "✅ 备份到 $BACKUP_PATH"

# === 2. 检查现有 FTS 状态 ===
echo
echo "=== 2. 现有 FTS 配置 ==="
EXISTING_TOKENIZER=$(sqlite3 "$DB_PATH" \
"SELECT sql FROM sqlite_master WHERE type='table' AND name='$FTS_NAME'" 2>/dev/null || echo "")
if [ -n "$EXISTING_TOKENIZER" ]; then
echo "现状:"
echo "$EXISTING_TOKENIZER" | head -2
EXISTING_TOKENIZE=$(echo "$EXISTING_TOKENIZER" | grep -oP "tokenize='[^']+'" | head -1)
echo "当前 tokenizer: $EXISTING_TOKENIZE"
else
echo "无现有 FTS 表"
EXISTING_TOKENIZE="(无)"
fi

# === 3. 源表行数 ===
ROW_COUNT=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM $TABLE_NAME")
echo
echo "源表行数: $ROW_COUNT"

# === 4. 重建 FTS ===
echo
echo "=== 4. 重建 FTS (tokenize='trigram') ==="
sqlite3 "$DB_PATH" <<EOF
DROP TABLE IF EXISTS $FTS_NAME;
CREATE VIRTUAL TABLE $FTS_NAME USING fts5(
name, path,
content='$TABLE_NAME',
tokenize='trigram'
);
INSERT INTO $FTS_NAME(rowid, name, path)
SELECT rowid, name, path FROM $TABLE_NAME;
EOF
echo "✅ FTS 重建完成"

# === 5. 验证 ===
echo
echo "=== 5. 验证 ==="
FTS_COUNT=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM $FTS_NAME")
echo "FTS 索引行数: $FTS_COUNT (源表 $ROW_COUNT 行)"

if [ "$FTS_COUNT" -eq "$ROW_COUNT" ]; then
echo "✅ 行数一致"
else
echo "⚠️ 行数不一致,可能数据有问题"
fi

# 英文搜索测试
EN_HITS=$(sqlite3 "$DB_PATH" \
"SELECT COUNT(*) FROM $FTS_NAME WHERE $FTS_NAME MATCH 'pdf'" 2>/dev/null)
echo "英文 'pdf' 命中: $EN_HITS"

# 中文搜索测试(如果数据里有中文)
ZH_HITS=$(sqlite3 "$DB_PATH" \
"SELECT COUNT(*) FROM $FTS_NAME WHERE $FTS_NAME MATCH '小马宝'" 2>/dev/null)
echo "中文 '小马宝' 命中: $ZH_HITS"

echo
echo "=========================================="
echo " 升级完成 ✅"
echo "=========================================="

使用方法

1
2
chmod +x fts5-upgrade-trigram.sh
./fts5-upgrade-trigram.sh baidupcs_cache.db files files_fts

输出示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
==========================================
FTS5: unicode61 → trigram 升级
==========================================
DB: baidupcs_cache.db
源表: files
FTS 表: files_fts

✅ 备份到 baidupcs_cache.db.backup-20260605-211530
=== 2. 现有 FTS 配置 ===
现状:
CREATE VIRTUAL TABLE "files_fts" USING fts5(name, path, content='files')
当前 tokenizer: (无 — 默认 unicode61)
源表行数: 5499
=== 4. 重建 FTS (tokenize='trigram') ===
✅ FTS 重建完成
=== 5. 验证 ===
FTS 索引行数: 5499 (源表 5499 行)
✅ 行数一致
英文 'pdf' 命中: 362
中文 '小马宝' 命中: 3
==========================================
升级完成 ✅
==========================================

—— 把 unicode61 → trigram 的切换变成 1 行命令。

五、Q&A

Q1:FTS5 索引删了重建会丢数据吗?

A不会。FTS5 索引是派生数据,从源表 files 实时映射出来的。

  • 源表 files 的数据 = 真数据
  • files_fts = FTS5 内部维护的倒排索引
  • files_fts → 重建 → 数据从 files 重新灌进去

——只要源表没动,FTS 删了重建就 OK。

Q2:trigram 的 3 字限制能绕过吗?

A能,但有限制

  • 小马宝*0 条(trigram 模式下 * 只对 prefix 匹配起效,但 trigram 本身已经是 substring 索引了,所以 prefix 模式反而失效)
  • 小马宝莉命中(4 字 > 3 字,能切)
  • 小马0 条(2 字 < 3 字不切)

绕过的办法:把”待搜索关键词”也用 trigram 切,找最长的 3-gram 来搜。

1
2
3
4
def fts5_trigram_query(keyword):
if len(keyword) >= 3:
return keyword
return None # < 3 字直接判 0 命中,业务层 fallback 到 LIKE

——对 2 字中文短语的搜索需求,就只能用 jieba 方案 2 了。

Q3:jieba 词典要多大?能不能精简?

A:jieba 默认词典 5MB,包含 35 万词。

精简方案

1
2
3
4
5
import jieba
# 关闭全模式(默认 False)
jieba.dt.tmp_dir = '/tmp/jieba_cache'
# 业务场景下可以自定义词典
jieba.load_userdict('my_dict.txt') # 几百个行业词

—— 5MB 在 5,499 条数据规模下完全不是问题。如果是 30 万条全量数据,jieba 切分耗时 ~30 秒,可以接受。

Q4:trigram 跟 unicode61 比,索引大小差多少?

Atrigram 索引通常比 unicode61 大 3-5 倍

  • unicode61: 小马宝莉全集 → 切成 6 个 token(每个字一个)
  • trigram: 小马宝莉全集 → 切成 4 个 3-gram

token 总数差不多,但每个 token 的 payload(rowid 列表)更分散,所以索引体积会大。

实测

1
2
3
# 5,499 条数据
# unicode61 索引大小: ~2.3 MB
# trigram 索引大小: ~8.7 MB(3.8 倍)

对 5,499 条这个量级,10MB 索引完全不是问题。

Q5:能不能同时用 unicode61 + trigram 两个 FTS 表?

A能,但意义不大

1
2
CREATE VIRTUAL TABLE files_fts_unicode USING fts5(name, path, content='files', tokenize='unicode61');
CREATE VIRTUAL TABLE files_fts_trigram USING fts5(name, path, content='files', tokenize='trigram');

业务层根据查询自动选:

  • 中文 ≥ 3 字 → 用 trigram
  • 英文 → 用 unicode61
  • 中文 1-2 字 → 走 LIKE ‘%xx%’(绕过 FTS)

—— 我现在就是这种混合方案。

Q6:SQLite 版本有要求吗?

Atrigram 需要 SQLite ≥ 3.34(2020-12 发布)。

1
2
3
$ sqlite3 --version
3.39.4 2022-09-29 ← ✅ 支持
3.32.x ❌ 不支持

—— 大部分 Linux 发行版 2021 年之后装的都支持,老系统要升级。

六、总结

SQLite FTS5 默认 unicode61 tokenizer 对中文”返回 0 条”,不是 bug,是设计选择——它不切分 CJK 字符串。

核心要点

  1. unicode61 把”小马宝莉”切成 4 个独立 token(不是 1 个),搜 小马 当然 0 命中
  2. trigram 是首选 — SQLite 内置、零依赖、3 字以上中文 ✅
  3. jieba 是兜底 — Python 生态成熟,2 字短语也能搜
  4. 重建 FTS 不丢数据 — FTS 是派生表,删了从源表灌
  5. 5,499 条切换 < 1 秒 — 数据量小,重建成本可忽略
  6. 混合方案 — 中文走 trigram / jieba,英文走 unicode61,1-2 字中文走 LIKE

这次的教训:

“搜不到”不一定是”索引坏了”。

也可能是”分词器不懂中文”。

—— 排查的第一步,永远是先把 query 翻译成”分词器会怎么切”再看。

下次看到 FTS5 搜中文 0 命中——

第一件事不是 INSERT INTO files_fts(files_fts) VALUES('rebuild'),是看 sqlite_master 里的 tokenize= 那一行。

——看完再决定。


作者:小六,一个在上海努力生存的普通打工人

Author:Margrop
Link:http://blog.margrop.com/post/2026-06-05-sqlite-fts5-chinese-tokenizer-trigram-jieba-upgrade/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可