跳到主要内容
Cowers://
全部文章
检索与 RAG

余弦、内积与欧氏距离:RAG 该用哪个度量

余弦看方向,欧氏看位置,内积两个都看。RAG 比较的是「两段文字是不是一个意思」,与文本长短无关,所以要的是方向,选余弦。 而一旦向量做过 L2 归一化,三者的排序结果就完全一致了——所以真正该做的动作不是纠结选哪个度量…

余弦、内积与欧氏距离:RAG 该用哪个度量

修订说明(核实于 2026-08-13,东八区) 本篇的数学部分经复核全部正确(枢纽等式、L22=22cos\text{L2}^2=2-2\cos 的推导、几何示例的数值),修订集中在工程结论

  • 撤回「归一化后改用 IP 更快」这一核心建议。Milvus 官方文档从未做过此性能比较;模长在建索引时已算好,两者查询开销基本相同。归一化的收益是确定性,不是速度。
  • 补上「两侧都要归一化」:原文只强调入库侧,漏了查询侧。
  • 阈值换算漏了平方:Milvus / FAISS 的 L2 返回的是平方欧氏距离,cos=0.75\cos=0.75 对应库内分数 0.5 而非 0.707。
  • 「高维 L2 失去区分度而余弦不受影响」与本文自己的单位球等价结论矛盾,已限定为「仅对未归一化向量成立」。
  • 删除三条错误的 L2 适用场景:人脸/声纹实际用余弦(ArcFace 等在超球面训练)、鞋码应走标量过滤而非 Embedding 几何、chunk 去重用余弦同样可行。
  • 「文本越长模长越大」是示例的人为设定,不是模型性质,已标注。
  • TopK 50/50→20→5 改为「实验起点」而非生产配置。

新增前置步骤:一切度量讨论的前提是先读你所用模型的 model card,按它规定的归一化和 query/document 编码协议来。

阅读提示 本笔记的前置阅读是 RAG概念(为什么 RAG 需要向量检索)。本篇只回答一个问题:建 Collection 时 metric_type 该填 COSINE、IP 还是 L2,以及为什么这个选择其实没你想的那么重要。

默认读者已经知道什么是 Embedding、什么是 TopK 召回,正在配置一个真实的向量库。

本篇不讲 BM25 怎么算分(见 BM25 算法原理),不讲两路召回放在哪个存储(见 混合检索选型)。本篇只管向量这一路,用什么公式比较两个向量

⚠️ = 常见陷阱 🆚 = 对比说明 💡 = 机制或选择建议

目录


核心概念 余弦看方向,欧氏看位置,内积两个都看。RAG 比较的是「两段文字是不是一个意思」,与文本长短无关,所以要的是方向,选余弦。

而一旦向量做过 L2 归一化,三者的排序结果就完全一致了——所以真正该做的动作不是纠结选哪个度量,是入库时把向量归一化

一、两个度量分别在算什么?

先把两个公式摆在一起。设两个 n 维向量 AABB

余弦相似度,看两者的夹角:

cos(A,B)=ABAB=iAiBiiAi2iBi2\cos(A,B) = \frac{A \cdot B}{\|A\|\|B\|} = \frac{\sum_i A_i B_i}{\sqrt{\sum_i A_i^2}\sqrt{\sum_i B_i^2}}

欧氏距离,看两点之间的直线距离,就是勾股定理推广到 n 维:

L2(A,B)=AB=i(AiBi)2\text{L2}(A,B) = \|A - B\| = \sqrt{\sum_i (A_i - B_i)^2}

同一组向量,两个度量给出的答案不一样

拿一组最简单的数据跑一遍。取 A=[1,2]A = [1, 2]B=[2,4]B = [2, 4]——注意 BB 就是 AA 乘以 2,两者方向完全相同

余弦:
  点积  = 1×2 + 2×4 = 10
  ‖A‖  = √(1+4)  = √5  ≈ 2.236
  ‖B‖  = √(4+16) = √20 ≈ 4.472
  cos  = 10 / (2.236 × 4.472) = 10 / 10 = 1.000   ← 完全同向
 
