跳到主要内容

当「意思相近」不够用

这一章讲三件事: 按意思搜会在哪三种地方稳定地失手、每一种该拿什么补、 以及每一种补法在什么条件下才值得加上去

它在全书链条上的位置:第 02 章那条走查的第 4 步。 「算出与每块的相近程度 → 取最近三块」——这一步归这一章重做一遍。

1. 这一章讲什么

三句话:

  1. 前面九章一路把「按意思搜」建起来了,而这一章从反面开始: 它有三类固定的失手,而且每一类都不是「模型不够好」能修的;
  2. 三类失手各有一种补法,三种补法发生在三个不同的时刻—— 一种在算相近程度的同时(把按字面命中的老办法并进来)、 一种在算相近程度之前(先按标注把范围缩小)、 一种在打开库之前(先决定该问哪个库);
  3. 这一章最值钱的不是三种做法本身,是每一种的启动条件—— 书在三个不同的章节里用了几乎相同的句式说同一件事: 先只用最简单的那种,直到你在检索日志里亲眼看见那一类失败,才加下一层。

一句话交代这一章的材料从哪来: 这同一件事被原书拆散在第 5、6、7 三章里 (嵌入那一章讲了两遍、向量库那一章讲了一遍、检索那一章讲了两遍), 我们把它们收拢在一起,因为分开读会以为是五件事,合起来才看得出是三类失手对三种补法

2. 顶层全景:三类失手,三种补法

用户的问题

├── 补法三:先选源 ──────────→ 这问题该问哪个库?(第 9、10 节)
│ (打开库之前) 失手三:问错了库

├── 补法二:先缩范围 ─────────→ 只在有资格的那些块里比(第 4 节)
│ (算相近程度之前) 失手二:两个领域的同一个词串台

└── 补法一:两条路并行 ───────→ 按意思比 + 按字面比,两个榜合并(第 5、6 节)
(算相近程度的同时) 失手一:精确串一个字都不能差

图说:三种补法互不替代,可以叠加,也可以一个都不加。
这一章的顺序是从里往外:先讲最靠近"比一比"那一步的,再一层层往外退。

另外两个时刻不在这一章:问之前先把问题改写一遍是第 11 章, 搜完之后补上下文、再挑一遍结果是第 12 章。 这一章只管「搜的那一刻」。

3. 三类固定的失手(先看现象)

先不讲任何做法,先看三个真实的问句是怎么歪掉的。

失手一:一个字都不能差的那种串

书列了四类1:

这一类串差一个字就是另一件事
商品编号SKU-4792 必须一模一样地命中
法条编号「第 230 条」和「第 230 节」不是同一个东西
医疗诊断码那套国际疾病分类编码,一位数字都不能错
技术标识接口路径名、数据库表名

为什么按意思搜对付不了它们: 第 08 章讲过,那串数表示的是「这段话大概在讲什么」。 而 SKU-4792SKU-4793 在「大概在讲什么」这个层面上几乎完全一样—— 它们都是「一个商品编号」。你要的那个差别恰好被压掉了。

书还给了一种更常见的混合形态2:用户把大白话和精确串混在一句里问——

How do I configure SSL for endpoint /api/v2/users? (「我怎么给 /api/v2/users 这个接口配 SSL?」)

前半句要靠意思理解,后半句要靠字面精确命中。 这一句是这一章的主走查(第 7 节)。

失手二:两个领域用同一个词,串台了

书给的例子极准3:

用户问的是: "market performance"(市场表现)
库里有: 金融板块的财报分析
体育板块的 "team performance"(球队表现)

按意思一比: 两者都在讲"某某的表现",相近程度都很高
取回三块: 体育文章挤了进来

问题不在模型算错了。 它算的就是「意思像不像」,而这两段话在那个层面上确实像。 是问的人心里有一个「哪个领域」的前提,而这个前提一个字都没写进问句里。

失手三:这个问题根本不该问这个库

书举的是最直白的一对4:

  • 问「87 乘 99 等于几」——该交给一个会算数的工具,不该去搜任何文档;
  • 问「某年谷歌营收多少」——该去搜文档,因为答案多半就写在某份财报里。

这一类失手的形状是:你的系统里有好几个可以问的地方,而它每次只会问同一个。

4. 补法二:先缩范围 —— 那道闸在查询语句里长什么样

先讲这一个,因为它最便宜,而且第 06 章已经讲过它是什么。

第 06 章讲的是:元数据(关于这块文字本身的说明:出自哪份文件、第几页、 谁写的、属于哪个部门)不参与「意思像不像」的比较,它管的是哪些块有资格参加比较; 而且它发生在算相近程度之前,不是「搜出来再筛一遍」。

这一节只补一件事:它在真实的查询语句里长什么样。

那一列叫什么、那一句怎么写

书的做法是在存块的那张表上多加一列5:

CREATE TABLE embeddings_table_with_metadata(
id SERIAL PRIMARY KEY,
text_chunk TEXT,
embedding VECTOR(1536),
metadata JSONB ← 多的就是这一列
)

↑ JSONB 是这个数据库里存"一小段结构化说明"的列类型:
里面装的是 {"topic": "football"} 这样的"字段名:值"对,
存进去之后可以直接拿字段名去筛。

查的时候:

SELECT text_chunk, 1 - (embedding <=> '<问题那一串数>') AS 相近程度
FROM embeddings_table_with_metadata
WHERE metadata->>'topic' = 'football' ← 先关上闸
ORDER BY 相近程度 DESC ← 再在剩下的里排序
LIMIT 5;

图说:关键是 WHERE 在 ORDER BY 之前。
数据库先把 topic 不是 football 的行整个扔掉,再在剩下的行里算相近程度。
第 09 章第 6 节说的"它真正的价值来自和条件过滤组合起来",指的就是这一句。

metadata->>'topic' 那个 ->> 是「把这个字段的值当文字取出来」的写法。

走查:「谁是史上最强球员?」

这是这一章另起的第一处走查,因为它落不到主走查上(主走查那句里没有歧义)。 用的是书那个跑例6

场景:一个体育网站的问答机器人,库里同时装着足球、网球、篮球的内容。

