Rust in Everything
基于 tantivy 0.27.0

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)
tag0.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 的自定义扩展、quickwit feature、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 见怎么读这一卷。

各部分

  1. 00 导读:锁定版本;三条主线每一步落在哪;实验环境;feature 表,quickwit 写的索引默认构建读不了;crate 依赖图、src/ 各目录与规模。
  2. 01 全景:tantivy 在全文检索方案里的位置;一次写入(实测线程名、opstamp、段数);一次查询(实测 explain);一个段(实测文件清单、meta.json 原文、footer)。
  3. 02 模式与分词:Schema 与字段、Term、文档、分词。
  4. 03 写入:IndexWriter、SegmentWriter 与内存池、提交、合并。
  5. 04 存储格式:Directory、倒排表、位置、词典、文档存储、快速字段、fieldnorm 与删除位图。
  6. 05 查询:IndexReader 与 Searcher、Query/Weight/Scorer、BM25、布尔查询、短语与其他查询、查询解析器、收集器、高亮、聚合。
  7. 06 设计取舍:不可变段、批量提交、没有主键、同步 API 与线程模型、mmap 的代价,和 Lucene 对照。
  8. 07 常见坑:报错原文做标题,每个都先跑出最小复现。
  9. 08 在本站中:本站搜索模块(tantivy 0.27.0 + tantivy-jieba)每一处调到 tantivy 的哪段源码,从 0.26.1 升上来改了什么。
  10. 09 动手:不依赖 tantivy,从分词、内存倒排表到 BM25 TopK 写一个最小倒排索引。

适合谁读

  • 用过 tantivy、想知道 commit 和 search 背后发生了什么的读者:先读 01 全景 的三页实测,再读 03 写入 和 05 查询。
  • 熟悉 Lucene 或 Elasticsearch 的读者:为什么是 tantivy 有一张概念对照表,之后直接读 04 存储格式 和 06 设计取舍。
  • 需要的前置知识:倒排索引的基本概念(词 → 包含它的文档列表);Rust 的 trait 对象与 Send/Sync;知道 mmap 是什么。不需要 async:tantivy 是同步库,不依赖 tokio。

💬 关联讨论

发起讨论
加载中...