Rust in Everything
基于 tantivy 0.27.0

源码地图

主线位置:三条主线经过的 crate 和目录。写入主线在 src/indexer/、src/postings/ 和子 crate stacker 里;查询主线在 src/reader/、src/core/、src/query/、src/collector/ 里;磁盘主线在 src/directory/、src/termdict/、src/postings/、src/store/ 和子 crate columnar、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.0tantivy-columnar、tantivy-stacker、tantivy-bitpacker、tantivy-common、tantivy-query-grammar、tantivy-tokenizer-apitantivy-fst(词典)、crossbeam-channel(写入的队列)、rayon(SegmentUpdater、合并、多线程查询)、memmap2 与 fs4(mmap)、lz4_flex、frostem、levenshtein_automata、census、arc-swap、bon、typetag、datasketches
tantivy-columnar 0.27.0tantivy-sstable、tantivy-stacker、tantivy-bitpacker、tantivy-commonfastdivide、rand
tantivy-sstable 0.27.0tantivy-bitpacker、tantivy-commontantivy-fst、zstd、futures-util
tantivy-stacker 0.27.0tantivy-commonmurmurhash32
tantivy-common 0.27.0ownedbytesasync-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。
📝NOTE
没有蠢问题

问: 词典默认是 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/36320Elasticsearch 风格的聚合:请求、每段的中间结果、合并查询聚合
query/22174Query、Weight、Scorer 的各种实现,BM25,查询解析器查询05 查询
indexer/11518IndexWriter、SegmentWriter、SegmentUpdater、删除队列、合并策略与合并写入03 写入
schema/9882Schema、字段类型与选项、Term、Document 与它的二进制格式写入02 模式与分词
tokenizer/9111内置分词器和过滤器、TextAnalyzer、TokenizerManager写入、查询分词
collector/7755Collector/SegmentCollector、TopDocs、Count、facet查询收集器
postings/4999索引时的倒排表写入(PostingsWriter、Recorder),磁盘上的倒排表读取、跳表三条SegmentWriter 与内存池、倒排表
index/4201Index、IndexMeta、Segment、SegmentReader、SegmentComponent、倒排索引插件三条一个段
directory/3203Directory 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/663IndexReader、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,声明自己拥有哪些扩展名:

插件扩展名写什么讲它的页
InvertedIndexPluginfieldnorm、term、idx、pos字段长度、词典、倒排表、位置;四个都是复合文件(CompositeFile),每个字段一段fieldnorm 与删除位图、词典、倒排表、位置
FastFieldsPluginfast快速字段的列存;schema 里一个快速字段都没有时也写一个空的列存文件快速字段
StorePluginstore标了 STORED 的字段,压缩分块文档存储

另外两种文件不归任何插件:<uuid>.<opstamp>.del(删除位图,提交时有删除才写)和 <uuid>.store.temp(开了索引排序或手动指定 doc id 顺序时,先不压缩写下的临时文档存储)。实测的目录清单、每个文件的大小和末尾的 footer 见一个段。

子 crate 从哪读起

行数是 tag 上 <crate>/src/ 下所有 .rs 文件的行数,含测试模块。卷目录(plans/)里的规模数字含 benches/ 等目录,所以 columnar 是 15526、stacker 是 2012,比下表大。

crate先读被谁用src/ 行数
tantivy-columnarColumnarWriter、ColumnarReader、column_values/ 的几种编码fastfield/14438
tantivy-sstableDictionary、streamer.rs、delta.rscolumnar 的字典;quickwit 下的词典5388
tantivy-commonFileSlice、BitSet、vint.rs、BinarySerializable几乎所有目录3213
tantivy-query-grammarparse_query、UserInputAst、query_grammar.rs(nom 组合子)query/query_parser/2957
tantivy-bitpackerBitPacker、BlockedBitpacker、filter_vec/(0.27.0 加了 NEON 和 SVE 实现)columnar、sstable、postings/1875
tantivy-stackerMemoryArena、ArenaHashMap、ExpUnrolledLinkedListpostings/ 的索引时倒排表、columnar 的写入1742
ownedbytesOwnedBytes(只有 lib.rs)common363
tantivy-tokenizer-apiTokenizer(只有 lib.rs)tokenizer/、第三方分词器180
jitexpr本卷不讲jitexpr feature15605

上游文档对应本卷哪一页

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.mdJSON 字段的编码和限制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-api 0.7.0 单独发布,0.26 和 0.27 共用,tantivy-jieba 不用跟着升。
  • src/ 124966 行,聚合和查询占 47%;三条主线经过的十个目录约 3.2 万行。
  • 段的六个文件由三个内置 SegmentPlugin 写出:倒排索引插件写 fieldnorm、term、idx、pos,另两个各写 fast 和 store。
  • ARCHITECTURE.md 的 core/ 一节已过时;doc/ 的小书大半是空章节。

💬 关联讨论

发起讨论
加载中...