库里四块(书给的原样):
① "Roger Federer has won 20 Grand Slam titles in tennis." topic=tennis
② "The FIFA World Cup is the most prestigious football …" topic=football
③ "Serena Williams is one of the greatest tennis players …" topic=tennis
④ "Lionel Messi has won multiple Ballon d'Or …" topic=football

用户问:"谁是史上最强球员?"(代码里那句是 "Who is the best player?")

── 不关闸 ──
①③ 都在讲"最伟大的球员",相近程度很高
④ 也高
取前 3:很可能是 ③、④、① —— 混着两个项目,答不出来

── 关闸(topic = football)──
①③ 在算相近程度之前就被扔掉了,根本不参与比较
只剩 ②④ 参与
取前 1:④(梅西那条) ← 书说这一条排在最高位

图说:闸门的值从哪来?书这里是从用户资料来的 ——
"Jim 是利物浦的球迷",所以默认给他关到 football 这一格。

闸门的值也可以让模型来定

书给了一条延伸做法7:让模型看一眼问题、再看一眼库里有哪些类别,自己提出该用哪些条件。

它同时给了这条做法的成本纪律,而且这条纪律值得单独记:

这一步用快而便宜的小模型就行,因为多数情况下用户要什么是明摆着的; 真正难的是分析复杂片段、得出好结论,那一步才值得上大模型。

这是第 03 章那条「选仍然满足要求的最小模型、而且逐个步骤地选」在这里的一次具体落地。

一条性能忠告,和一条启动条件

性能忠告(书给的)8:

别在做相似度搜索的同一条查询里做复杂的多表连接,那会拖慢它。 先在子查询里过滤,再做相似度搜索。

子查询」就是把「先筛出这些行」写成一条独立的小查询,让外层只在它的结果上干活。

启动条件(书给的反面判据)9:整个库题材同质、 或者你手上根本没有可以拿来筛的标注、或者维护这些标注的力气超过了它带来的准确度收益—— 这三种情况下不加过滤更简单也更快。

还有一个不显眼的收益值得记:可测性10。按主题分段之后, 你可以为每个主题各建一套评测题,更容易量出性能、找出弱在哪一块。 (评测那件事本身是第 16、17 章。)

5. 承重词一:BM25 —— 按字面命中打分的那套老办法

这一章第一个承重词。一句话先给结论:BM25 是一套只看「查询里的词有没有在这块文字里 出现、出现了几次」的打分办法,它完全不管意思; 它管的正是按意思搜抓不住的那一半——字面命中。

先看现象:那个接口路径为什么搜不到

回到那句话:How do I configure SSL for endpoint /api/v2/users?

纯按意思搜,取回三块:
"TLS/SSL 证书的配置流程与常见错误" 相近程度 0.71
"如何为服务端启用 HTTPS" 相近程度 0.68
"API 网关的安全基线" 相近程度 0.64

用户真正要的那一块:
"端点 /api/v2/users 的鉴权与传输加密配置" 相近程度 0.58 ← 排在第 7 位,没进前三

(这些相近程度的数是为演示编的,书没有给这个例子的实测分数。)

图说:三块都在讲 SSL,一块都没提那个端点。
按意思搜做的事完全正确 —— 它就是按"大概在讲什么"排的序。

它是什么、它凭什么能补上这一刀

书只用了一句话交代它11:「关键词搜索用 BM25 按精确的词项匹配给文档排名」。 书没有讲它内部怎么算。 而不讲清楚,读者就只会把它当成一个咒语。

补充(不在书里,来自通用知识):BM25 是一个具体的打分公式,它同时看三件事。

它看什么具体怎么影响分数
词频:查询里的某个词在这块文字里出现了几次出现越多分越高,但收益递减——出现 10 次不会比出现 5 次高一倍
这个词在整个库里有多罕见越罕见越值钱。/api/v2/users 全库只在三块里出现过,命中它几乎就是决定性的;the 到处都是,几乎不加分
这块文字有多长长的要打折。否则一篇又臭又长的文档只靠「词多」就能碰巧命中一堆查询

第二件事有个正式名字叫逆文档频率,第三件叫长度归一化。 名字记不住不要紧,记住形状就够:罕见的词最值钱、长文档不会因为长而占便宜。

BM25 这个名字本身是「Best Matching 25」的缩写,是一族排序公式里的第 25 号变体, 1990 年代就有了——它比按意思搜老得多,不是被淘汰的东西,是管另一半事的东西。

反过来看一次:什么时候它反而没用

书自己那个跑例恰好是反面,值得走一遍12:

库里三块(书给的原样):
① "The Great Fire of London in 1666 destroyed over 13,000 houses."
② "Julius Caesar was assassinated on the Ides of March (March 15) in 44 BCE."
③ "The Black Death is estimated to have killed nearly one-third of the
European population."

用户问:"Tell me something interesting about diseases in history"

── BM25 那一路 ──
查询里的实词:diseases、history、interesting
③ 里有 "diseases" 吗?没有。有 "history" 吗?没有。
→ 三块的字面命中都接近零,这个榜基本是瞎排的

── 按意思那一路 ──
"Black Death"(黑死病)和 "diseases in history" 在意思上贴得很近
→ ③ 稳稳排第一

图说:同一个例子,前一节 BM25 赢、这一节 BM25 输。
这正是"两者互补"的意思 —— 不是谁更好,是各管一半。
(书跑完这段代码没有给出具体分数,上面的胜负是照机制推的。)

两条一起用有个名字,以及什么时候才该用

两条路一起用、再把两个榜合成一个,这件事的名字叫混合检索。

该用的场景1:库里含有编号、代码、专有名词这类嵌入抓不住的串; 或者用户把大白话和精确串混着问。

不该用的场景,书说得同样明确13:

纯知识型检索、用户用自然语言提问、没有任何技术标识——不需要它。 「我怎么重置密码?」这种问题按意思搜已经完全抓得住意图, 加上 BM25 只增加复杂度,不改善结果。

启动条件(这一章第二次出现同一个句式)13:

先只用按意思搜。只有当你在检索日志或用户反馈里看见「精确匹配失败」,才加混合检索。

6. 承重词二:排名融合 —— 两个榜怎么合成一个

这一章第二个承重词。一句话先给结论:两条路各出一个榜, 把两个榜合成一个最终榜的那一步叫排名融合; 而书给了两种合法做法,它们合并的东西根本不同——一种合名次,一种合分数。

