本节摘要:拆第一组样本:Sourcegraph。它的检索是两层组合——上层是意图级语义检索:用户直接描述"我想做什么"("哪里校验了 JWT 的过期时间"),语义层负责跨命名鸿沟召回候选;下层是 Zoekt(荷兰语"搜索",源自 Google 三元组代码搜索思路的开源引擎):把每个文件切成重叠的三字符组(trigram)建倒排索引,负责保真 grep 式的精确匹配。Zoekt 的关键选择是索引 trigram 而不是词元:代码的语义单位藏在标识符内部、没有空格,子串与正则查询是刚需——
Session能同时命中kill_session、SESSION_TTL、sessionManager,这是词元索引做不到的。"语义召回、字面验证"的分工与第 5 章的混合检索完全同构:向量对应意图层、BM25/Zoekt 对应字面层,只是产品把字面层做到了 grep 级保真。本节末给自建视角的启示对照表:哪些可以直接搬(trigram 替换 BM25 的条件)、哪些不必搬(意图层的实现细节以官方为准)。
阅读完本节,你应当能够:
Sourcegraph 把查询入口从"关键词"升到"意图":where is JWT expiry validated 这类自然语言直接进搜索框(形态与能力以官方文档为准)。第 1.3 节讲过它跨越的是意图鸿沟——用户不知道、也不必知道实现叫 verify_claims 还是 check_exp。同一个意图,三种问法在两层的下场:
| 查询 | 意图层(语义) | 字面层(Zoekt/BM25) |
|---|---|---|
| 哪里校验 JWT 过期 | 召回 check_exp 等 |
无命中(词面不含) |
check_exp |
也能召回 | 精确到行 |
exp.*valid(正则) |
— | 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 → 各自的倒排表求交集 → 候选文件 ② 候选文件内逐个验证子串真命中 → 输出行级结果
两个设计决定的用意:
kill_session 当一个词(或 5.1 那样拆成两个词),子串查询 session、Sess、正则 kill_?session 都要回退到扫描;trigram 是任意子串的公共货币——任何足够长的查询都能拆成 trigram 交集先缩候选集。代价是索引膨胀(约原文的 2~3 倍,经验值)与短查询(<3 字符)无能为力。查询的完整两步,对着上面的例子走一遍:
session 拆出的每个 trigram 各取倒排表求交——文件必须同时含全部 trigram 才进候选集,百万文件瞬间缩到个位数百;盲区与代价也要记住:短于 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 不打分,只有命中);两者甚至可以并存——词法一路内部再分"打分层"与"保真层"。
⚠️ 口径提醒:本节对 Sourcegraph 产品行为的描述基于撰写时点的公开文档与博客,功能迭代很快(如语义检索的入口形态、可用范围),以官方文档为准;repowise 2026-05 横评提供了多家工具的第三方视角(公开评测),引用其结论时应注明时点。
同样的管线,另一组样本押注在完全不同的两段。下一节看 Greptile 与 Copilot:一个把宝押在 chunk 质量,一个押在索引新鲜度。