Skip to content

相似度 0.86,这两段文字其实毫不相干 ​

给知识库检索定了一条规则:余弦相似度超过 0.7 才算相关。换了一个嵌入模型之后,几乎所有的文档对都超过了 0.7,连毫不相干的也有 0.86。另一处,接口要求返回 10 条结果,加上「只看本租户」的过滤之后,经常一条也没有。再有,把精确检索换成向量索引之后,同一个查询的前 10 条里有两条换了。

向量检索的接口很简单:给一个向量,返回最相似的 k 个。围绕它的几个数字却各有各的口径:相似度的数值代表什么,索引返回的是不是真正最近的 k 个,过滤条件加在检索之前还是之后,以及「最近」和「相关」是不是一回事。

本文用合成的向量把这四个口径分别量出来。数据是固定种子生成的:64 维,200 个主题,20000 篇文档,每篇是「主题方向加随机噪声」,不涉及任何真实的模型。它说明的是机制与方向;具体数值换一个模型或语料就会不同。实验见文末。

一、用哪种距离,结果一样吗 ​

常见的三种度量是余弦相似度、内积和欧氏距离。它们是否给出相同的排序,取决于向量有没有归一化成单位长度。

向量余弦与内积的前 10 条重合余弦与欧氏距离的前 10 条重合
归一化之后100%100%
不归一化(长度在 0.5 到 3 之间)32%20%

向量都是单位长度时,三种度量在数学上给出同一个排序,选哪个只影响计算成本。不归一化时它们量的是不同的东西:

  • 内积偏向长的向量。实验里全部文档的平均长度是 1.76,内积取回的前 10 条平均长度 2.72。
  • 欧氏距离偏向和查询一样长的向量。查询是单位长度,取回的前 10 条平均长度只有 0.65。
  • 余弦只看方向。

由此得到两条规则。第一,模型的文档里说明了应该用哪种度量、输出是否已经归一化,按它来;拿不准时自己归一化再用内积,结果与余弦一致。第二,写入索引时用的度量和查询时用的度量必须是同一个。建索引时选了欧氏距离、查询时按余弦理解分数,不会报错,只是排序不是想要的那个。

二、相似度的数值不是相关的概率 ​

余弦相似度的取值范围是 -1 到 1,很容易把它读成「有多相关」,进而定一个阈值。但同一个数值在不同条件下的含义相差很大。

维度。 随机生成的、毫无关系的向量对:

维度余弦的第 95 百分位超过 0.3 的比例
80.5921.64%
640.200.75%
7680.06接近 0

8 维时,无关的向量对有五分之一超过 0.3;768 维时 0.3 已经是极强的信号。维度越高,随机向量越接近互相垂直。

向量的整体分布。 上面说的是均匀分布的随机向量,真实的嵌入向量往往不是这样:所有向量都偏向空间里的某一个方向,于是任意两个向量之间都有不低的相似度。实验里用「给所有向量加上同一个方向」来模拟这种情况:

阈值 0.7原始向量不同主题 -0.02同主题 0.41加上共同方向不同主题 0.86同主题 0.92减去均值后不同主题 -0.03同主题 0.410.00.20.40.60.81.0余弦相似度随机的无关向量对,余弦的第 95 百分位:8 维 0.59,64 维 0.20,768 维 0.06近似索引只查 1 个桶:前 10 条的召回 0.86;查 10 个桶 0.96;全查才是 1先取前 10 条再按租户过滤:平均只剩 0.6 条,一半以上的查询一条不剩精确的最近邻里也有不相关的:前 10 条全部同主题的查询占 82%
图 1 · 实测(合成向量):同一批文档,主题关系完全没变。上面一行里,不同主题的文档对余弦接近 0,同主题约 0.41。给所有向量加上同一个方向之后(真实的嵌入模型常有这种整体偏移),不同主题的也到了 0.86。阈值 0.7 在第一种分布下一个不同主题的都放不过,在第二种分布下全部放过。相似度的数值只在同一个模型、同一种处理方式之内可以互相比较,它不是「相关的概率」。
向量同主题文档对不同主题文档对阈值 0.7 放过的不同主题对
原始0.41-0.020
加上共同方向0.920.86100%
减去全体均值后0.41-0.030

文档之间的主题关系一点没变,只是整体平移了一下,0.86 就成了「不相干」对应的数值。阈值 0.7 从一个都不放过变成全部放过。

这解释了开头的第一个现象:阈值是和模型绑定的。换模型、换模型版本、换归一化或预处理方式,都要在自己的数据上重新确定。确定的办法是拿一批标注好相关与否的文档对,看两类的分数各自落在哪里;实验里第 1 名的结果,与查询同主题时余弦的中位数是 0.61,不同主题时是 0.52,两类的分数是重叠的,没有哪个阈值能把它们干净地分开。

整体平移对排序的影响小一些,但不是没有:加上共同方向前后,前 10 条结果重合 82%。

三、索引返回的不一定是最近的 ​

逐个比较全部向量得到的是精确结果,文档多了之后太慢。向量索引为了快,只检查一部分候选,返回的是近似的最近邻。实验里实现了最简单的一种:先把文档聚成 100 个桶,查询时只在离查询最近的几个桶里找。