做法一:按名次合(书在嵌入那一章用的)

公式就写在书的代码里14:

合并分 = 1/(k + 这块在关键词榜上的名次) + 1/(k + 这块在意思榜上的名次)

书给的示例值:k = 60

为什么用"1 除以名次":名次越靠前,这一项越大。
第 1 名贡献 1/61 ≈ 0.0164,第 10 名贡献 1/70 ≈ 0.0143 —— 前者更大。

于是:两个榜上都靠前的那一块,拿到最高的合并分。

那个 k 是唯一的旋钮,书交代了它管什么15:

k 的取值效果
(60 到 100)给靠后的名次更多权重——榜首和第十名的差距被拉平了
(10 到 20)强烈偏向那些在两个榜里都排得很高的项

这一族做法的名字叫倒数排名融合(reciprocal rank fusion,常写作 RRF)—— 「倒数」指的就是那个「1 除以名次」。你在检索框架的配置里会撞见这个缩写。

做法二:按分数合(书在向量库那一章用的)

同一本书在另一章用了完全不同的做法16:不合名次,直接把两个分数加权相加。

混合分 = 关键词分 × 0.5 + 相近程度 × 0.5

书给的调法(这条可以直接带走):

先五五开

用户抱怨"相关的没出来,因为它们没有那个确切的词" → 把意思那一侧提到 0.7
用户抱怨"结果离我问的字面漂太远" → 把字面那一侧提到 0.7

领域倾向:法律与技术类通常要更高的字面权重
对话类通常要更高的意思权重

两种做法为什么不能混着理解

补充(不在书里,来自通用知识): 按名次合的那种之所以要先转成名次, 是因为两个分数本来就不可比——按意思算出来的相近程度落在 −1 到 1 之间, 而 BM25 的分数没有上限,一块文字命中得够多可以到几十。 直接相加,等于让 BM25 那一侧永远说了算。名次没有这个毛病:第一名就是第一名。

判断(我们的,不是书里的):书在两章里各给了一种做法,却从没有把它们放在一起比过, 而读者很容易以为这是同一件事的两种写法。 分界其实很清楚:按分数合的那种要求你先把两个分数拉到同一个尺度上 (书那个例子能成立,是因为它交给数据库的那个关键词分本身就落在 0 到 1 附近); 按名次合的那种不要求这个,代价是它丢掉了「赢了多少」的信息—— 第一名领先第二名一个身位还是十个身位,合并之后看不出来了。 如果错,会错在: 如果你的两个检索器分数天生就在同一个量级上 (比如两边都是归一化过的 0 到 1),那按分数合更准,因为它保住了差距的大小。 判据是:把两边的分数各抽 20 条打印出来,看它们的取值范围差几倍。

7. 主走查:那句 SSL 问题,一路走到最终榜

这是本章的主走查,前面每个机制都在它上面占一步。 用书那句混合形态的查询2

(书没有给这个查询的实测分数和名次。下面所有的分数、名次、块数都是我们为演示编的, 用来把每一步的中间结果显出来;/api/v2/users 这个查询本身和「加 BM25」这个判断是书里的。)

第 0 步:库里有什么

一份技术文档知识库,4 200 块,每块都带两样东西:
· 1 536 个数(第 08 章那一步算出来的)
· 一列 metadata,里面有 {"product": "gateway", "version": "v2"}

第 1 步:选源(第 9、10 节讲)

这套系统连着三个地方:
A 技术文档向量库 B 工单数据库(SQL) C 一个会算数的工具

问题:"How do I configure SSL for endpoint /api/v2/users?"
→ 选 A。理由:它问的是"怎么配",答案写在文档里,不在工单表里,也不用算数。

第 2 步:先缩范围(第 4 节)

WHERE metadata->>'product' = 'gateway'

4 200 块 → 剩 860 块参与后面所有比较

为什么能这么筛:用户是从网关产品的文档页进来提问的。

第 3 步:按意思那一路(第 08、09 章)

把问句换成 1 536 个数 → 在 860 块里算相近程度 → 取前 5

名次 块 相近程度
1 "TLS/SSL 证书的配置流程与常见错误" 0.71
2 "如何为服务端启用 HTTPS" 0.68
3 "API 网关的安全基线" 0.64
4 "证书轮换的运维手册" 0.60
5 "端点 /api/v2/users 的鉴权与传输加密配置" 0.58 ← 要的那块只排第 5

第 4 步:按字面那一路(第 5 节)

查询切成词:how / do / i / configure / SSL / for / endpoint / /api/v2/users

BM25 打分时:
"how" "do" "i" "for" —— 全库到处都是,几乎不加分
"SSL" —— 全库出现在 190 块里,加一点分
"/api/v2/users" —— 全库只出现在 3 块里,命中它几乎是决定性的

名次 块 BM25 分
1 "端点 /api/v2/users 的鉴权与传输加密配置" 18.4 ← 它在这个榜上第 1
2 "端点 /api/v2/users 的返回字段说明" 15.9
3 "TLS/SSL 证书的配置流程与常见错误" 6.2
4 "API 网关的安全基线" 5.1
5 "如何为服务端启用 HTTPS" 4.7

第 5 步:合成一个榜(第 6 节,按名次合,k = 60)

块 意思榜名次 字面榜名次 合并分
"端点 /api/v2/users 的鉴权与传输加密配置" 5 1 1/65 + 1/61 = 0.03178
"TLS/SSL 证书的配置流程与常见错误" 1 3 1/61 + 1/63 = 0.03227
"API 网关的安全基线" 3 4 1/63 + 1/64 = 0.03150
"如何为服务端启用 HTTPS" 2 5 1/62 + 1/65 = 0.03151
"端点 /api/v2/users 的返回字段说明" 未进前 5 2 1/60+? 见下

↑ 一块只上了一个榜怎么办?
书没有交代这一步。常见做法是给它在另一个榜上按"末位 + 1"计名次。
按 6 计:1/66 + 1/62 = 0.03128。

↑ 合并分为什么写到小数点后五位:第 3 名和第 4 名只差 0.00001。
名次融合把分数的绝对差距全抹掉了,只剩名次 —— 所以两块名次相邻时,
合出来的分也会紧紧挨着。少写一位就分不出先后。

