源码地图
主线位置:三条主线经过的 crate 和目录。写入主线在
src/indexer/、src/postings/和子 cratestacker里;查询主线在src/reader/、src/core/、src/query/、src/collector/里;磁盘主线在src/directory/、src/termdict/、src/postings/、src/store/和子 cratecolumnar、bitpacker里。本页说明这些 crate 谁依赖谁、每个目录管什么、一个段的文件由谁写出、上游文档对应哪一页。
flowchart TB tv["tantivy"] --> col["tantivy-columnar"] tv --> stk["tantivy-stacker"] tv --> bp["tantivy-bitpacker"] tv --> cm["tantivy-common"] tv --> qg["query-grammar"] tv --> tapi["tokenizer-api"] tv --> fst["tantivy-fst"] tv -.->|quickwit| sst["tantivy-sstable"] col --> sst col --> stk col --> bp col --> cm sst --> fst sst --> bp sst --> cm stk --> cm cm --> ob["ownedbytes"] bp --> bpk["bitpacking"]
虚线那条边只在开了 quickwit 时存在;jitexpr 只在开同名 feature 时出现,图里没画。下面逐条核实。
依赖图:cargo tree 核实
依赖关系按实验项目 intro-run(模板的拷贝,tantivy = "=0.27.0",默认 feature)实际解析出来的看,命令是 cargo tree --offline -e normal -p <crate> --depth 1:
| crate(版本) | 直接依赖的工作区 crate | 值得一提的其他依赖 |
|---|---|---|
tantivy 0.27.0 | tantivy-columnar、tantivy-stacker、tantivy-bitpacker、tantivy-common、tantivy-query-grammar、tantivy-tokenizer-api | tantivy-fst(词典)、crossbeam-channel(写入的队列)、rayon(SegmentUpdater、合并、多线程查询)、memmap2 与 fs4(mmap)、lz4_flex、frostem、levenshtein_automata、census、arc-swap、bon、typetag、datasketches |
tantivy-columnar 0.27.0 | tantivy-sstable、tantivy-stacker、tantivy-bitpacker、tantivy-common | fastdivide、rand |
tantivy-sstable 0.27.0 | tantivy-bitpacker、tantivy-common | tantivy-fst、zstd、futures-util |
tantivy-stacker 0.27.0 | tantivy-common | murmurhash32 |
tantivy-common 0.27.0 | ownedbytes | async-trait、time |
tantivy-bitpacker 0.27.0 | 无 | bitpacking(SIMD 的 128 个整数一块的打包) |
tantivy-query-grammar 0.27.0 | 无 | nom 7 |
tantivy-tokenizer-api 0.7.0 | 无 | serde |
ownedbytes 0.9.0 | 无 | stable_deref_trait |
从表里能读出三条:
- 没有 tokio,也没有 async 运行时。 整棵树里一个
tokio都没有;futures-util是tantivy-sstable带进来的,async-trait是tantivy-common带进来的,给quickwit下的异步读文件接口用。写入的并发靠标准库线程加crossbeam-channel,后台任务靠rayon线程池,见一次写入的线程清单。 - 分词器的接口单独成 crate。
tantivy-tokenizer-api只有 180 行,定义Tokenizer、TokenStream、Token和TokenFilter,版本号是 0.7.0,和主 crate 分开发布。第三方分词器(本站用的tantivy-jieba)只依赖它,所以主 crate 从 0.26 升到 0.27 时分词器不用跟着升:两版用的都是tantivy-tokenizer-api 0.7.0。 ownedbytes是 tantivy 自己的字节容器。OwnedBytes只有两个字段:一个&'static [u8]和一个让底层数据(mmap、Vec)活着的Arc<dyn Deref<Target = [u8]> + Sync + Send>,tantivy-common的FileSlice读出来的就是它。它和 bytes 卷 讲的Bytes都是「共享所有权的只读字节」,但没有BytesMut那一半,底层对象一律装进一个Arc的 trait 对象,不像Bytes那样按来源换 vtable。
问: 词典默认是 FST,sstable 是 quickwit 下的另一种词典。为什么默认 feature 下 cargo tree 里还有 tantivy-sstable?
答: 因为 tantivy-columnar 无条件依赖它。快速字段里的字符串列和字节列要把值换成序号(字典编码),字典就存成一个 sstable。反查一下:
tantivy-sstable v0.27.0
`-- tantivy-columnar v0.27.0
`-- tantivy v0.27.0
`-- intro-run v0.1.0 (~/exp-tantivy/intro-run)
开了 quickwit 的 intro-qw 里,tantivy-sstable 多出一条直接来自 tantivy 的边,用来做词典。所以默认构建也编译 sstable 的全部代码,默认 feature 里的 columnar-zstd-compression 管的就是这些字典块要不要用 zstd 压。
src/ 下的目录
src/ 共 124966 行(tag 上所有 .rs 文件,含 #[cfg(test)] 模块,不含顶层 tests/、benches/、examples/)。按目录:
| 目录 | 行数 | 管什么 | 主线 | 讲它的页 |
|---|---|---|---|---|
aggregation/ | 36320 | Elasticsearch 风格的聚合:请求、每段的中间结果、合并 | 查询 | 聚合 |
query/ | 22174 | Query、Weight、Scorer 的各种实现,BM25,查询解析器 | 查询 | 05 查询 |
indexer/ | 11518 | IndexWriter、SegmentWriter、SegmentUpdater、删除队列、合并策略与合并 | 写入 | 03 写入 |
schema/ | 9882 | Schema、字段类型与选项、Term、Document 与它的二进制格式 | 写入 | 02 模式与分词 |
tokenizer/ | 9111 | 内置分词器和过滤器、TextAnalyzer、TokenizerManager | 写入、查询 | 分词 |
collector/ | 7755 | Collector/SegmentCollector、TopDocs、Count、facet | 查询 | 收集器 |
postings/ | 4999 | 索引时的倒排表写入(PostingsWriter、Recorder),磁盘上的倒排表读取、跳表 | 三条 | SegmentWriter 与内存池、倒排表 |
index/ | 4201 | Index、IndexMeta、Segment、SegmentReader、SegmentComponent、倒排索引插件 | 三条 | 一个段 |
directory/ | 3203 | Directory trait、MmapDirectory、RamDirectory、ManagedDirectory、锁、footer、复合文件 | 磁盘 | Directory |
fastfield/ | 3020 | 快速字段的写入与读取(底下是 columnar)、删除位图 | 磁盘 | 快速字段、fieldnorm 与删除位图 |
store/ | 2813 | 文档存储:分块、压缩、跳表索引、块缓存 | 磁盘 | 文档存储 |
termdict/ | 1917 | 词典:FST 实现与 sstable 实现 | 磁盘 | 词典 |
core/ | 1562 | 只剩 Searcher、Executor 和 json_utils | 查询 | IndexReader 与 Searcher |
snippet/ | 946 | 高亮片段 | 查询 | 高亮 |
fieldnorm/ | 827 | 字段长度压成一个字节 | 磁盘 | fieldnorm 与删除位图 |
reader/ | 663 | IndexReader、ReloadPolicy、warmer | 查询 | IndexReader 与 Searcher |
space_usage/ | 585 | 按段、按组件、按字段统计占用 | 磁盘 | 一个段 |
positions/ | 478 | 位置的写入与读取 | 磁盘 | 位置 |
根目录下还有 lib.rs(1281 行,大半是测试)、plugin.rs(634 行,SegmentPlugin trait 和它的测试)、docset.rs(342 行,DocSet 与 TERMINATED)、error.rs 等。聚合和查询两个目录加起来占 src/ 的 47%;三条主线真正经过的 indexer、postings、index、directory、store、termdict、fieldnorm、positions、reader、core 加起来是 32181 行。
一个段的文件由谁写出
SegmentComponent 列出了段的组件和扩展名。0.27.0 起,写这些文件的不再是一个统一的序列化器,而是 builtin_plugins 返回的三个内置插件,每个实现 SegmentPlugin,声明自己拥有哪些扩展名:
| 插件 | 扩展名 | 写什么 | 讲它的页 |
|---|---|---|---|
InvertedIndexPlugin | fieldnorm、term、idx、pos | 字段长度、词典、倒排表、位置;四个都是复合文件(CompositeFile),每个字段一段 | fieldnorm 与删除位图、词典、倒排表、位置 |
FastFieldsPlugin | fast | 快速字段的列存;schema 里一个快速字段都没有时也写一个空的列存文件 | 快速字段 |
StorePlugin | store | 标了 STORED 的字段,压缩分块 | 文档存储 |
另外两种文件不归任何插件:<uuid>.<opstamp>.del(删除位图,提交时有删除才写)和 <uuid>.store.temp(开了索引排序或手动指定 doc id 顺序时,先不压缩写下的临时文档存储)。实测的目录清单、每个文件的大小和末尾的 footer 见一个段。
子 crate 从哪读起
行数是 tag 上 <crate>/src/ 下所有 .rs 文件的行数,含测试模块。卷目录(plans/)里的规模数字含 benches/ 等目录,所以 columnar 是 15526、stacker 是 2012,比下表大。
| crate | 先读 | 被谁用 | src/ 行数 |
|---|---|---|---|
tantivy-columnar | ColumnarWriter、ColumnarReader、column_values/ 的几种编码 | fastfield/ | 14438 |
tantivy-sstable | Dictionary、streamer.rs、delta.rs | columnar 的字典;quickwit 下的词典 | 5388 |
tantivy-common | FileSlice、BitSet、vint.rs、BinarySerializable | 几乎所有目录 | 3213 |
tantivy-query-grammar | parse_query、UserInputAst、query_grammar.rs(nom 组合子) | query/query_parser/ | 2957 |
tantivy-bitpacker | BitPacker、BlockedBitpacker、filter_vec/(0.27.0 加了 NEON 和 SVE 实现) | columnar、sstable、postings/ | 1875 |
tantivy-stacker | MemoryArena、ArenaHashMap、ExpUnrolledLinkedList | postings/ 的索引时倒排表、columnar 的写入 | 1742 |
ownedbytes | OwnedBytes(只有 lib.rs) | common | 363 |
tantivy-tokenizer-api | Tokenizer(只有 lib.rs) | tokenizer/、第三方分词器 | 180 |
jitexpr | 本卷不讲 | jitexpr feature | 15605 |
上游文档对应本卷哪一页
ARCHITECTURE.md 是仓库里最有用的一篇,按目录讲职责;doc/src/ 是一本 mdBook,大部分章节没写完。它们是资料,本卷引用前先核实:
| 上游文档 | 讲什么 | 本卷 | 和 0.27.0 的代码对不上的地方 |
|---|---|---|---|
ARCHITECTURE.md 开头 | 设计原则:查询和索引的内存是 O(1),代价是批量更新 | 为什么是 tantivy | |
ARCHITECTURE.md 的 core/ 一节 | Index、段、删除、DocId、合并、Searcher | 一次写入、提交、合并 | 标题写「core/: Index, segments, searchers」,Index 和 Segment 早已移到 src/index/,src/core/ 只剩 Searcher 和 Executor |
ARCHITECTURE.md 的 directory/、store/、fastfield/、倒排索引各节 | 存储格式 | 04 存储格式 | 文中链接到的 29 个路径在 tag 上都还存在 |
ARCHITECTURE.md 的 query/、collector/、query-grammar | 查询的三层与收集器 | 05 查询 | 没有讲 aggregation/、reader/、snippet/ 和 plugin.rs |
doc/src/avant-propos.md | 「Tantivy is a search engine library」:库不是服务 | 为什么是 tantivy | |
doc/src/basis.md | 直接读磁盘、段与日志法、合并 | 一个段 | 最后一节「Indexing throughput, number of indexing threads」只有标题 |
doc/src/index_sorting.md | 索引排序 | 合并 | |
doc/src/json.md | JSON 字段的编码和限制 | Term、文档 | 没有列进 SUMMARY.md |
doc/src/facetting.md、faq.md、schema.md、innerworkings.md、inverted_index.md、examples.md | 空或只有标题 | facetting.md 的正文是占位的「wewew」;SUMMARY.md 里「Best practise」链到的是 inverted_index.md |
小结
- 依赖方向:
tantivy→columnar→sstable、stacker、bitpacker、common→ownedbytes;tantivy自己也直接用stacker、bitpacker、common、query-grammar、tokenizer-api和tantivy-fst。 tantivy-sstable默认就在依赖树里(columnar的字典),quickwit只是多出一条做词典的边。- 没有 tokio;并发是标准库线程、
crossbeam-channel和rayon。 tantivy-tokenizer-api0.7.0 单独发布,0.26 和 0.27 共用,tantivy-jieba不用跟着升。src/124966 行,聚合和查询占 47%;三条主线经过的十个目录约 3.2 万行。- 段的六个文件由三个内置
SegmentPlugin写出:倒排索引插件写fieldnorm、term、idx、pos,另两个各写fast和store。 ARCHITECTURE.md的core/一节已过时;doc/的小书大半是空章节。