检索要回答的不是「有没有」,而是「有多像」。所以它的索引和数据库长得很不一样。 这一节建一张倒排索引,用 TF-IDF 排序,再量一量布尔检索到底丢了什么。
上一节的索引是「按主键查一行」,这一节的索引是「按词查一批文档」。 一个求精确,一个求相关,索引结构因此完全不同。 它和 11.2 的 TF-IDF 是同一套东西的两面: 那边用它给词算权重、看谁跟谁像;这边用它给文档打分、看哪篇该排前面。
一、倒排索引:把翻文档变成查表
十二篇小文档,一共 62 个不同的词。正排索引是「文档 → 词」,倒排反过来:「词 → 文档」。
| 词 | 出现在哪几篇 | 篇数 |
|---|---|---|
| 数据 | 1、4、5、9、11 | 5 |
| 索引 | 3、7、10 | 3 |
| 查询 | 3、4 | 2 |
| 网络 | 2、5、9 | 3 |
| 查「数据」的时候不用读十二篇文档,直接看这一行。 | ||
| 这就是倒排索引的全部意义。 | ||
| 它的规模很吓人:Demo 里平均每个词的 postings 只有 1.50 篇、 | ||
| 平均每篇 7.8 个词;真实搜索引擎的词典有几千万个词、postings 有几亿条。 |
二、TF-IDF:让结果有先后
命中往往不止一篇,得排序。两个信号:
| 信号 | 含义 | 公式 |
|---|---|---|
| TF | 这个词在这篇里出现几次 | 词频 |
| IDF | 这个词在整个库里有多稀有 | log(N/df) |
| 两者相乘就是权重,再用余弦比「查询向量」和「文档向量」的夹角。 | ||
| Demo 里查询「数据 训练 模型」,前三名是第 1 篇(0.7556)、第 11 篇(0.2501)、第 9 篇(0.0516)。 | ||
| 第 1 篇三个词全有,分数遥遥领先。 | ||
| 注意第 3 名只有 0.0516,和第 1 名差了十四倍。 | ||
| 这个差距不是 bug——余弦相似度本来就可以很小,排序只看相对大小。 | ||
| 「的」「和」这类词的 IDF 接近 0,几乎不影响排序。 | ||
| 这正是 IDF 存在的理由:把到处都有的词自动降权。 |
三、布尔检索:说得准,可它只会回答是或否
布尔查询「机器学习 AND 数据」,命中第 1 篇和第 11 篇,两篇。看着很准——两篇都真的相关。 可四篇真正相关的文档里,它只给出了两篇。 准确率 1.0000,召回率只有 0.5000。 同一句话交给排序,前三是 1、11、6。 第 6 篇在布尔里直接出局(它没提「数据」),可在排序里排第三。 布尔没有「多命中一点」这种说法,只有命中和不命中。 这就是搜索引擎一律用排序、不用布尔的主要原因。 顺便说清两个指标。准确率是「给你的里面有多少对」。召回率是「对的里面你给了多少」。 它们天生互相拉扯——多给结果召回上去,准确率就下来。
四、求交的代价:一千万次对八千次
布尔查询要算交集。两万篇文档、两个词各命中五千篇,两种做法:
| 办法 | 比较次数 | 耗时 |
|---|---|---|
| 逐个比 | 12604315 | 2321.95 毫秒 |
| 归并求交 | 8753 | 5.28 毫秒 |
| 差 1440 倍。 | ||
| 因为 postings 本来就是按文档编号排好序的,归并只需要两边各走一遍。 | ||
| 逐个比那种写法把「有序」这个信息完全浪费了。 | ||
| 真实搜索引擎还会在这个基础上再加跳表,一跳跳过一大段。 | ||
| 所以索引压缩和跳表不是「锦上添花的优化」,是几亿条 postings 能不能跑起来的问题。 |
五、动手 Demo:先建一张词到文档的表
五段:建倒排索引、TF-IDF 排序、布尔与排序对撞、求交代价、准确率与召回率。 全部手写,没有用检索库。 语料只有十二篇,刚好够看清每一步在干什么。
POST = {} # 倒排索引:词 -> 文档编号列表
for i, d in enumerate(DOCS):
for w in d.split():
POST.setdefault(w, []).append(i)
IDF = np.log(len(DOCS) / (X > 0).sum(0)) # 越稀有的词越有区分度
W = X * IDF # tf 乘 idf
def rank(q):
qv = np.zeros(len(VOC))
for w in q.split():
if w in IDX:
qv[IDX[w]] += 1.0
qv = qv * IDF
return W @ qv / (NRM * np.sqrt((qv ** 2).sum())) # 余弦
实跑输出:
--------------------------------------------------------------------
先建一张「词 → 文档」的表
--------------------------------------------------------------------
==============================================================
[1] 倒排索引:不问「这篇文档有什么词」,问「这个词在哪几篇」
12 篇文档,一共 62 个不同的词。
建一张表:每个词对应一串文档编号(这叫 postings 列表)。
词 出现在哪几篇 篇数
数据 1、4、5、9、11 5
索引 3、7、10 3
查询 3、4 2
网络 2、5、9 3
知识 6、8 2
模型 1、11 2
查「数据」:不用读十二篇文档,直接看这一行,5 篇。
**倒排索引的全部意义就在这儿:把「翻文档」变成「查表」。**
平均每个词的 postings 长度 1.50 篇,最长的词是「数据」(5 篇)。
[2] TF-IDF:光看有没有不够,还要排个先后
一个查询常常命中好几篇,得给它们排序。两个信号:
**TF(这篇里出现几次,越多越相关)和 IDF(整个库里越稀有越有区分度)。**
权重取 tf × log(N/df),再用余弦比相似度。
查询「数据 训练 模型」:
第 1 篇 相似度 0.7556 机器学习 需要 大量 数据 训练 模型
第 11 篇 相似度 0.2501 机器学习 模型 的 效果 取决于 数据 质量
第 9 篇 相似度 0.0516 神经 网络 需要 大量 数据 和 算力
查询「索引 查询」:
第 3 篇 相似度 0.4890 数据库 用 索引 加快 查询 速度
第 4 篇 相似度 0.2521 关系 数据库 把 数据 存成 表 用 SQL 查询
第 10 篇 相似度 0.1359 搜索引擎 先 建 索引 再 排序 结果
查询「知识 表示」:
第 6 篇 相似度 0.5262 人工智能 包括 机器学习 和 知识 表示
第 8 篇 相似度 0.4206 知识 图谱 用 三元组 表示 实体 之间 的 关系
第 1 篇 相似度 0.0000 机器学习 需要 大量 数据 训练 模型
**同一个词在不同文档里权重不同,靠的就是 IDF。**
**「的」「和」这种词 IDF 接近 0,几乎不影响排序。**
[3] 布尔检索:说得准,可它只会回答「是」或「否」
布尔查询「机器学习 AND 数据」——两篇都含这两个词才算命中:
命中文档:1、11(2 篇)
同一句话交给排序:前三是 1、11、6
只被排序选中的 6
布尔选中的 1、11
**布尔只有「命中」和「不命中」,没有「多命中一点」这种说法。**
一篇只提了「机器学习」没提「数据」的文档,在布尔里直接出局,
可在排序里它可能排第二。**这就是搜索引擎一律用排序、不用布尔的主要原因。**
[4] 索引有多大:词典、postings 和跳表
项目 数量
词典里的词 62
postings 总长 93
平均每篇几个词 7.8
两词求交(A AND B)要按编号往后跳着比。真实规模下差别很大:
办法 比较次数 耗时(毫秒) 结果
逐个比 12604315 2043.84 1244 篇
归并求交 8753 5.98 1244 篇
两万篇文档、两个词各命中五千篇:逐个比要 1260 万次,归并只要 8753 次。
**逐个比做了 1260 万次比较,归并只做了 8753 次——差 1440 倍。**
这还没算跳表。真实搜索引擎的词典有几千万个词、postings 有几亿条。
**所以压缩和跳过不是优化,是能不能跑起来的问题。**
[5] 怎么知道排得好不好:准确率和召回率
手工标了四篇真正相关的文档:1、2、9、11。
拿查询「机器学习 数据」看前三条:
前 3 条:命中 2 篇,准确率 0.6667,召回率 0.5000
前 5 条:命中 4 篇,准确率 0.8000,召回率 1.0000
**准确率是「给你的里面有多少是对的」,召回率是「对的里面你给了多少」。**
两个数一起涨很难——多给结果召回上去,准确率就下来。
**前面那个布尔查询命中的 2 篇全都相关,准确率是 1.0000。**
**可四篇相关的它只给出两篇,召回率只有 0.5000。**
五段连起来:**倒排索引把「翻文档」变成「查表」**;
**TF-IDF 让结果有先后**;**布尔只能回答有没有,所以搜索必须排序**;
**真实索引大到必须压缩**;**而好不好用,得靠准确率和召回率一起量。**
第 1 段那张表要横着看:「数据」出现在五篇里,「查询」只出现在两篇。 篇数少说明这个词有区分度,这正是 IDF 要用的信息。 第 2 段三个查询的分数衰减得很快。 这不是模型不好,是余弦的常态——只有第一篇真的三个词全中。 第 3 段是这一节的转折:布尔命中两篇全对,可它漏了两篇。 「准确率 1.0000、召回率 0.5000」这句话,比任何解释都清楚。 第 4 段那两个数(1260 万对 8753)差 1440 倍,原因只有一个: postings 是有序的,而归并利用了这一点。
30 秒实验
- 把第 1 段的语料加五篇讲「数据库」的文档,重跑第 2 段查询「索引 查询」。 前几名会变——IDF 跟着库变了,同样的查询在更大的库里排名不同。
- 把第 3 段的布尔查询换成「机器学习 OR 数据」,重跑。 召回率会冲到 1.0000,准确率会掉下来——OR 就是拿准确率换召回率的开关。
- 把第 4 段的两个 postings 长度从 5000 改成 500,重跑。 差距会从 1440 倍缩小到一百多倍——嵌套比是平方,归并是线性,规模越小差别越小。
常见误区
- 以为「检索就是数据库加个 like」。like 是全表扫描加字符串匹配, 而且它不能排序。真正让它变成检索的,是倒排索引和相关性打分。
- 以为「TF-IDF 里的 IDF 越极端越好」。IDF 高只说明稀有。 一个错别字也很稀有,可它不该排在第一位。
- 以为「准确率和召回率能一起涨」。在不换模型的前提下, 它们就是一条曲线上的两端。想同时涨只有一个办法:换个更好的模型。
- 以为「相关就是包含关键词」。第 3 段那篇没提「数据」的文档, 在排序里排第三,而且它确实相关——语义相关不等于字面包含。
自测三问(点开对答案)
1. 倒排索引为什么比正排快? —— 因为**查询通常是给一个词、找一批文档**。 **正排要从每篇文档里翻这个词在不在,倒排直接拿到一串编号。** **索引的方向应该跟着查询的方向走,这是所有索引的共同原则。** 2. 为什么布尔检索的准确率能到 1.0000,召回率却只有 0.5000? —— 因为**AND 要求两个词都出现,条件很严**。 **严的好处是命中的基本都是对的(准确率高),坏处是漏掉很多(召回率低)。** **把 AND 换成 OR,两个数就调个个儿。** 3. 归并求交为什么快这么多? —— 因为**postings 天然按文档编号有序**。**有序就能「两边各走一遍」,代价是两边长度之和;** **无序就只能「左边每个都去右边找一遍」,代价是乘积。** **Demo 第 4 段:1260 万对 8753,差 1440 倍。**检索解决的是「找得到」,下一节往上一层:让机器不只是找到,还能「懂关系」。 把知识拆成一句一句的主谓宾,再让规则把没写下的事实推出来。
- ☐ (本节完,下一节 12.3 语义网络和语义网)