欧氏:
  L2 = √((1-2)² + (2-4)²) = √(1+4) = √5 ≈ 2.236   ← 明显不为零

余弦说「这俩一模一样」,欧氏说「这俩差了 2.236」。两个都没算错,它们回答的本来就是两个问题:

维度 余弦相似度 欧氏距离
衡量什么 方向(夹角) 空间中的直线距离
取值范围 [1,1][-1, 1] [0,+)[0, +\infty)
好的方向 越大越相似 越小越相似
对模长 不敏感,公式里已除掉 非常敏感

注意「越大越好」和「越小越好」是反的 这一点看着琐碎,但它是后面 陷阱 4:把分数当阈值直接照搬 的根源。余弦是相似度,欧氏是距离,一个升序一个降序,任何写死的阈值换个度量就全反了。

工程上还有个常见变体:欧氏距离平方,省掉开根号这一步。

L22(A,B)=i(AiBi)2\text{L2}^2(A,B) = \sum_i (A_i - B_i)^2

开不开根号不影响排序,所以 FAISS 的 L2 默认算的就是平方值,不要拿它去和别处的距离数值直接比大小。

二、内积和余弦是什么关系?

一句话:内积 = 余弦 × 两个向量的模长

把余弦公式两边乘开就得到了这条全篇的枢纽等式:

AB=ABcos(A,B)A \cdot B = \|A\| \|B\| \cos(A,B)

所以内积既看夹角,也看长度:向量越长,内积越大。余弦则把长度这一项彻底除掉了。

未归一化时,内积会被长度带偏

设查询 q=[2,0]q = [2, 0],三个候选 chunk:

向量 含义 与 q 的关系
C1=[1.2,0]C_1 = [1.2, 0] 同义、写得短 方向完全相同
C2=[4,0]C_2 = [4, 0] 同义、写得啰嗦 方向完全相同
C3=[4.6,1.4]C_3 = [4.6, 1.4] 跑题,但篇幅最长 有 16.9° 夹角

向量度量 长度噪音几何直觉

图分两半读:左边是几何,C1C_1 / qq / C2C_2 三个箭头压在同一条射线上(方向一致,只是长短不同),C3C_3 明显偏离;右边是三种度量分别打出的分。

          C1(短同义)   C2(长同义)   C3(跑题但最长)
余弦        1.000       1.000        0.957     ← C1 = C2 并列第一 ✅
内积         2.40        8.00         9.20     ← C3 排第一 ❌
欧氏         0.80        2.00         2.95     ← C2 被判成很远 ❌

C1C_1C2C_2 是同一句话的长短两个版本,RAG 想要的答案是「它们同样相关」。只有余弦给出了这个答案。内积把模长最大的跑题 chunk 顶到了第一,欧氏则狠狠惩罚了模长偏大但方向正确的 C2C_2

⚠️ 这个例子演示的是「模长差异」,不要读成「文本越长模长越大」 上表把 C2C_2 标成「写得啰嗦」、模长设成 4,容易让人得出「文本越长 → Embedding 模长越大」的印象。这一步是人为设定的,不是模型的性质。

实际上多数句向量模型(bgee5 等)在池化后会做归一化或近似归一化输出,文本长度和最终模长之间没有可靠的单调关系——长文本的 token 向量平均下来甚至可能互相抵消,得到更小的模长。

这个例子的正确读法是:假设两个向量方向相同但模长不同,三种度量会怎么排。 至于「模长差异从哪来」,那是另一个问题——可能来自模型没归一化、来自不同模型混用、来自你自己做了加权求和。先确认你的向量确实存在模长差异(np.linalg.norm() 看一眼),再决定要不要在意这件事。

归一化之后,三者变成同一件事