最终榜(按合并分从高到低):
① "TLS/SSL 证书的配置流程与常见错误" 0.03227
② "端点 /api/v2/users 的鉴权与传输加密配置" 0.03178 ← 从第 5 位升到第 2 位
③ "如何为服务端启用 HTTPS" 0.03151
④ "API 网关的安全基线" 0.03150

第 6 步:把这一步的效果说清楚

要的那块排第几
纯按意思搜 5 ← 进不了"取前三"
加了按字面搜 + 排名融合 2 ← 进了

图说:注意这一步没有让任何模型变聪明,也没有重算任何一串数。
它只是把一个从 1990 年代就有的打分公式接了回来,
然后用一个只有一行的公式把两个榜合起来。

k 从 60 换成 15 会怎样? 那一块在两个榜上分别是第 5 和第 1, 而 k 小的时候「强烈偏向两个榜都靠前的项」——它在意思榜上排第 5, 会被那个惩罚拉下去这就是那个旋钮的实际手感。

8. 书里那段查询语句,演示的不是它声称的那件事

这是我们在这一章核出来的一处问题,而且它出在能直接抄去跑的代码上。

书在向量库那一章给了一整段「在一条查询里同时算两个分数」的语句。 用户的问题是 "I am looking for a job as a data scientist in Berlin.", 可那段语句里出现了三次的关键词,全都被写死成了固定的字符串 'PostgreSQL'17—— 把一个本该随每次调用变化的值直接钉在代码里,这件事有个通行的叫法:硬编码

ts_rank(tsv, plainto_tsquery('PostgreSQL')) AS text_score, ← 硬编码
1 - (embedding <=> '<查询那一串数>') AS vector_score, ← 这一半是对的
ts_rank(tsv, plainto_tsquery('PostgreSQL')) * 0.5
+ (1 - (embedding <=> '<查询那一串数>')) * 0.5 AS hybrid_score ← 又两次
FROM test_embedding_table
WHERE tsv @@ plainto_tsquery('PostgreSQL') ← 还先过滤了一道
ORDER BY hybrid_score DESC
LIMIT 10

图说:上面那个 tsv 列是这个数据库自带的全文检索列 ——
它把一段文字预先切成词、去掉词尾变化存起来,ts_rank 就是照它算字面命中分的。
这一段和 BM25 不是同一个公式,但干的是同一件事。

发生了什么:

  1. 字面那一半根本没在搜用户的问题。 它在搜「PostgreSQL」这个词—— 而用户问的是柏林的数据科学家职位;
  2. 那个 WHERE 更要命。 它先把结果过滤成「必须含有 PostgreSQL 这个词」的行, 然后才在这个子集里算混合分。这已经不是混合检索了,这是「先按关键词过滤、再排序」;
  3. 于是这段代码跑出来的东西,和它上一段文字声称在演示的东西不是一回事。

判断(我们的,不是书里的):这一处不是笔误级别的小错,它会让照抄的人得到一个 看起来在工作、实际上换了一种行为的系统。 因为它不报错、结果也不空——你会拿到十条「含有 PostgreSQL 的职位描述」, 而且它们确实按混合分排过序。只有当你换一个查询、发现结果集诡异地不动,才会起疑。 如果错,会错在: 如果这是书为了「拿一张已有的表演示语法」而故意固定的常量, 那它至少该在正文里写一句「这里为演示写死了关键词」——而书一个字没写。 判据是:把 'PostgreSQL' 三处全换成用户的查询串,再跑一遍,看结果集变不变。

9. 补法三:选源 —— 它和「缩范围」是两件事

这一节只干一件事:把两个非常容易读成同一件事的东西分开。

书自己写了这一句区分18:

它在什么时候发生它在什么之间选
选源(书里叫查询路由)打开任何一个库之前几个不同的地方之间选一个:这个向量库、那个向量库、一张 SQL 表、一个接口、一个会算数的工具
缩范围(第 4 节那道闸)打开了一个库、还没开始比之前同一个库内部把行筛掉一批

两者可以叠加,而且叠加的顺序是固定的:先选源,再在选中的那个源里缩范围。 主走查的第 1 步和第 2 步就是这个顺序。

什么时候需要选源、什么时候跳过

该用19:你有好几个明显不同的数据源,而且查询能清楚地对上其中一个; 或者「搜错地方」的代价超过了选源本身的开销——这个代价有两部分: 拿回一堆没用的东西,以及白白烧掉的算力(机器为这次查询实际干的计算量, 它直接换算成电费和云上的账单)。

跳过20:

只有一个源;或者「全都搜一遍再筛」比「先选源」还快; 或者你的查询含糊到根本分不可靠。

中间那一条容易被忽略: 选源不是免费的,它自己要花时间(下一节给数)。 源少、每个源都不大的时候,全搜一遍反而更省。

10. 选源的三条路,延迟差两个数量级

这一节是这一章最该带走的一张表。三条路干的是同一件事,代价差 40 到 200 倍。

怎么选源多花多少时间代价
让模型判断每次请求加 500 毫秒到 2 秒21慢,但不需要任何训练数据
全都搜一遍,再看哪个源命中最多和只搜一个源一样快算力翻倍(搜了 N 个源)
训一个小分类器只加 10 到 50 毫秒22要标注数据、要定期重训

书对第一条的态度值得照搬21:两秒不见得就是致命伤, 但在用户等着看回答的对话应用里会造成明显的迟滞感; 它更适合批处理、邮件自动化这类没人盯着等的场景。

对照一下就知道 500 毫秒有多重: 第 08 章那条选型标准是「交互式应用要小于 100 毫秒」, 而第 09 章测出来 20 万条不建索引全算一遍是 85.6 毫秒。 也就是说,让模型判断一次源,比整个检索本身还慢五到二十倍。

第三条路具体怎么做

书的做法是把第 08 章那串数直接当特征用23。 「特征」就是喂给一个判断程序、让它照着下判断的那几个数—— 判断一间房子值多少钱,面积、楼层、建成年份就是它的特征; 而这里的做法是把那 1 536 个数原样当成 1 536 个特征,一个都不另外加工。

训练:
每一块的 1 536 个数 → 当作这一块的特征
每一块所属的类别 → 当作答案
喂给一个随机森林分类器(一种把很多棵判断树的投票合起来的常规做法)

