tantivy 进了 DuckDB:不建索引也能 BM25 搜索
在 SQL 里做全文搜索,以前的选项要么是把数据导去 Elasticsearch,要么在 DuckDB 里用 LIKE 硬扛。现在多了个轻量选择:tantivy 扩展进了 DuckDB 社区扩展库,一条 SQL 出 BM25 排序的结果。
底层是 tantivy,Rust 写的搜索引擎,Quickwit 背后用的就是它。这个扩展把它整个嵌进 DuckDB,通过 C Extension API 直连,不走 JSON 中转,不落盘建索引。
● ● ●
先看实测
本机 DuckDB 1.5.6,三行数据起步:
INSTALL tantivy FROM community;
LOAD tantivy;
CREATE TABLE docs(doc_id BIGINT, body VARCHAR);
INSERT INTO docs VALUES
(0, 'the cat sat'),
(1, 'a cat here'),
(2, 'the dog barked');
SELECT * FROM bm25_search('docs', 'cat');
结果只有 doc 0 和 doc 1,各 0.470003 分,含 cat 的两篇按 BM25 打分排出来,不含 cat 的 doc 2 不出现。加 top_k := 1 就只取第一名。
bm25_search 是表函数,第一个参数是表名,绑定真表真列。id 列、文本列、语言都是 named parameter 可调,默认文本列叫 body。
● ● ●
不只是搜索,还有零件
四个工具函数单独可用,这是这个扩展比「能搜」更实用的地方:
-- 分词(不带词干)
SELECT tokenize('Running quickly');
-- [running, quickly]
-- 分词 + 英文词干提取
SELECT tokenize('Running quickly', 'english');
-- [run, quick]
-- 单词词干
SELECT stem('cats', 'english');
-- cat
-- 给一句话打 BM25 分
SELECT bm25_score('the cat sat', 'cat');
-- 0.28768211603164673
词干提取走 Snowball 算法,支持 18 种语言(supported_languages() 可查全表)。搜索场景之外,这些函数拿来做 NLP 预处理、建特征列、做文本清洗都是顺手的。
● ● ●
设计上刻意收着的边界
每次 bm25_search 调用会在内存里现场建一次索引,索引上限 100 万行加文本字节上限,单条文本最长 16 MiB,top_k 限制在 1 到 10 万。超限全部在 bind 阶段报干净的 SQL 错误,不会静默截断数据,更不会崩。
这个设计划出了适用范围:百万行级以下的即席搜索,它比外部搜索引擎省事;亿级数据常驻检索,还是该上独立的搜索集群。
两个已知限制要留意:临时表和未提交的事务对 bm25_search 不可见(C API 回调拿不到调用方的连接),用永久表;自动生成的行号在表变动后不稳定,需要稳定 id 就显式传 id_column。
● ● ●
装完多大
构建产物 5.4 MB,无 Python 依赖,无 JVM。嵌入式版本号:tantivy v0.24.2,索引格式 v7。
社区收录流程已走完(PR #3032),覆盖 Linux、macOS 和 Windows 三平台的社区构建。对经常在 DuckDB 里处理文本列的人来说,这是把「排序找几条」升级成「按相关度找几条」的最低成本路径。
● ● ●
参考来源
https://github.com/duckdb/community-extensions/pull/3032
https://github.com/alitrack/duckdb-tantivy
https://github.com/quickwit-oss/tantivy