如果入库时把每个向量都除以自己的模长,即 v^=v/v\hat{v} = v / \|v\|,那么 A^=B^=1\|\hat{A}\| = \|\hat{B}\| = 1,代入枢纽等式:

A^B^=1×1×cos(A,B)=cos(A,B)\hat{A} \cdot \hat{B} = 1 \times 1 \times \cos(A,B) = \cos(A,B)

内积和余弦数值完全相等。 欧氏距离也被绑定了:

A^B^2=A^2+B^22A^B^=22cos(A,B)\|\hat{A} - \hat{B}\|^2 = \|\hat{A}\|^2 + \|\hat{B}\|^2 - 2\hat{A}\cdot\hat{B} = 2 - 2\cos(A,B)

用上面的 C3C_3 验一下(cos=0.9567\cos = 0.9567):

按公式推:  √(2 - 2×0.9567) = √0.0866 = 0.2943
直接算:    q̂  = [1, 0]
           Ĉ3 = [4.6/4.8083, 1.4/4.8083] = [0.9567, 0.2911]
           L2 = √((1-0.9567)² + 0.2911²) = √0.08661 = 0.2943   ← 一致

22cos2 - 2\cos 是关于 cos\cos 的单调递减函数,所以余弦越大 ⇔ 欧氏越小,两个排序严格互为镜像,TopK 结果完全相同

⚠️ 「归一化后改用 IP 会更快」——这个说法站不住 本篇早前版本写的是「内积只需一轮乘加,余弦还要额外算两个模长,所以 IP 更快」,并说这是 Milvus / FAISS 文档的意思。核对官方文档后,这个说法要撤回。

① 官方没有做过这个性能承诺。 Milvus 的度量文档只说明了三种度量各自的定义和适用场景,通篇没有比较过它们的速度。「IP 明显更快」是社区流传的推断,不是文档结论。

② 「每次查询重算模长」不是数据库的实际实现。 向量库不会在每次比较候选时现算两个模长——文档向量的模长在建索引时就已算好并存下(Milvus 对 COSINE 的做法是入库时归一化,此后按内积执行),查询向量的模长每次查询只算一次,摊到成千上万个候选上可以忽略。也就是说,归一化数据上 COSINE 和 IP 的查询开销基本相同

③ 真正的开销大头在别处。 ANN 检索的时间花在图遍历(HNSW)或聚类探查(IVF)上,距离计算本身只占一小部分。想快,该调的是 ef / nprobe / 量化方式,不是度量名。

那还要不要归一化?要,但理由是「正确」不是「快」: 归一化消除了模长这个噪音变量,让 IP 和 COSINE 的语义合一,从此不必再担心「选错度量会不会改变排序」。这是确定性收益,不是性能收益。 度量选 COSINE 还是 IP 在归一化数据上几乎无差别——真要挑,选 COSINE 更稳妥:即使某天有人漏了归一化,它也会自己补上,而 IP 会静默给出被模长带偏的结果。

⚠️ 用 IP 实现 cosine 时,两侧都要归一化 常见的漏做法是只归一化入库向量,忘了查询向量。

A^B=1×B×cos\hat{A}\cdot B = 1 \times \|B\| \times \cos——只要有任何一侧没归一化,结果就仍然带着那一侧的模长。查询侧没归一化时后果反而不明显:同一次查询里所有候选都乘了同一个 q\|q\|排序不变,只是分数整体缩放——于是你的绝对阈值失效了,而 TopK 看起来一切正常。这种「只坏一半」的故障最难查。

所以:入库侧和查询侧用同一个归一化函数,写在同一个封装里,别两处各写一遍。

三、为什么 RAG 默认选余弦?

RAG 的召回在问:用户这句话和这个 chunk,是不是在聊同一件事。 这是个纯方向问题。有三条理由把欧氏距离排除掉。

第一,chunk 长度是噪音,不是信号。

