8.1 Sourcegraph 与 Zoekt


8.1 Sourcegraph 与 Zoekt

本节摘要:拆第一组样本:Sourcegraph。它的检索是两层组合——上层是意图级语义检索:用户直接描述"我想做什么"("哪里校验了 JWT 的过期时间"),语义层负责跨命名鸿沟召回候选;下层是 Zoekt(荷兰语"搜索",源自 Google 三元组代码搜索思路的开源引擎):把每个文件切成重叠的三字符组(trigram)建倒排索引,负责保真 grep 式的精确匹配。Zoekt 的关键选择是索引 trigram 而不是词元:代码的语义单位藏在标识符内部、没有空格,子串与正则查询是刚需——Session 能同时命中 kill_sessionSESSION_TTLsessionManager,这是词元索引做不到的。"语义召回、字面验证"的分工与第 5 章的混合检索完全同构:向量对应意图层、BM25/Zoekt 对应字面层,只是产品把字面层做到了 grep 级保真。本节末给自建视角的启示对照表:哪些可以直接搬(trigram 替换 BM25 的条件)、哪些不必搬(意图层的实现细节以官方为准)。

学习目标

阅读完本节,你应当能够:

  1. 描述 Zoekt 三元组索引的原理:建索引与查索引各两步;
  2. 解释"索引 trigram 而非词元"对代码检索的意义;
  3. 说清"语义召回、字面验证"与第 5 章混合检索的同构关系;
  4. 判断自己的系统何时该把 BM25 换成 trigram(Zoekt)。

一、意图级语义检索:查询说的是"想做什么"

Sourcegraph 把查询入口从"关键词"升到"意图":where is JWT expiry validated 这类自然语言直接进搜索框(形态与能力以官方文档为准)。第 1.3 节讲过它跨越的是意图鸿沟——用户不知道、也不必知道实现叫 verify_claims 还是 check_exp。同一个意图,三种问法在两层的下场:

查询 意图层(语义) 字面层(Zoekt/BM25)
哪里校验 JWT 过期 召回 check_exp 无命中(词面不含)
check_exp 也能召回 精确到行
exp.*valid(正则) Zoekt 保真命中

产业现场这类查询的处理与自建管线并无神秘差异:语义层(嵌入与向量检索)召回候选,字面层提供精确定位与验证。差异在工程质量:索引规模(百万级仓库)、多语言、权限过滤(企业代码搜索的硬需求)。

二、Zoekt:三元组索引的原理

Zoekt(开源,源自 Google 内部代码搜索的 trigram 思路)把文本切成首尾重叠的三字符组,对每个 trigram 建倒排表:

文本 "kill_session" trigram: kil, ill, ll_, l_s, _se, ses, ess, ssi, sio, ion 建索引: kil → [文件A, 文件B, ...] (每个 trigram 一张倒排表) 查询 "session": ① 拆成 ses, ess, ssi, sio, ion → 各自的倒排表求交集 → 候选文件 ② 候选文件内逐个验证子串真命中 → 输出行级结果

两个设计决定的用意:

  1. 为什么是三字符、不是词元:词元索引(BM25 一路)把 kill_session 当一个词(或 5.1 那样拆成两个词),子串查询 sessionSess、正则 kill_?session 都要回退到扫描;trigram 是任意子串的公共货币——任何足够长的查询都能拆成 trigram 交集先缩候选集。代价是索引膨胀(约原文的 2~3 倍,经验值)与短查询(<3 字符)无能为力。
  2. 为什么交集后还要验证:trigram 命中只保证"这些三字符组都在文件里出现过",不保证按查询顺序相邻——所以候选文件内跑一次真匹配。两段式(索引缩圈+原位验证)与 4.3 的"先粗后精"同构。

查询的完整两步,对着上面的例子走一遍:

  1. 交集缩圈session 拆出的每个 trigram 各取倒排表求交——文件必须同时含全部 trigram 才进候选集,百万文件瞬间缩到个位数百;
  2. 原位验证:候选文件里跑真子串(或正则)匹配,输出行级结果——这一步保证"绝不漏报、绝不误报"的 grep 语义。

