duckdb-tantivy:10 万行 0.7 秒建索引,查询毫秒级
DuckDB 里做全文检索,以前的选项要么把数据导出去喂给专门的搜索引擎,要么在内存里临时建一次索引、查一次就扔。duckdb-tantivy 的 v0.3.0 把第二条路修成了正路:索引建一次,落在磁盘上,之后每次查询都是毫秒级。
10 万行中文语料,建索引 0.7 秒,此后单次查询毫秒级。对照旧版每次查询重建索引的 0.6 秒,是两个数量级的差距。Rust 写的原生扩展,MIT 协议,四个平台的产物都在 release 页。
● ● ●
先看它解决什么问题
duckdb-tantivy v0.3.0 四组能力
v0.2 的 bm25_search 是"一次性"的:每次调用都在内存里把整张表重新索引一遍,搜完就扔。单发查询无所谓,多跳检索那种一轮循环几十次查询的场景就直接不可用了——0.6 秒乘以几十次,用户等不起。
v0.3.0 加了持久命名索引:从表建一次索引,写到磁盘目录(mmap 读,没有后台进程),注册进当前进程。之后查多少次都不再碰那张表。
-- 建索引(建一次)
SELECT * FROM tantivy_create_index('news', 'articles',
id_column := 'id', text_column := 'body',
language := 'chinese', path := '/data/idx/news');
-- 查询(毫秒级,查多少次都行)
SELECT doc_id, score FROM tantivy_search_index('news', '苹果 股票', top_k := 5);
-- 看当前有哪些索引
SELECT * FROM tantivy_list();
两个容易被忽略的细节,都值得单独说。
tantivy_create_index 是重建语义(CREATE OR REPLACE):再次对同名索引调用时,索引目录先清空再重建,不会静默追加。旧数据不会被残留。
tantivy_search_index 的结果和 bm25_search 逐位一致。同一批数据、同样的参数,两条路给出的 (doc_id, score) 完全相同——包括同分时按 doc_id 升序的排序规则。迁移不需要重算基准。
● ● ●
索引搬家:save 和 load
索引建好之后是磁盘上的一个目录,tantivy_save_index 把它拷贝到任意路径(目标非空会拒绝,永远不覆盖),tantivy_load_index 在任何进程里只读打开并注册。两个 bit-identical 保证:搜索结果和 bm25_search 一致,跨进程 save → load 后结果一致。
这个组合的用途比看起来广:笔记本上建好索引拷到服务器,或者把索引和数据文件一起归档,换台机器直接加载,不用重新索引。
● ● ●
中文:jieba 分词,不是逐字
language := 'chinese'(别名 zh、中文)路由到 jieba 分词。中文没有词干变形,所以管线是"分词 + 小写化",不做 Snowball 那套词形归并。
SELECT tokenize('浙江大学邱建 crater 星系团', 'chinese');
-- [浙江大学, 邱建, crater, 星系团] -- 按词切,不是按字切混合文本正常工作:汉字串按词切分,拉丁字母和数字串小写直通(iPhone 索引成 iphone)。一点要留意的:默认查询语义是 AND(全词共现),拿一整句中文问题当查询会 0 命中——长查询要先拆词,或者在自己的应用层做最稀有词回退。
● ● ●
一个 benchmark 插曲,值得记下来
验收 10 万行查询时,返回的前几行是 doc 0/1/2、分数全是 0。第一反应是 bug。
工人有据反驳:那份测试语料每一行都恰好含一次"苹果"和"股票",全体文档同分,BM25 的 idf 在这种语料下趋近于零,tie-break 按 doc_id 升序返回 0/1/2 完全符合契约。换稀有词复测,df=1 的 "9699" 精准命中 doc 9699、分数 11.11。
benchmark 数字先怀疑语料,再怀疑引擎。这句教训比那天的任何代码都值钱。
● ● ●
边界在哪
诚实框,逐条说清:
- AND 语义
:整句查询必 0 命中,生产化建议应用层做 OR 或稀有词回退(SAG2 原版 FTS5 就是 OR)。 - 临时表不可见
: bm25_search看不到临时表和未提交事务(C API 回调拿不到调用方连接)。持久表和挂载表正常。 - 单次索引上限
: bm25_search每次调用最多索引 100 万行,更大的表加 WHERE 过滤。持久索引不受此限。 - 产物 unsigned
:本地加载需 duckdb -unsigned(社区扩展通道安装的版本不受影响)。
● ● ●
安装
社区扩展上架中(v0.2.0 已在社区源,v0.3.0 升级 PR 审核中),合并后一条命令:
INSTALL tantivy FROM community;
LOAD tantivy;
等不及审核的,release 页有四个平台的产物,下载后本地加载。
仓库:https://github.com/alitrack/duckdb-tantivy (MIT,Rust,v0.3.0)
多跳检索的应用场景(本地模型当裁判、证据召回 0.533 → 0.967 那篇)见前作duckdb-tantivy:中文多跳检索,裁判换成自己的。