查几个桶扫描的文档比例前 10 条的召回10 条全部找对的查询
11.1%0.85666%
22.1%0.89370%
55.2%0.92976%
1010.2%0.95780%
2020.1%0.98288%
100(全部)100%1.000100%

这里的召回,指的是「精确结果的前 10 条里,索引找回了几条」。只查一个桶时要比较的文档只有全量的百分之一,代价是三分之一的查询至少漏掉一条真正的近邻:那条文档被分到了旁边的桶里。

各种索引(分桶的、基于图的)都有这样一个在速度和召回之间调节的参数,而且默认值通常偏向速度。这意味着:

  • 从精确检索换成索引,或者调整索引参数、重建索引,结果会变,这是预期行为。
  • 索引的召回要在自己的数据上测:抽一批查询,同时跑精确检索和索引检索,比较前 k 条的重合。这个测量不需要人工标注,成本很低,应该是上线前的固定步骤。
  • 文档数量不大(几万到几十万)时,精确检索往往已经够快,不建索引就没有这个问题。

四、过滤条件加在哪一步 ​

实际的查询几乎都带条件:只看本租户的、只看有权限的、只看最近一个月的。条件加在向量检索之后还是之前,结果不一样。实验里每篇文档随机属于 20 个租户之一,查询要求返回本租户最相似的 10 条:

做法平均返回一条都没有的查询凑够 10 条的查询
先取前 10 条,再按租户过滤0.6 条53%0
先取前 50 条,再过滤2.4 条10%0
先取前 200 条,再过滤8.7 条057%
先按租户筛出候选,再检索10 条0100%

先检索后过滤时,前 10 条里属于本租户的平均只有二十分之一。多取一些再过滤能缓解,但要多取多少取决于过滤条件有多严,而这因查询而异:大租户够了,小租户仍然不够。

把四种做法的返回条数和零结果比例画在一起:

平均返回条数(目标 10 条)零结果查询先取前 10 条再过滤检索后过滤平均 0.6 条53%先取前 50 条再过滤检索后过滤平均 2.4 条10%先取前 200 条再过滤检索后过滤平均 8.7 条0先按租户筛候选再检索检索前过滤平均 10 条010 条检索后过滤:别的租户的文档先进入候选,任何一处遗漏都是越权
图 2 · 实测(合成数据,20 个租户,要求返回本租户最相似的 10 条):先检索再过滤时,前 10 条里属于本租户的平均只有 0.6 条,一半以上的查询一条都没有;多取再过滤能缓解,但要多取多少因租户而异。先按租户筛出候选再检索,每个查询都拿到 10 条,别的租户的文档也从未进入候选

更重要的是权限类的条件。把它放在检索之后,意味着别的租户的文档先进入了候选,再靠后面的代码剔除;任何一处遗漏(缓存了过滤前的结果、把候选交给了下游的重排或生成步骤)都是越权。权限条件应当在检索之前生效:按租户分开建索引,或者使用支持「带条件检索」的索引并确认它是先过滤的。

五、最近的不一定相关 ​

前面三节讨论的都是检索系统有没有忠实地返回「最近的 k 个」。最后一个口径是:最近的 k 个,是不是用户要的。

实验里把「与查询同主题」当作相关。用精确检索,没有任何近似:

  • 前 10 条里同主题的占 97%;
  • 前 10 条全部同主题的查询占 82%,其余 18% 的查询至少混进了一条别的主题的文档。

这些混进来的文档确实离查询更近,向量空间里的「近」和主题上的「相关」本来就不是同一件事。合成数据里这个差距来自噪声;真实场景里它来自模型的训练目标、文档的切分方式、查询与文档的表述差异。

所以评估要分两层,用两套指标:

  • 索引相对精确检索的召回(第三节):检索系统的工程指标,不需要标注。
  • 检索结果相对人工标注的相关性:模型和数据处理的效果指标,需要一批「查询—相关文档」的标注。

第一个指标是 1.0 也不说明第二个指标好。把检索结果交给生成模型时,下游拿到的是「最近的」,其中混着不相关的,要有相应的处理:重排、让模型在证据不足时拒答、保留出处以便核对。对模型输出做独立校验的另一个例子,见 模型写的 SQL 能跑通,离放它去查库还差几道检查。

六、上线前核对 ​

  1. 度量与归一化:模型文档要求的度量、索引配置的度量、应用里解释分数的方式三者一致。
  2. 阈值:在当前模型和自己的数据上用标注样本确定;换模型后重新确定。
  3. 索引召回:对照精确检索测量,记录所用的参数;重建索引或调参后重测。
  4. 过滤:权限类条件在检索之前生效;其余条件确认返回条数不足时的行为。
  5. 相关性:有一套标注数据,能回答「前 k 条里有多少是相关的」。
  6. 版本:向量是由哪个模型的哪个版本生成的,和文档一起存下来。两个模型的向量不能放在同一个索引里比较。

检索效果怎么评估、评估分数在什么情况下不可比,见新检索系统的评估分数只有旧系统的四分之一。


配套实验

  • codesphere-labs/ai/vector-search-pitfalls:numpy 2.5.3 上用固定种子的合成向量测量:三种度量在归一化前后的排序重合、不同维度与分布下的余弦取值、分桶近似索引的召回、先检索后过滤的返回条数、精确最近邻与主题的一致程度(验证记录)

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权