alitrack

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