用的时候:
新问题 → 换成 1 536 个数 → 交给分类器 → 它给出每个类别的可能性

书那个跑例:
两份 PDF:一份深度学习史、一份英超联赛史
问 "What is the name of the top football league in England?"
→ 分类器给英超那一类 94% 的可能性(另一类 6%)

它买到了什么24:不用在全部 10 万份文档里搜, 分类器把体育类问题路由到那 2 万份体育文档——延迟降了,准确度也高了。

另起一处走查:「market performance」为什么会被体育版捞走

这是这一章另起的第二处走查,它是失手二的放大镜3

库里 10 万份文档,混着金融和体育。

用户问:"market performance"(市场表现)

── 什么都不加 ──
在全部 10 万份里比意思
体育版 "team performance"(球队表现)那批文章相近程度很高
取回三块:两块财报 + 一块球队战绩分析
模型读完,答案里混进了球队的胜率 ← 串台了

── 先分类,再搜 ──
分类器看这个问题的那 1 536 个数 → 判为 finance,可能性 0.91
只在 2 万份金融文档里比意思 ← 8 万份根本不参与
取回三块:三块都是财报

(0.91 这个数是为演示编的;94% 那个是书里的真实跑例,但那是另一个问题。)

图说:注意这一步和第 4 节那道闸的区别 ——
那道闸的值是从用户资料来的(Jim 是球迷),这里的值是从问题本身算出来的。

它的三个前提和一个硬风险

前提25:

  1. 类别清楚且稳定(体育 / 政治 / 金融这种);
  2. 每类至少 100 到 200 个已经标好的块——不够它学不出区分的模式;
  3. 文档集别太小。书说不到一万份文档、或者内容本来就同质, 训练和维护一个分类器的开销就超过收益了。

硬风险,而且这一条很少有人说破22:

分类器会犯错,而路由错了会彻底丢掉相关文档—— 不像按意思搜,它至少还能返回次优但仍然相关的结果。

这是这一章里最不对称的一条代价。 前面每一种补法失手了都只是「结果差一点」, 只有这一条失手了是「那批文档这一次根本没被看过」。

类别重叠的时候别用它,改用第 4 节那道闸

书给了明确的分界26:

"体育博彩监管" 这个问题同时属于体育和法律。
硬塞进一个类别 → 另一类的文档全丢了。

这时候该改用标注过滤:
入库时给块打上 category
查询时写 category IN ('sports', 'legal')
→ 两个类别的文档都保得住。

图说:一个是"选一个",一个是"保留几个" —— 这就是重叠时必须换做法的原因。

三条路的推荐顺序(这一章第三次出现同一个句式)

书给的路径27:

先用"让模型判断"这条 → 它慢,但先拿它验证"选源逻辑到底对不对"
↓ 等到延迟真的成了问题,而且各个源主题分得清
换成小分类器
↓ 可是如果各个源之间大量重叠
只能退回"让模型判断",并吃下那 500 到 2000 毫秒

至此,这一章出现了三次同一个句式: 「先只用按意思搜,直到日志里看见精确串没命中」「先只用按意思搜,直到看见跨领域串台」 「先用让模型判断,直到延迟成问题」。 这不是巧合,而是全书说了九遍、一次都没命名的一条原则——第 19 章把它收在一起。

11. 作者的判断与证据

说法它是什么
四类「必须精确命中」的串(编号、法条、诊断码、技术标识)是事实描述,而且例子选得准
「market performance / team performance」会串台是机制推导,和第 08 章讲的余弦相似度性质一致
「先只用按意思搜,直到日志里看见失败才加」作者的工程建议,没有实验支撑。 但它在三个不同章节里重复出现,是全书最稳定的一条态度
BM25「按精确的词项匹配排名」是事实,但书没有讲它内部怎么算——本章第 5 节那三件事是我们补的
RRF 的公式与 k = 60是代码里的实现事实;k 的大小效果(60–100 / 10–20)是作者给的经验值,没有来源
0.5 / 0.5 起步,抱怨往哪边调就往哪边加到 0.7作者的经验做法,没有来源。 但它给了明确的可操作判据(照用户抱怨的方向调)
「法律与技术偏字面、对话类偏意思」作者的领域经验,没有数据
「让模型判断加 500 毫秒到 2 秒」「分类器加 10 到 50 毫秒」作者给的经验区间,没有测量条件(什么模型、什么机器,一个字没有)
「全都搜一遍再投票,延迟不变、算力翻倍」是机制推导,成立
94% 那个置信度是书里跑出来的一次真实输出,但那是两份 PDF 的玩具例子,不能当作分类器精度的证据
「10 万份文档里只搜 2 万份」是举例说明的数,不是实测
每类至少 100–200 个标注块作者给的经验门槛,没有来源
「路由错了会彻底丢掉相关文档」是机制推导,而且是这一章最硬的一条反面判据
「路由器往往只是一个写得好的提示词」作者的判断28,用来劝人别急着为它加框架依赖

12. 边界与局限

  • 书没有讲一块只上了一个榜时怎么合并。 主走查第 5 步那个情况在真实系统里很常见 (某块字面命中很高、但意思榜前 50 名都没进),而书的公式默认两个名次都存在;
  • 书没有把两种融合做法放在一起比过。 同一本书里一章用名次、另一章用分数, 中间隔了一章,没有一句话交代它们的分工;
  • 那段混合检索的查询语句是错的(第 8 节),而且书没有勘误;
  • BM25 的内部机制一个字没讲。 对一本要求读者「不必懂机器学习」的书来说, 这里留了一个不小的坑——照抄能跑,但调不动;
  • 三条选源路的延迟数没有测量条件。 用的是什么模型、什么机器、多长的查询, 一个字没有,所以那三个数只能当量级看;
  • 书没有讲选源本身错了怎么办。 它坦白了「路由错会彻底丢文档」, 但没有给任何兜底做法(比如「置信度低于某个值就全都搜一遍」);
  • 书没有讲混合检索和选源怎么配合。 主走查里我们把它们串成了一条线, 但这条线是我们排的,书里没有一处把两者接在一起。

