数据截至 (上游 commit 21b29048d7bc)
仓库上下文:tree-sitter 切片、Tantivy 索引与 RRF 混合检索
30 秒导读: Tabby 的"聪明"不在模型,而在它先把你整个代码仓库读懂、切碎、建好索引, 于是补全或提问时能一瞬间捞出"仓库里最相关的几段代码"塞进 prompt。这一章讲清这条链路: 从 tree-sitter 切片 → Tantivy 双路索引(向量 + 关键词)→ 查询时两路并跑 + RRF 融合排名。
本章聚焦"检索"这一侧。前一环——补全如何消费这些捞回来的片段——见 第 2 章; 后一环——Answer Engine 如何编排这套检索——见 第 5 章。
1. 这是什么(先建直觉)
要解决的问题: 大模型的上下文窗口装不下一个几十万行的仓库。你在 foo.rs 里写代码时,
真正有用的参考可能在 bar.rs 的某个函数里——怎么在毫秒级把那几段找出来?
Tabby 的答案: 把"找相关代码"做成一套信息检索(IR)系统。离线时把仓库嚼碎、建索引; 在线时把"当前光标附近的代码 / 用户的问题"当查询词,去索引里捞 Top-K。
一句话类比:它给你的代码仓库建了一个"搜索引擎"——既能像 Google 那样按关键词搜 (BM25),又能按"语义相似"搜(向量 embedding),最后把两份结果排名合并。
为什么要两种搜索一起上? 各有各的盲区:
| 检索方式 | 擅长 | 盲区 |
|---|---|---|
| BM25(关键词) | 精确命中标识符名 getFileExtension | 换个说法就搜不到("读文件扩展名") |
| Embedding(向量) | 抓语义近似 | 对精确符号名不敏感,可能漏掉 |
Tabby 把两者结果用 RRF(Reciprocal Rank Fusion,倒数排名融合) 合并,取长补短——这是本章的重点。
2. 顶层全景(检索的完整链路)
整条链路分两个阶段:离线建索引(indexing)和在线查询融合(retrieval)。先看全图:
离线:建索引 (crates/tabby-index)
─────────────────────────────────────────────────────────
git 仓库 ──sync/checkout──▶ 遍历文件 ──▶ ① 切片(CodeIntelligence)
│ tree-sitter 抽 tags
│ CodeSplitter 按语法切 chunk
▼
② 每个 chunk 双路编码
│ · embedding 向量 → 二值化成 token
│ · 源码文本 → 关键词 token
▼
③ 写进一个 Tantivy 索引
(corpus=code, doc→chunk 层级)
在线:查询 + 融合 (crates/tabby/src/services/code.rs)
─────────────────────────────────────────────────────────
查询内容 ──┬──▶ A. 向量查询(embedding token) ─┐
└──▶ B. 关键词查询(BM25 body) ─┤
▼
RRF 融合 (RANK_CONSTANT=60) ──▶ 过阈值 ──▶ Top-K 命中
怎么读这张图: 左→右是数据流。上半区是"把仓库变成可检索的东西",下半区是"拿查询去检索并融合"。 注意 A、B 两路并行跑同一个索引,最后在 RRF 汇合——这是 Tabby 检索的核心设计。
各部件一句话职责:
| 部件 | 干什么 | 在哪 |
|---|---|---|
CodeIntelligence | 读文件、抽 tags、切 chunk | crates/tabby-index/src/code/intelligence.rs |
CodeBuilder | 把 chunk 编码成 Tantivy 文档(向量+关键词) | crates/tabby-index/src/code/mod.rs |
index_repository | 遍历仓库、增量写索引、垃圾回收 | crates/tabby-index/src/code/index.rs |
TantivyDocBuilder / Indexer | 通用的"文档→chunk"落库封装 | crates/tabby-index/src/indexer.rs |
IndexSchema | 定义 corpus/doc/chunk 三层 schema | crates/tabby-common/src/index/mod.rs |
CodeSearchImpl | 在线查询 + RRF 融合 | crates/tabby/src/services/code.rs |
3. 切片:把文件变成可检索的 chunk
这节讲图里的 ①:一份源码文件,怎么变成一组带元数据的"块"(chunk)。
3.1 为什么不能整文件塞进索引
检索的最小单位是 chunk,不是文件。文件太大,整份算一个向量,语义会被"平均"糊掉; 按行硬切又会把一个函数拦腰斩断。Tabby 的做法:用语法边界来切。
3.2 tree-sitter 抽取"定义/引用"tags
第一步不是切,是读懂结构。CodeIntelligence::find_tags 用 tree-sitter-tags 扫一遍语法树,
抽出"这里定义了一个函数""那里引用了一个符号"这类 Tag:
// crates/tabby-index/src/code/intelligence.rs:24 CodeIntelligence::find_tags
let mut context = TagsContext::new();
let Ok((tags, has_error)) = context.generate_tags(&config.0, content.as_bytes(), None)
else { return empty; };
抽出的每个 Tag 记录了它的字节范围、名字范围、行范围、是否是定义(is_definition)、语法类型名等
(code/types.rs:69 Tag;位置用 Point{row,column},types.rs:57)。哪些语言支持,由
code/languages.rs 里一张 language → TagsConfiguration 的表决定(python/rust/java/scala…)。
直觉:tags 相当于给文件建了一份"符号目录",让后续能知道每个 chunk 里"住着"哪些定义。