同一个问题「什么是大模型幻觉」,知识库里可能有两段答案:一段 50 字直接给定义,一段 500 字绕了半天但也说对了。两者向量方向几乎一致,模长却差很多。余弦认为它俩同样相关,这是对的;欧氏会把长的那段推远,等于对写得详细的文档施加惩罚。RAG 里 chunk 长短天然不齐,这个惩罚纯属干扰。

第二,主流文本 Embedding 的模长通常不承载你想要的语义。

BGE、E5、GTE、OpenAI 的 text-embedding-* 这类模型,训练时普遍采用对比学习,loss 围绕余弦相似度或(温度缩放的)内积设计——优化目标主要约束方向

⚠️ 但别把这条推成「模长一定没有任何信息」。各家模型的 loss 细节并不相同(有的做了显式归一化、有的没有),也有研究发现模长与词频、文本长度等因素存在相关性。准确的说法是:模长不是模型有意用来表达「这段文本更重要 / 更可信」的通道,所以把它带进相似度计算,等于引入一个你无法解释、模型也没打算表达的变量。

判断方法比记结论可靠: 拿你实际用的模型编码一批文本,np.linalg.norm() 看看模长分布。如果分布很窄(说明模型自己就在输出近似单位向量),归不归一化差别不大;如果分布很宽,那这个变量正在悄悄影响你的 IP 排序。

第三,未归一化的高维欧氏距离容易失去区分度。

RAG 的向量是 768、1024、3072 维的。维度越高,任意两点的欧氏距离越趋于集中在一个相近的值附近,「最近」和「较近」拉不开差距,这就是维度灾难在距离度量上的表现。

⚠️ 这条只对未归一化的向量成立,别和上一节的结论打架 上一节刚证明过:向量归一化后 L22=22cos\text{L2}^2 = 2-2\cos,两者严格单调等价,排序完全相同。既然排序相同,就不可能出现「L2 失去区分度而余弦没有」——同一批向量上,两者的区分度是同一件事

所以第三条理由要限定清楚:它说的是没做归一化时,模长差异叠加高维集中效应,让 L2 的分数挤成一团。一旦归一化,这条理由自动消失。

换句话说,三条理由指向的都是同一个动作——见下方总结。

三条理由的共同点 它们说的都是同一件事:欧氏距离携带了模长信息,而在 RAG 里模长通常是噪音。 所以正确的处理不是「避开欧氏」,是「先把模长信息去掉」——也就是归一化。去掉之后,三种度量给出的 TopK 完全相同,选哪个都对。

四、欧氏距离该用在哪里?

🆚 这里要说清楚:欧氏距离不是差的度量,只是和 RAG 的目标不匹配。只要你关心绝对数值差多少,而不是方向对不对,就该用欧氏。

场景 为什么用欧氏 注意
K-means 等聚类 判断样本归属哪个簇,看的是它在空间里的绝对位置离哪个中心近 这是 K-means 的定义本身要求的(它最小化簇内平方和)。要按方向聚类得用球面 K-means
风控与异常检测 正常行为聚成一团,离群点的判据就是「离中心的物理距离超过阈值」 前提是各维度已做过量纲对齐,否则量纲大的维度会主导距离
真正的数值特征 传感器读数、坐标、价格这类本身就是数值的特征向量 见下方警告:这里指的不是 Embedding

⚠️ 原表里有三行是错的,已删除,说明如下 ① 「人脸 / 声纹比对适合 L2,因为模长编码特征强度」——反了。 当代主流人脸识别(ArcFace、CosFace 及其衍生)恰恰是在超球面上做的:训练时就把特征和权重都归一化,损失直接作用于夹角,推理时用余弦相似度比对。模长在这些系统里被有意消除,正因为它编码的是图像质量、光照、姿态这类与身份无关的因素。声纹(如 ECAPA-TDNN)同样以余弦打分为主。

② 「搜『25cm 的鞋』用 Embedding 的 L2,24.5cm 就会排在 27cm 前面」——不成立。 Embedding 空间里不存在「1 厘米对应多少距离」这种线性几何关系。模型对数字的表示由分词方式和训练语料决定,24.527 可能被切成完全不同的 token,几何上谁近谁远无法预期。

