tantivy 源码解析
tantivy 是一个用 Rust 写的全文检索库。它不是服务:没有 HTTP 接口,没有集群,你在自己的进程里建索引、查索引。README 的原话是「Tantivy is closer to Apache Lucene than to Elasticsearch or Apache Solr in the sense it is not an off-the-shelf search engine server, but rather a crate that can be used to build such a search engine」。一个索引是一个目录:若干个不可变的段(segment),加一个记录「哪些段算数」的 meta.json。写入由 IndexWriter 自己的线程完成,攒一批再 commit;查询把 Query 变成 Weight,在每个段上生成 Scorer,交给 Collector 收集,打分用 BM25;读取时文件直接 mmap 进来,不需要先加载。这几句话落在哪些代码上,和 Lucene、SQLite FTS5、PostgreSQL 全文检索、meilisearch、Elasticsearch 有什么不同,见为什么是 tantivy。
锁定的版本
| 项 | 本卷 | 说明 |
|---|---|---|
| 版本 | 0.27.0 | 主 crate tantivy;工作区里的子 crate 在同一个 tag 下各有版本号(tantivy-columnar 等是 0.27.0,tantivy-tokenizer-api 是 0.7.0,ownedbytes 是 0.9.0) |
| tag | 0.27.0 → 67889269c(2026-10-17,「Version bump and changelog for tantivy 0.27 (#3197)」) | crates.io 上 tantivy-0.27.0 包的 .cargo_vcs_info.json 记的就是这个 commit |
| 历史 | 首个提交 2016-01-10,到 tag 共 3832 个提交 | 0.26.x 是单独的维护分支,0.26.1 不是 0.27.0 的祖先 |
| tag 之后 | main 到 360a5ebbb(2026-10-27)又有 7 个提交,没有发版 | 本卷不跟;某页讲的代码在这之后改过,会提一句 |
| 本站 | 0.27.0(从 0.26.1 升上来),加 tantivy-jieba 0.19 | 升级前后的差别在 08 在本站中 讲 |
源码链接一律指向 tag,形如 IndexWriter::commit。
讲什么
- 索引与写入:
Index、IndexWriter的线程与内存预算、SegmentWriter和stacker的内存池、提交与删除、段合并。 - 模式与分词:
Schema与字段选项、Term的字节布局、Document、分词器链和TokenizerManager。 - 存储格式:
Directory与 mmap、倒排表、位置、词典(FST)、文档存储、快速字段(columnar)、fieldnorm 与删除位图。 - 查询:
IndexReader与Searcher、Query → Weight → Scorer、BM25、布尔查询与 Block-Max WAND、短语等查询、查询解析器、收集器、高亮,以及聚合的主线。 - 只提一句:
jitexpr(0.27.0 新增、基于 Cranelift 的表达式 JIT,实验 feature)、SegmentPlugin的自定义扩展、quickwitfeature、failpoints、compare_hash_only;聚合不逐个讲聚合类型。
三条主线
全卷沿三条主线展开,每页开头标出自己在哪条线上:
- 写入主线(加 3 篇文档,提交一次):
Index::create_in_dir→Index::writer(budget)起索引线程、拿目录锁 →add_document打 opstamp、进有界 channel → 某个索引线程的SegmentWriter分词、写进内存池 →commit时每个线程把自己的段写成六个文件 →SegmentUpdater落删除、原子写meta.json、回收没用的文件、考虑合并。 - 查询主线(
old man,取前 10):IndexReader给出Searcher快照 →QueryParser把old man解析成四个Should子句的BooleanQuery→weight()用全部段的统计算 BM25 → 每个段生成Scorer,TopDocs用 Block-Max WAND 剪枝 → 合并各段的前 10 →searcher.doc从文档存储解压出原文。 - 磁盘主线(一个词怎么找到):段目录与
meta.json→MmapDirectory打开文件、读 footer →.term查到TermInfo→.idx读倒排表 →.pos读位置 →.fieldnorm取字段长度 →.store取原文 →.fast列存。
每条线的每一步落在哪个函数、哪一页讲,见怎么读这一卷。
flowchart TB intro[00 导读] --> overview[01 全景] overview --> schema["02 模式与分词"] schema --> indexing[03 写入] indexing --> storage[04 存储格式] storage --> search[05 查询] search --> design[06 设计取舍] design --> pit[07 常见坑] pit --> site[08 在本站中] site --> hands[09 动手]
实验环境
关于行为的说法(起几个线程、提交后目录里有哪些文件、分数怎么算出来、报什么错)都配一个小程序和它的真实输出。平台是 M2 Max、macOS 27、rustc 1.99.0,12 核(8 性能核 + 4 能效核),available_parallelism() 是 12;索引线程数随核数变化,页面上遇到会注明。实验只用 crates.io 上的 tantivy 0.27.0(默认 feature),需要对照本站升级前的版本时用 0.26.1。模板、语料和 feature 见怎么读这一卷。
各部分
- 00 导读:锁定版本;三条主线每一步落在哪;实验环境;feature 表,
quickwit写的索引默认构建读不了;crate 依赖图、src/各目录与规模。 - 01 全景:tantivy 在全文检索方案里的位置;一次写入(实测线程名、opstamp、段数);一次查询(实测
explain);一个段(实测文件清单、meta.json原文、footer)。 - 02 模式与分词:Schema 与字段、Term、文档、分词。
- 03 写入:IndexWriter、SegmentWriter 与内存池、提交、合并。
- 04 存储格式:Directory、倒排表、位置、词典、文档存储、快速字段、fieldnorm 与删除位图。
- 05 查询:IndexReader 与 Searcher、Query/Weight/Scorer、BM25、布尔查询、短语与其他查询、查询解析器、收集器、高亮、聚合。
- 06 设计取舍:不可变段、批量提交、没有主键、同步 API 与线程模型、mmap 的代价,和 Lucene 对照。
- 07 常见坑:报错原文做标题,每个都先跑出最小复现。
- 08 在本站中:本站搜索模块(tantivy 0.27.0 + tantivy-jieba)每一处调到 tantivy 的哪段源码,从 0.26.1 升上来改了什么。
- 09 动手:不依赖 tantivy,从分词、内存倒排表到 BM25 TopK 写一个最小倒排索引。