Rust in Everything
基于 tantivy 0.27.0

查询

查询主线(old man,取前 10)从打开 IndexReader 开始,到拿回文档原文为止。核心代码在 src/query/(0.27.0 下 22174 行,含单元测试),读者和快照在 src/reader/(663 行)和 src/core/(1562 行,Searcher、Executor),收集器在 src/collector/(7755 行),查询语法在工作区的 query-grammar crate(2957 行),高亮在 src/snippet/(946 行),聚合在 src/aggregation/(36320 行,是主 crate 里最大的模块)。

这一部分回答几个问题:一次查询看到的是哪一组段,提交以后多久能看到新文档;一个查询对象怎么变成每个段上的游标,分数从哪些统计算出来;a b、+a +b、a -b 各走哪种合并算法,哪些形状能用上 Block-Max WAND 剪枝;用户输入的字符串怎么变成查询对象;结果怎么收集、排序、高亮、聚合。

  1. IndexReader 与 Searcher:Searcher 是一组 SegmentReader 的不可变快照,reload 用 ArcSwap 换代;默认的 OnCommitWithDelay 在磁盘目录上靠 500 ms 轮询,实测提交后 24 到 481 ms 才可见;Executor 默认单线程,而且要在建 reader 之前设置才生效。
  2. Query、Weight、Scorer:Query 描述查什么,Weight 绑定一次查询的统计,Scorer 是一个段上的 DocSet 游标;TermQuery 走一遍;段里有删除时,TermQuery 的计数从读词典里的 doc_freq 退回逐篇数,实测从 0.2 µs 变成 31 µs。
  3. BM25:公式和 Bm25Weight 的实现,k1、b 是常量;N 和 avgdl 按整个 Searcher 的 max_doc 算,不看文档有没有这个字段;用 explain 逐项手算一篇维基文档的 5.736597。
  4. 布尔查询:BooleanWeight 按子句形状挑 Intersection、BufferedUnionScorer、Exclude、RequiredOptionalScorer;Block-Max WAND 在 0.27.0 扩到交集和单个词;实测剪枝的收益和失灵的形状,以及块上界在多段索引里会偏低。
  5. 短语与其他查询:短语查询怎么在位置上求交、slop 怎么算,以及前缀短语、模糊、正则、范围、exists、TermSetQuery 这些查询各自走倒排表还是快速字段。
  6. 查询解析器:query-grammar 把字符串解析成 UserInputAst,QueryParser 结合 schema 变成 LogicalAst 和查询对象,默认 OR、字段、boost、模糊和容错解析都在这一步决定。
  7. 收集器:Collector 和 SegmentCollector 两层,TopDocs 的堆和阈值怎么喂给剪枝,Count、MultiCollector、FacetCollector 和按快速字段排序。
  8. 高亮:SnippetGenerator 怎么从原文里挑出得分最高的片段、标出命中的词、输出 HTML。
  9. 聚合:Elasticsearch 风格的聚合请求怎么变成每个段上的中间结果、再跨段合并成最终结果。
flowchart TB
  reader["IndexReader"] --> searcher["Searcher 快照"]
  qp["QueryParser"] --> query["Query"]
  query --> weight["Weight 全局统计"]
  searcher --> weight
  weight --> scorer["每段 Scorer"]
  scorer --> coll["Collector"]
  coll --> merge["merge_fruits"]
  merge --> doc["Searcher doc"]
  merge --> snip["Snippet"]
  coll --> agg["聚合"]

整条查询主线的时序和一次 explain 的逐项拆解在一次查询。查询读的都是段文件:词典在词典,倒排表和块上界在倒排表,字段长度在fieldnorm 与删除位图,取原文在文档存储。

💬 关联讨论

发起讨论
加载中...