盲区与代价也要记住:短于 3 字符的查询拆不出 trigram(回退扫描);正则里的通配会让部分 trigram 缺失(引擎按最长相邻字面段退化处理);索引体积约为原文的 2~3 倍(经验值)。

三、语义层与字面层的分工

Sourcegraph 的公开材料把组合讲得很直白:语义检索回答"跟这个意图相关的代码在哪",Zoekt 式精确检索回答"这个字符串/正则到底在哪几行"。落到自建管线的一一对应:

产品层 自建对应
意图层(自然语言→候选) 向量一路(embed + mini_store/向量库) 2/4
字面层(精确匹配保真) BM25 一路;要求子串/正则保真时换 Zoekt 5.1 / 本节
两层结果合并 RRF 融合或级联(精确命中置顶) 5.1 表五
权限过滤(企业刚需) 元数据过滤(4.1 选型轴"生态") 4.1

把三路检索放在同一张表里对照,自建选型时按列挑(何时用哪路,5.1 表一的延续):

能力 Zoekt(trigram) BM25(词元) 向量
子串/正则查询 精确保真 拆词后近似 不支持
相关性打分 无(只有命中) 有(TF-IDF 系) 有(相似度)
中文注释/查询 支持(字符级) 需额外分词 支持
索引成本 2~3 倍原文(经验值) 词表+倒排,轻 嵌入费+向量存储
新鲜度代价 增量分片成熟 重建便宜 重嵌最贵(4.2)

💡 什么时候值得把 BM25 换成 Zoekt:查询日志里子串与正则查询占比高(Agent 常用 rg 式模式)、或用户抱怨"明明有这个字符串却搜不到"(分词把标识符拆丢了)。BM25 的优势是词元级相关性打分(Zoekt 不打分,只有命中);两者甚至可以并存——词法一路内部再分"打分层"与"保真层"。

四、自建视角的启示

  1. 保真与弹性是两种需求:grep 用户要的是"绝不漏掉任何命中"(保真),问答用户要的是"最相关的排前面"(弹性)。自建时别用一个引擎糊弄两种需求——5.1 的级联策略(精确命中置顶)就是最小实现;
  2. 索引膨胀要预算:trigram 索引 2~3 倍于原文(经验值),百万行仓库就是 GB 级的额外存储与更新成本——这是保真的价格标签;
  3. 新鲜度跟着索引走:索引越重,4.2 的增量更新越要紧——Zoekt 这类引擎的增量分片策略值得借鉴(按仓库/目录分片,改动只重建受影响分片,与 4.3 的思路一致);
  4. 权限是企业的第一公民:企业代码搜索落地的头号需求往往不是精度而是权限(谁能搜到什么)——自建时把权限放进元数据 schema(4.1 的生态轴),别事后补。

⚠️ 口径提醒:本节对 Sourcegraph 产品行为的描述基于撰写时点的公开文档与博客,功能迭代很快(如语义检索的入口形态、可用范围),以官方文档为准;repowise 2026-05 横评提供了多家工具的第三方视角(公开评测),引用其结论时应注明时点。

本节要点回顾

  1. Sourcegraph = 意图级语义检索(上层)+ Zoekt 三元组精确索引(下层)的组合;
  2. trigram 而非词元:任意子串的公共货币,代价是 2~3 倍索引膨胀与短查询盲区;
  3. 两段式查询(倒排交集缩圈 + 原位验证)与全书"先粗后精"的资源哲学同构;
  4. 自建启示:保真与弹性分开伺候;换 Zoekt 的触发条件是子串/正则查询占比高。

同样的管线,另一组样本押注在完全不同的两段。下一节看 Greptile 与 Copilot:一个把宝押在 chunk 质量,一个押在索引新鲜度。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U