正确做法是根本不要用向量检索解决这个问题:把尺码存成结构化数值字段,用范围过滤或排序:

client.search(..., filter="size_cm >= 24 and size_cm <= 26")

这是标量过滤该干的活(见 Milvus 实操与五种检索)。凡是能写成 WHERE 的条件,就不该指望 Embedding 的几何来表达。

③ 「chunk 去重适合 L2」——L2 不是必需的。 去重判断的是「两段文本是不是几乎一样」,这在归一化向量上用余弦同样成立(且阈值更好定,比如 > 0.95)。而且归一化后两者排序等价,谈不上谁更适合。真正影响去重质量的是切块粒度和阈值标定,不是度量选择。

一句话切分:

比较的是「意思像不像」→ 余弦 / 内积;比较的是「本身就是数值的量差多少」→ 欧氏距离。

关键限定:后半句里的「数值」指真正的数值特征(坐标、读数、价格),不是把数值写进文本再过 Embedding 得到的向量。

五、常见陷阱

陷阱 1:向量没归一化就直接用 IP

最容易踩的一个,因为它不报错,只是悄悄让召回质量变差。

⚠️ 未归一化直接用内积 错误操作: 建索引时选了 metric_type="IP",但入库的向量是模型原样输出、没做 L2 归一化。

实际结果: 长 chunk、重复词多的 chunk 稳定占据 TopK 前排;明明更精准的短答案被挤到后面。看起来像「召回不准」,但换 Embedding 模型、调切块大小都不见好转。

原因: AB=ABcosA \cdot B = \|A\|\|B\|\cos,模长直接乘在分数上。一个方向偏一点但足够长的向量,内积可以轻松超过方向完全正确的短向量(见 二、内积和余弦是什么关系?C3C_3 压过 C1C_1 的例子)。

正确做法: 二选一——入库前执行 v = v / np.linalg.norm(v),或者干脆把 metric_type 换成 COSINE(由库来做归一化,代价是每次查询多算模长)。推荐前者,一次性成本换全程性能。

陷阱 2:反复切换 metric_type 想提升召回质量

这个陷阱浪费的时间最多。

⚠️ 把度量当成调优旋钮 错误操作: 召回效果不好,于是把 COSINE 改成 IP,再改成 L2,反复重建索引对比效果。

实际结果: 三次跑出来的 TopK 几乎一模一样,评测指标纹丝不动,几小时就这么没了。

原因: 向量已归一化的前提下,A^B^=cos\hat{A}\cdot\hat{B} = \cosA^B^2=22cos\|\hat{A}-\hat{B}\|^2 = 2-2\cos,三个度量对同一批向量给出的排序完全相同,只是分数刻度不同。换度量在数学上不可能改变 TopK。

正确做法: 承认度量这一环没有调优空间,把精力放到真正能动的地方——切块策略、Embedding 模型、加一路 BM25(见 混合检索选型)、上 reranker。这些才会改变排序。

陷阱 3:建完索引才想改度量

⚠️ metric_type 是索引的一部分,不能事后改 错误操作: Collection 建索引时用了 L2,上线后想换成 COSINE,直接在查询参数里改。

实际结果: 要么直接报 metric type 不匹配的错误,要么(更糟)在某些客户端版本里静默按索引的度量执行,你以为换了其实没换,评测结论完全建立在错误前提上。

原因: 距离度量是在建索引时烧进索引结构的(HNSW 的邻居图、IVF 的聚类中心都按该度量算好了),查询参数只能与之一致,不能覆盖。

正确做法: 建库前就定好,且建索引与查询用同一个度量。要改就得 drop index 重建。既然改了排序也不变(见陷阱 2),通常最优解是直接归一化 + 用 IP,一次定死。

陷阱 4:把分数当阈值直接照搬