13. 可带走的

  1. 按意思搜有三类固定的失手: 精确串(编号、法条、接口路径)搜不到、 两个领域的同一个词串台、以及这个问题根本不该问这个库。 三种补法一一对应,而且发生在三个不同的时刻;
  2. 先缩范围那道闸,在查询语句里就是把 WHERE 写在 ORDER BY 之前—— 不合条件的行在算相近程度之前就被扔掉了,不是搜出来再筛;
  3. 闸门的值可以来自用户资料,也可以让一个便宜的小模型现场判断—— 难的那一步(读复杂片段、下结论)才值得上大模型;
  4. 别在做相似度搜索的同一条查询里做复杂的多表连接,先在子查询里过滤;
  5. BM25 是按字面命中打分的老办法,它同时看三件事: 这个词出现了几次(收益递减)、这个词在全库里有多罕见(越罕见越值钱)、 这块文字有多长(长的打折)。它不是被淘汰的东西,是管另一半事的东西;
  6. 两条路一起用叫混合检索。启动条件很硬:先只用按意思搜, 直到你在检索日志里亲眼看见精确串没命中,才加它;
  7. 合并两个榜有两种合法做法,别混:名次合(1/(k+名次) 相加,k 小就强烈 偏向两个榜都靠前的)、按分数加权(先五五开,照用户抱怨的方向往 0.7 调);
  8. 书里那段在数据库里做混合检索的语句把关键词硬编码成了另一个词, 还先按那个词过滤了一道——照抄会得到一个看起来在工作、实际换了行为的系统;
  9. 「选源」和「缩范围」是两件事: 前者在打开库之前、在几个地方之间选一个; 后者在一个库内部筛行。可以叠加,顺序固定:先选源,再缩范围;
  10. 选源三条路差两个数量级: 让模型判断 +500 到 2000 毫秒、 全搜一遍再投票(延迟不变、算力翻倍)、小分类器 +10 到 50 毫秒;
  11. 推荐顺序:先用让模型判断验证逻辑对不对,等延迟成问题、且各源主题分得清,再换分类器; 源之间大量重叠时只能吃下那个延迟;
  12. 分类器有一条别人很少说的硬风险:它错了会彻底丢掉相关文档, 而按意思搜至少还能返回次优但仍然相关的结果;
  13. 类别之间重叠时别用分类器,改用标注过滤(category IN ('sports','legal')), 这样两个类别的文档都保得住。

14. 原文地图

主题原书章原文位置
混合检索该用在哪(编号、法条、诊断码、技术标识)Chapter 5. Embeddingstext/07-ch05-chapter-5-embeddings.txt:827(搜「SKU-4792」)
大白话与精确串混着问Chapter 5. Embeddingstext/07-ch05-chapter-5-embeddings.txt:829(搜「configure SSL for endpoint」)
BM25 是干什么的Chapter 5. Embeddingstext/07-ch05-chapter-5-embeddings.txt:701(搜「BM25 to rank documents based on exact term matches」) · text/07-ch05-chapter-5-embeddings.txt:713(搜「product names, IDs, codes」)
三块文字那个跑例Chapter 5. Embeddingstext/07-ch05-chapter-5-embeddings.txt:729(搜「The Great Fire of London」) · text/07-ch05-chapter-5-embeddings.txt:740(搜「diseases in history」)
不该用混合检索、以及启动条件Chapter 5. Embeddingstext/07-ch05-chapter-5-embeddings.txt:831(搜「reset my password」) · text/07-ch05-chapter-5-embeddings.txt:833(搜「Add hybrid search only when you observe」)
按名次合的公式Chapter 5. Embeddingstext/07-ch05-chapter-5-embeddings.txt:792(搜「k = 60」) · text/07-ch05-chapter-5-embeddings.txt:809(搜「rrf_score」)
k 控制多偏向榜首Chapter 5. Embeddingstext/07-ch05-chapter-5-embeddings.txt:836(搜「higher k (60–100) gives more weight」)
两个信号互补Chapter 5. Embeddingstext/07-ch05-chapter-5-embeddings.txt:823(搜「two independent ranking signals」)
串台那个例子Chapter 5. Embeddingstext/07-ch05-chapter-5-embeddings.txt:670(搜「market performance」)
分类器怎么训、94%Chapter 5. Embeddingstext/07-ch05-chapter-5-embeddings.txt:649(搜「94% confident」) · text/07-ch05-chapter-5-embeddings.txt:666(搜「1,536-dimensional embedding space」)
10 万份里只搜 2 万份Chapter 5. Embeddingstext/07-ch05-chapter-5-embeddings.txt:668(搜「100,000 documents」)
分类器的前提与它的硬风险Chapter 5. Embeddingstext/07-ch05-chapter-5-embeddings.txt:672(搜「100–200 labeled chunks」) · text/07-ch05-chapter-5-embeddings.txt:678(搜「misrouting queries loses relevant documents entirely」)
类别重叠时改用标注过滤Chapter 5. Embeddingstext/07-ch05-chapter-5-embeddings.txt:674(搜「sports betting regulations」)
文档集太小就别训分类器Chapter 5. Embeddingstext/07-ch05-chapter-5-embeddings.txt:676(搜「Add classification only when you observe」)
按分数加权与调权重Chapter 6. Vector Databases and Similarity Searchestext/08-ch06-chapter-6-vector-databases-and-similarity-search.txt:957(搜「combining them with weights」) · text/08-ch06-chapter-6-vector-databases-and-similarity-search.txt:965(搜「Start with equal weights」)
那段硬编码的查询语句Chapter 6. Vector Databases and Similarity Searchestext/08-ch06-chapter-6-vector-databases-and-similarity-search.txt:938(搜「plainto_tsquery('PostgreSQL')」) · text/08-ch06-chapter-6-vector-databases-and-similarity-search.txt:945(搜「WHERE tsv @@」)
混合检索 / 条件过滤 / 纯语义各管什么Chapter 6. Vector Databases and Similarity Searchestext/08-ch06-chapter-6-vector-databases-and-similarity-search.txt:967(搜「Use SQL filtering when you need to restrict by metadata」)
别做复杂多表连接Chapter 6. Vector Databases and Similarity Searchestext/08-ch06-chapter-6-vector-databases-and-similarity-search.txt:680(搜「Filter first in a subquery」)
体育网站那个跑例Chapter 7. Retrievaltext/09-ch07-chapter-7-retrieval.txt:81(搜「best player of all time」) · text/09-ch07-chapter-7-retrieval.txt:85(搜「Jim is a football fan」) · text/09-ch07-chapter-7-retrieval.txt:187(搜「Lionel Messi」)
那张表和那句查询Chapter 7. Retrievaltext/09-ch07-chapter-7-retrieval.txt:107(搜「metadata JSONB」) · text/09-ch07-chapter-7-retrieval.txt:179(搜「metadata->>'topic'」)
让模型自己提过滤条件Chapter 7. Retrievaltext/09-ch07-chapter-7-retrieval.txt:191(搜「propose the best metadata filters」) · text/09-ch07-chapter-7-retrieval.txt:193(搜「fast and efficient model for this step」)
过滤发生在搜之前、以及什么时候跳过Chapter 7. Retrievaltext/09-ch07-chapter-7-retrieval.txt:197(搜「filter by metadata first」) · text/09-ch07-chapter-7-retrieval.txt:201(搜「topically homogeneous」)
可测性的收益Chapter 7. Retrievaltext/09-ch07-chapter-7-retrieval.txt:203(搜「Testability improves significantly」)
87 乘 99 那个例子Chapter 7. Retrievaltext/09-ch07-chapter-7-retrieval.txt:424(搜「multiplying 87 by 99」)
什么时候该选源、什么时候跳过Chapter 7. Retrievaltext/09-ch07-chapter-7-retrieval.txt:514(搜「Use routing when you have multiple distinct data sources」) · text/09-ch07-chapter-7-retrieval.txt:516(搜「Skip routing when you have a single data source」)
三条路的延迟Chapter 7. Retrievaltext/09-ch07-chapter-7-retrieval.txt:518(搜「adds latency of 500 ms to 2 seconds」) · text/09-ch07-chapter-7-retrieval.txt:520(搜「Majority vote」) · text/09-ch07-chapter-7-retrieval.txt:524(搜「Embedding classifier」)
推荐顺序Chapter 7. Retrievaltext/09-ch07-chapter-7-retrieval.txt:528(搜「Start with LLM routing to validate」)
选源和缩范围的区别Chapter 7. Retrievaltext/09-ch07-chapter-7-retrieval.txt:530(搜「routing selects from separate data sources」)
路由器往往只是一个提示词Chapter 7. Retrievaltext/09-ch07-chapter-7-retrieval.txt:538(搜「a router is often just a well-crafted prompt」)

