四个搜索引擎,没一个能搜中文(实测复盘)
上周刷到一篇小众爆文:一位开发者把 SQLite、DuckDB、Tantivy、PostgreSQL 四个"嵌入式"全文检索引擎拉到同一个基准上跑 BM25,结论是 SQLite 和 DuckDB 并列第一,PostgreSQL 垫底。文章标题起得很挑衅——《不用 Elasticsearch 的搜索》。
我顺手做了两件事:第一,把它复现了一遍;第二,加了一个原作者没测的东西——中文。
第二件事的结论,一句话就能讲完:四个引擎,没一个能搜中文。
不是"搜得差",是 nDCG 直接掉到 0.004 那种级别的"搜不了"。而这四个引擎里,有的已经号称"开箱即用"好几年了。
先把复现交代了,再讲中文怎么个全军覆没法。
● ● ●
一、复现:原文是对的
原文基准用 BEIR 的 SciFact 数据集(5,183 篇科学摘要、300 条查询),比 nDCG@10。我在自己的 WSL2 环境里把四个引擎全部跑了一遍:
四位全部逐位吻合。数据可信,harness 写得也干净——这年头能复现的基准文章,值得先点个赞。
但复现通过的那一刻,问题也来了:原文的配置,是这四个引擎的实力上限吗?
不是。我改了一个参数,排名就变了。
● ● ●
二、一个参数改写排行榜
DuckDB 内置 FTS 默认带英文停用词表。把它关掉——stopwords='none'——同一个基准,同一个引擎:
nDCG@10 从 0.6808 涨到 0.6924,单独登顶。
原因不复杂:停用词表把查询里的高频词过滤掉了,但查询里那些"被停用"的词在 BM25 里本来是有贡献的(它们 IDF 低,但不是零)。关掉之后,信号全保留。
更值得说的是另一个新选手。
● ● ●
三、DuckDB 里现在长了一个 Tantivy
写这篇文章的由头之一,是 DuckDB 的社区扩展生态里现在有一个 tantivy 扩展——把 Rust 那个快得离谱的搜索引擎内核直接编译成 DuckDB 扩展,SQL 里一句 bm25_search() 就能用。
我把它拉进了同一个基准:
| DuckDB FTS(去停用词) | 0.6924 | |
| duckdb-tantivy 扩展 | 0.6839 | |
2026 年 10 月的"不用 Elasticsearch"榜单,前两名都姓 DuckDB。
但这里必须诚实交底:tantivy 扩展的延迟是 385ms,比独立 Tantivy 慢三个数量级。因为它的架构是每次查询调用都在内存里重建索引——5,183 篇文档重建一次要几百毫秒。这是设计声明的限制(定位是中小表的精确搜索层),不是 bug,但你选型时得知道。
● ● ●
四、中文:全灭
好,前面都是英文。现在轮到这篇文章真正的主角。
中文基准我选了 C-MTEB 的 CmedqaRetrieval(医疗问答检索,100,001 篇文档、3,999 条查询、带人工标注的相关性)。这是标准中文检索基准,不是我自己编的题。
先测"开箱即用"——不加分词,直接把中文文本喂进去:
十万个文档的库,3,999 条查询,99% 的查询返回空结果。
为什么会这样?我挖到了最底层:直接去查 DuckDB FTS 索引的内部词表(fts_main_t.dict),结果是这样的——
李红艳在浙江大学生物信息实验室工作 ← 这是一个"词"
浙江大学的生物信息专业很好 ← 这也是一个"词"
整句中文,被当成了一个 token。
因为这些引擎的默认分词器都按空格和标点切词——这对英文天经地义,但中文没有空格。一句 16 个字的中文,在这些引擎眼里就是一个不可拆分的巨型单词。查询"浙江大学"当然匹配不上"李红艳在浙江大学生物信息实验室工作"这个词——字面上就不相等。
这不是哪个引擎的缺陷,是所有"默认按空格切词"引擎的共同盲区。
● ● ●
五、分词之后,都活了
解法是老老实实预分词:用 jieba 把文本切成空格分隔的词,再喂给引擎。
| 0.1622 | ||
| 1.9ms | ||
*扩展的架构限制(见第三节),10 万文档每次调用重建索引。
三个观察:
- 01全部回到 0.14-0.16 档
——与这个数据集公开的 BM25 基线(约 0.15)同档。中文检索管线 sanity 成立。 - 02引擎间差距缩到 2 个点以内
。英文榜上 DuckDB 和 SQLite 差 0.1%;中文榜上决定质量的是分词,不是引擎。 - 03
Tantivy 的延迟优势跨语言成立:1.9ms,比其他引擎快 40 倍。
● ● ●
六、唯一救不回来的:PostgreSQL
有一个引擎,分词也救不了。
PostgreSQL 用 jieba 预分词 + to_tsvector('simple') + GIN 索引,nDCG@10 = 0.0159。不是 0.15,是 0.0159,比其他引擎低一个数量级。
我逐层排查了原因。把单条查询的所有命中拉出来看:OR 查询命中 96,472 篇文档,标注相关的目标文档按 ts_rank 排在第 49,652 位。而同一个查询,BM25 系引擎把它排进前 30。
根因:ts_rank 没有 IDF 分量。
"的、了、是、在"这些高频助词,在 BM25 里会被 IDF 压到近乎零权重;在 ts_rank 里它们和"睾丸炎""腮腺炎"权重一视同仁。于是一篇匹配了一堆助词的无关文档,分数高过匹配了核心症状词的目标文档。排序被虚词淹没。
回头看英文榜上 PostgreSQL 垫底(0.560 vs 0.68),同一个根因——英文的 the/of/is 一样淹没排序。这不是中文特有的问题,是 ts_rank 这个排序函数在 BM25 时代的结构性落后。中文只是把它的病放大了十倍。
顺带一提:换 AND 查询也救不了——132/150 条查询直接空结果,nDCG 掉到 0.007。PG 全文检索这条路,在 BM25 语境下可以正式排除了。
● ● ●
七、彩蛋:三条 SQL 搞定中文检索
测到最后我发现了一条最短路径。DuckDB 的 luajit 社区扩展里有一个纯 Lua 的 jieba 移植(词典 34 万条词内嵌在单个 .lua 文件里,零外部依赖),于是中文检索可以全链路不出 DuckDB:
INSTALL luajit FROM community; LOAD luajit;
SELECT * FROM luajit_module(mode := 'install', sql_name := 'jieba');
-- 切词即列变换
INSERT INTO docs SELECT id,
luajit_s('jieba', {v: body, op: 'join', sep: ' '}) ->> 'joined' FROM raw;
PRAGMA create_fts_index('docs', 'id', 'body');
-- 查询也用同一个分词器
SELECT id FROM (SELECT id, fts_main_docs.match_bm25(id,
(SELECT luajit_s('jieba', {v: ?, op: 'join', sep: ' '}) ->> 'joined'),
conjunctive := 0) s FROM docs)
WHERE s IS NOT NULL ORDER BY s DESC LIMIT 100;
实测:nDCG@10 = 0.1380,与 Python 外置分词管道(0.1385)打平——0.0005 的差距来自分词模式细节(Lua 版是精确模式,Python 版是搜索引擎模式),不是引擎。查询延迟同为 78ms。
代价只有一次性切词时间:10 万文档 411 秒(约 4ms 一篇)。
对"不想在数据库旁边再挂一个 Python 进程"的场景,这是目前部署摩擦最小的中文检索路线:两个扩展、三条 SQL,完事。
● ● ●
八、选型表(2026-10 版)
把 20 个配置压成一张决策表:
- 英文、要质量
→ DuckDB 内置 FTS,记得关停用词(0.692) - 英文、要延迟和体积
→ Tantivy(0.31ms / 2.8MB) - 中文、数据在 DuckDB 里、不想加管道
→ luajit jieba + 内置 FTS(0.138,全 SQL) - 中文、要极致质量
→ jieba 分词 + tantivy 系(0.144-0.162) - 中文、没有 DuckDB
→ SQLite FTS5 + jieba(0.148,标准库就行) - 任何语言、都别用
→ PostgreSQL 原生全文检索(英文 0.560,中文 0.016,同根因:ts_rank 无 IDF)
● ● ●
尾声
这次调研的全部数据、复现脚本和中文基准 harness 都开源在 github.com/alitrack/fts-bench,四引擎英文复现、DuckDB 生态新选项、10 万文档中文基准、luajit 全 SQL 路线,一键可跑。
最后留一个开放的判断:四个引擎原生中文全灭,说明"支持中文"这件事,在嵌入式检索引擎的世界里还没有像 jieba-rs 这样的默认答案。谁先把中文分词做进引擎默认 tokenizer——比如 tantivy 的 Rust 生态其实已经有 jieba-rs——谁就能拿下"开箱即搜中文"这个空着的位子。
这不是预测,是一个待领取的 PR。
● ● ●
参考来源
- 01
基准仓库与配套文章:https://github.com/nazmul284/search-without-elasticsearch - 02
本文全部实测数据与复现脚本:https://github.com/alitrack/fts-bench - 03
BEIR SciFact 数据集(BEIR 官方分发) - 04
C-MTEB CmedqaRetrieval 中文检索基准(Hugging Face: C-MTEB) - 05
jieba 中文分词(MIT 协议):https://github.com/fxsjy/jieba - 06
Tantivy 全文检索库:https://github.com/quickwit-oss/tantivy - 07
DuckDB luajit 扩展与 luajit-libs(jieba 的纯 Lua 移植):https://github.com/alitrack/duckdb-luajit-libs