⚠️ 相似度阈值跨度量搬运 错误操作: 在余弦下调好了「score > 0.75 才算命中」,换到 L2 索引后沿用 0.75 这个数。

实际结果: 过滤逻辑彻底反向——该丢的全留下,该留的全丢掉。

原因: 余弦是相似度,越大越像,取值 [1,1][-1,1];欧氏是距离,越小越像,取值 [0,+)[0,+\infty)。方向相反,量纲也不同。

正确做法: 阈值必须和度量绑定记录,换度量就重新标定。更稳的做法是别用绝对阈值,改用 TopK + reranker 分数来卡——绝对阈值在换 Embedding 模型后同样会失效。

⚠️ 换算阈值时的第二个坑:Milvus / FAISS 返回的是平方** L2** 这一步几乎所有人都会算错,包括本篇早前的版本。

数学上,归一化向量满足 L2=22cos\text{L2} = \sqrt{2-2\cos},所以 cos=0.75\cos = 0.75 对应 22×0.75=0.50.707\sqrt{2-2\times0.75} = \sqrt{0.5} \approx 0.707

但数据库返回的不是这个数。 Milvus 官方文档明确写着:选择欧氏距离时,只计算开平方之前的值。FAISS 的 IndexFlatL2 同理,返回的也是 squared L2。所以:

cos=0.75\cos = 0.75 对应的值
数学上的 L2 距离 0.50.707\sqrt{0.5} \approx 0.707
Milvus / FAISS 实际返回的 distance 22×0.75=0.52 - 2\times0.75 = \mathbf{0.5}

把 0.707 当阈值填进 Milvus,你实际卡的是 cos0.646\cos \approx 0.646门槛比你以为的松了一截——而且不报错,只是悄悄多放进来一批不够相关的文档。

怎么避免: 换算公式直接用 scoreL2=22cos\text{score}_{L2} = 2 - 2\cos(不开根号),或者更省事——先跑几条已知相似度的样本,看数据库实际返回什么数,用实测值反推,别用纸上公式。这条规则在换向量库时要重新确认一次:不是所有库都返回平方值。

六、生产级 RAG 配置

把上面的结论收成一条可以直接照做的链路。

💡 三步定型 0|先读你所用 Embedding 模型的 model card。 这是第一步,不是补充说明。模型方会明确给出推荐度量、是否已内置归一化、query 和 document 要不要加不同前缀bge 系列的 "为这个句子生成表示以用于检索文档:"e5query: / passage:)。这些协议错了,后面所有度量讨论都没有意义——本篇的所有等价关系只在「双方都已归一化」这个前提下成立。

1|入库和查询两侧都归一化。 embedding = embedding / norm(embedding),封装成一个函数,两侧共用。做完这一步,COSINE / IP / L2 搜出来的 TopK 一致,不必再纠结度量选哪个(推荐仍填 COSINE,它对漏归一化有兜底)。

2|召回走两路。 向量召回和 BM25 召回各取 TopK,用 RRF 融合。TopK 具体取多少要按自己的评测定——常见起点是各召回 50、融合后取 20、精排后留 5,但这组数字取决于你的语料规模、chunk 粒度、LLM 上下文预算和延迟预算,抄过去只能当第一次实验的初值。纯余弦会漏掉 error code 0x80070057 这类精确字符串——语义模型对它没有概念,但倒排索引一击即中。两路的分数不可比,所以融合只看名次不看分数,细节见 混合检索选型BM25 算法原理

3|精排换模型,不换度量。 召回阶段的距离已经用尽了,精排上 Cross-Encoder(如 bge-reranker-v2-m3),把问题和 chunk 拼在一起过一遍模型打分。它能看到两段文本的交互信息,比任何向量距离都准。

向量度量 召回精排链路位置

顺着图从上往下读:查询分两路召回,融合后进精排。关键是那条红色虚线指向的位置——本篇讨论的所有度量选择,只影响左路最底下那一格。归一化把这一格变成了没有争议的确定项,剩下的质量提升全部来自 RRF 融合和精排模型。