Footnotes

  1. 出处:「Chapter 5. Embeddings」第 827 段(text/07-ch05-chapter-5-embeddings.txt:827,搜「SKU-4792」)。原文列的四类是商品 SKU、法条编号(「article 230」不同于「section 230」)、ICD-10 医疗诊断码、技术标识(接口名、数据库表名)。补充(不在书里,来自通用知识):ICD-10 是世界卫生组织维护的国际疾病分类第十版,每一种诊断对应一个字母加数字的编码。 2

  2. 出处:同章第 829 段(text/07-ch05-chapter-5-embeddings.txt:829,搜「configure SSL for endpoint」)。原文说这类问题「既得益于对 configure SSL 的语义理解,也得益于对那个端点路径的精确匹配」。补充(不在书里,来自通用知识):SSL 是给网络传输加密的一套做法,今天实际在用的是它的后继 TLS,但接口和文档里仍普遍沿用 SSL 这个叫法。 2

  3. 出处:同章第 670 段(text/07-ch05-chapter-5-embeddings.txt:670,搜「market performance」)。原文的说法是嵌入模型「觉得它们在语义上相似」,所以问市场表现会捞回讲球队表现的体育文章。 2

  4. 出处:「Chapter 7. Retrieval」第 424 段(text/09-ch07-chapter-7-retrieval.txt:424,搜「multiplying 87 by 99」)。

  5. 出处:同章第 107 段(text/09-ch07-chapter-7-retrieval.txt:107,搜「metadata JSONB」)是建表语句里那一列;查询写法见第 179 段(text/09-ch07-chapter-7-retrieval.txt:179,搜「metadata->>'topic'」)。补充(不在书里,来自通用知识):JSONB 是 PostgreSQL 存 JSON 的一种列类型,存进去时就被解析成二进制结构,所以可以直接按字段名查询和建索引。

  6. 出处:场景与那句问话见同章第 81 段(text/09-ch07-chapter-7-retrieval.txt:81,搜「best player of all time」);Jim 那个设定见第 85 段(text/09-ch07-chapter-7-retrieval.txt:85,搜「Jim is a football fan」);四块样例数据见第 116 段起(text/09-ch07-chapter-7-retrieval.txt:118,搜「Roger Federer」);梅西那条排在最高位见第 187 段(text/09-ch07-chapter-7-retrieval.txt:187,搜「Lionel Messi」)。「不关闸会取回什么」是我们照机制推的,书没有跑这一遍。

  7. 出处:同章第 191 段(text/09-ch07-chapter-7-retrieval.txt:191,搜「propose the best metadata filters」)与第 193 段(text/09-ch07-chapter-7-retrieval.txt:193,搜「fast and efficient model for this step」)。

  8. 出处:「Chapter 6. Vector Databases and Similarity Searches」第 680 段(text/08-ch06-chapter-6-vector-databases-and-similarity-search.txt:680,搜「Filter first in a subquery」)。这一条第 09 章第 6 节已经引过一次,这里是它在「先缩范围」这个语境下的落点。

  9. 出处:「Chapter 7. Retrieval」第 201 段(text/09-ch07-chapter-7-retrieval.txt:201,搜「topically homogeneous」)。机制那句「先按标注过滤,再只在那个子集上算余弦相似度」见第 197 段(text/09-ch07-chapter-7-retrieval.txt:197,搜「filter by metadata first」)。

  10. 出处:同章第 203 段(text/09-ch07-chapter-7-retrieval.txt:203,搜「Testability improves significantly」)。原文举的是「足球对网球」这个分法。

  11. 出处:「Chapter 5. Embeddings」第 701 段(text/07-ch05-chapter-5-embeddings.txt:701,搜「BM25 to rank documents based on exact term matches」)与第 713 段(text/07-ch05-chapter-5-embeddings.txt:713,搜「product names, IDs, codes」)。全书没有一处讲 BM25 内部怎么算。 补充(不在书里,来自通用知识):BM25 出自 1990 年代 Okapi 检索系统的一族排序函数(Best Matching),编号 25 的那一版最常用;它的分数由词频(带饱和)、逆文档频率、文档长度归一化三部分构成,代码里用的 rank_bm25 包实现的就是它,见第 725 段(text/07-ch05-chapter-5-embeddings.txt:725,搜「from rank_bm25 import BM25Okapi」)。

  12. 出处:三块文字见同章第 729 段(text/07-ch05-chapter-5-embeddings.txt:729,搜「The Great Fire of London」)起;查询串见第 740 段(text/07-ch05-chapter-5-embeddings.txt:740,搜「diseases in history」)。书跑完这段代码没有给出两个榜的具体分数,上面那次胜负是我们照机制推的。两个信号互补这一点书自己说了,见第 825 段(text/07-ch05-chapter-5-embeddings.txt:825,搜「keyword and semantic signals are complementary」)。

  13. 出处:同章第 831 段(text/07-ch05-chapter-5-embeddings.txt:831,搜「reset my password」)与第 833 段(text/07-ch05-chapter-5-embeddings.txt:833,搜「Add hybrid search only when you observe」)。 2

  14. 出处:同章第 809 段(text/07-ch05-chapter-5-embeddings.txt:809,搜「rrf_score」)是那段公式的代码;k = 60 见第 792 段(text/07-ch05-chapter-5-embeddings.txt:792,搜「k = 60」),那一行上面的注释写着「k 越大,最靠前那几个名次的影响越小」。整段做法的引出句见第 697 段(text/07-ch05-chapter-5-embeddings.txt:697,搜「reciprocal rank fusion」)。

  15. 出处:同章第 836 段(text/07-ch05-chapter-5-embeddings.txt:836,搜「higher k (60–100) gives more weight」)。

  16. 出处:「Chapter 6. Vector Databases and Similarity Searches」第 957 段(text/08-ch06-chapter-6-vector-databases-and-similarity-search.txt:957,搜「combining them with weights」)讲机制;调权重那一套见第 965 段(text/08-ch06-chapter-6-vector-databases-and-similarity-search.txt:965,搜「Start with equal weights」)。三种做法各管什么见第 967 段(text/08-ch06-chapter-6-vector-databases-and-similarity-search.txt:967,搜「Use SQL filtering when you need to restrict by metadata」)。

  17. 出处:同章第 938 段(text/08-ch06-chapter-6-vector-databases-and-similarity-search.txt:938,搜「plainto_tsquery('PostgreSQL')」)是三处硬编码里的第一处,第 940、942 段是另两处;那个先过滤的条件见第 945 段(text/08-ch06-chapter-6-vector-databases-and-similarity-search.txt:945,搜「WHERE tsv @@」)。用户的查询串是「I am looking for a job as a data scientist in Berlin.」,与第 09 章那个跑例同一句。建全文检索列的语句见第 901 段(text/08-ch06-chapter-6-vector-databases-and-similarity-search.txt:901,搜「to_tsvector(text_chunk)」)。

  18. 出处:「Chapter 7. Retrieval」第 530 段(text/09-ch07-chapter-7-retrieval.txt:530,搜「routing selects from separate data sources」)。原文还明说两者可以合起来用:先选源,再在源内按类别过滤。

  19. 出处:同章第 514 段(text/09-ch07-chapter-7-retrieval.txt:514,搜「Use routing when you have multiple distinct data sources」)。

  20. 出处:同章第 516 段(text/09-ch07-chapter-7-retrieval.txt:516,搜「Skip routing when you have a single data source」)。

  21. 出处:同章第 518 段(text/09-ch07-chapter-7-retrieval.txt:518,搜「adds latency of 500 ms to 2 seconds」)。「两秒不见得致命、但对话应用里迟滞感明显」「更适合批处理和邮件自动化」都在这一段。 2

  22. 出处:多数表决那条见同章第 520 段(text/09-ch07-chapter-7-retrieval.txt:520,搜「Majority vote」),嵌入分类器那条见第 524 段(text/09-ch07-chapter-7-retrieval.txt:524,搜「Embedding classifier」)。「路由错了会彻底丢掉相关文档」见「Chapter 5. Embeddings」第 678 段(text/07-ch05-chapter-5-embeddings.txt:678,搜「misrouting queries loses relevant documents entirely」),同一段还给了 10–20 毫秒这个另一个区间——注意这两个数出自不同章节:检索那一章写 10 到 50 毫秒,嵌入那一章写 10 到 20 毫秒,书自己没有对齐。 2

  23. 出处:「Chapter 5. Embeddings」第 666 段(text/07-ch05-chapter-5-embeddings.txt:666,搜「1,536-dimensional embedding space」)讲机制;训练代码见第 645 段(text/07-ch05-chapter-5-embeddings.txt:645,搜「Train a random forest classifier」);94% 那次输出见第 649 段(text/07-ch05-chapter-5-embeddings.txt:649,搜「94% confident」)与第 660 段(text/07-ch05-chapter-5-embeddings.txt:660,搜「predict_proba」)。补充(不在书里,来自通用知识):随机森林是把很多棵各自独立生成的判断树合起来投票的一种常规做法,不需要显卡,几百条样本就能训。

  24. 出处:同章第 668 段(text/07-ch05-chapter-5-embeddings.txt:668,搜「100,000 documents」)。

  25. 出处:同章第 672 段(text/07-ch05-chapter-5-embeddings.txt:672,搜「100–200 labeled chunks」)与第 676 段(text/07-ch05-chapter-5-embeddings.txt:676,搜「Add classification only when you observe」)。

  26. 出处:同章第 674 段(text/07-ch05-chapter-5-embeddings.txt:674,搜「sports betting regulations」)。

  27. 出处:「Chapter 7. Retrieval」第 528 段(text/09-ch07-chapter-7-retrieval.txt:528,搜「Start with LLM routing to validate」)。

  28. 出处:同章第 538 段(text/09-ch07-chapter-7-retrieval.txt:538,搜「a router is often just a well-crafted prompt」)。这是一条提醒框里的话,原意是:各家框架都有现成的路由组件,但加依赖之前先想清楚需不需要。