速查表

三种度量对照

维度 余弦 COSINE 内积 IP 欧氏 L2
公式 ABAB\dfrac{A \cdot B}{\lVert A\rVert \lVert B\rVert} ABA \cdot B (AiBi)2\sqrt{\sum (A_i-B_i)^2}
看什么 只看方向 方向 + 长度 绝对位置差
取值范围 [1,1][-1, 1] (,+)(-\infty, +\infty) [0,+)[0, +\infty)
排序方向 越大越像 越大越像 越小越像
对模长敏感
数据库返回值 相似度原值 内积原值 ⚠️ Milvus/FAISS 返回平方 L2
RAG 里的角色 语义召回主力,也最稳妥 归一化后与余弦等价 归一化后与前两者排序相同,无需刻意选用

归一化前后的差别

未归一化 已 L2 归一化(两侧都要)
IP 与 COSINE 数值不同,排序也不同 数值相等,排序相同
L2 与 COSINE 无固定关系 L22=22cos\text{L2}^2 = 2 - 2\cos,排序严格互逆
该选哪个 必须选 COSINE 都行;推荐仍填 COSINE,漏归一化时它能自我兜底

「理论排序等价」≠「实测 TopK 一定逐条相同」 上表说的是精确计算下的等价。实际跑 ANN 检索时,以下因素可能让两次结果差个一两条:

  • 近似检索本身:HNSW / IVF 都不保证返回精确 TopK,不同度量走的图或聚类划分可能不同;
  • 量化:PQ / SQ 会引入误差,不同度量的量化误差分布不一样;
  • 浮点误差22cos2-2\cos 这类变换在接近边界时会放大舍入误差;
  • 并列打破规则:分数相同的候选按什么顺序返回,各实现不一致。

所以陷阱 2 的结论要稍作限定:换度量不会带来有意义的效果提升,但如果你看到 TopK 有一两条微小差异,那是正常的数值现象,不用去追。真正的判据是评测指标(Recall / MRR / nDCG)有没有动,而不是结果列表逐条比对。

各家向量库怎么填

系统 参数写法 归一化后的建议
Milvus metric_type="COSINE" / "IP" / "L2" IP
FAISS IndexFlatIP / IndexFlatL2 IndexFlatIP
Pinecone metric="cosine" / "dotproduct" / "euclidean" dotproductcosine
Weaviate vectorIndexConfig.distance cosine(默认即是)
pgvector 操作符 <=> 余弦 / <#> 负内积 / <-> L2 <#>,注意它返回内积

复习重点

五条能直接复述的结论

  1. 余弦看方向,欧氏看位置,内积两个都看。 枢纽等式是 AB=ABcosA\cdot B = \|A\|\|B\|\cos——内积就是带长度加权的余弦。
  2. RAG 选余弦,因为 chunk 长度通常是噪音。 同一句话的长版本和短版本应当同样相关,除掉模长的度量才做得到。
  3. 两侧都归一化之后,三种度量排序一致。 A^B^2=22cos\|\hat{A}-\hat{B}\|^2 = 2-2\cos,所以纠结 metric_type 是伪问题,归一化(入库侧 + 查询侧)才是真动作
  4. 「归一化后改用 IP 更快」是错的。 官方文档没有这个性能承诺,模长在建索引时就算好了,两者查询开销基本相同。归一化的收益是确定性,不是速度。
  5. 最容易犯的错是拿度量当调优旋钮。 换度量不会带来有意义的效果变化;真正能改变排序的是切块、Embedding 模型、BM25 混合召回和 reranker。
  6. 换算阈值时记得 Milvus / FAISS 返回的是平方 L2cos=0.75\cos=0.75 对应的库内分数是 0.5,不是 0.707。

一句话收口:先按模型 model card 定归一化与编码协议,两侧都归一化后度量随便选(默认 COSINE),精排交给 reranker。