向量相似度是怎么计算的?从余弦距离到语义检索
码海寻道 · 大模型、智能体与 RAG 工程组件系列第 6 篇
上一篇我们把文字转换成了向量。接下来还有一个关键问题:
两个向量究竟怎样比较,才能判断它们的语义是否接近?
向量数据库的核心工作,可以概括成一句话:给定一个查询向量,从大量候选向量中找出“最相似”的 Top K 个结果。
这里的“相似”,不是凭直觉判断,而是由距离或相似度函数计算出来的。函数不同,分数含义不同,排序方向也可能相反。
在真实系统里,距离指标并不是只写在检索代码中的一个参数。它需要同时出现在 Embedding 服务配置、Milvus/pgvector Schema、索引操作类、评测脚本和监控日志中。只改其中一处,就可能出现“查询能返回结果,但排序和阈值都不可信”的问题。
一、先理解向量空间中的“近”
假设我们把一批文档转换成向量,每个向量都可以看成高维空间中的一个点:
文档 A → 向量 A → 空间中的点 A 文档 B → 向量 B → 空间中的点 B 用户问题 → 查询向量 Q → 空间中的点 Q语义检索要做的事情,就是计算 Q 与所有候选点之间的关系,然后按结果排序。
但“距离近”并没有唯一答案:
- 可以看两个点之间的直线距离;
- 可以看两个向量的夹角;
- 可以看两个向量对应坐标的乘积总和;
- 也可以使用关键词或稀疏向量的匹配分数。
因此,选择距离指标,本质上是在定义:你的系统认为“相似”是什么。
二、余弦相似度:最常见的文本向量比较方式
余弦相似度关注两个向量的夹角,而不是它们距离原点有多远。公式是:
cos(A, B) = (A · B) / (||A|| × ||B||)其中:
A · B是向量内积;||A||是向量 A 的长度;||B||是向量 B 的长度。
余弦相似度通常在[-1, 1]范围内:
- 越接近 1,方向越接近;
- 接近 0,方向接近正交;
- 越接近 -1,方向越相反。
在很多文本 Embedding 场景中,方向比向量长度更重要,因此余弦相似度经常被采用。但不能只因为“文本检索常用余弦”就盲目设置,还应该看 Embedding 模型的说明和实际评测结果。
三、内积 IP:为什么归一化后它等价于余弦?
内积的公式很简单:
A · B = a1b1 + a2b2 + ... + anbn内积同时受到两个因素影响:
- 两个向量的方向是否一致;
- 两个向量的长度是否较大。
如果先把向量归一化,使每个向量的长度都等于 1,那么:
A · B = cos(A, B)这也是为什么一些系统会使用 IP 搜索归一化后的向量。
但要注意,归一化不是一句可有可无的预处理。如果向量没有归一化,却把 IP 当作余弦相似度使用,结果可能被向量长度影响,排序就会与预期不同。
Milvus 官方文档明确说明:归一化后的向量使用内积时,结果等价于余弦相似度;未归一化时,IP 和余弦的结果可能不同。
四、欧氏距离 L2:直线距离越短越相似
欧氏距离也叫 L2 距离,二维情况下就是我们熟悉的两点直线距离:
L2(A, B) = sqrt((a1-b1)^2 + ... + (an-bn)^2)在 L2 检索中:
距离越小,结果越相似。
而在余弦和 IP 检索中,通常是分数越大,结果越相似。
这是工程中非常容易犯错的地方:如果业务代码统一把“分数越大越好”,却没有区分 L2 和 COSINE,可能把最不相关的结果排到前面。
对于长度已经归一化的向量,L2 与余弦之间存在单调关系,排序可能一致;但对于未归一化向量,二者的排序可能明显不同。不能仅凭“都是距离”来替换。
五、Milvus 中三种指标的直观对比
Milvus 对浮点向量常见的指标包括L2、IP和COSINE。它们的排序方向需要明确:
| 指标 | 衡量方式 | 更相似的结果 |
|---|---|---|
COSINE | 向量夹角 | 分数更大 |
IP | 向量内积 | 分数更大 |
L2 | 欧氏距离 | 距离更小 |
Milvus 的搜索结果字段通常被称为 distance,但它在不同指标下可能表示距离或相似度分数。不要只看字段名,必须结合metric_type判断数值含义。
组件之间必须保持同一套约定
Embedding 服务:是否归一化 ↓ 向量字段:维度和数据类型 ↓ 索引:metric / operator class ↓ 查询:距离方向、阈值和排序 ↓ Reranker:候选片段的二次排序例如,pgvector 中使用余弦距离时,索引操作类应使用vector_cosine_ops;Milvus 中则需要让 Collection 的索引和搜索参数使用相匹配的 metric。不能只在应用层把“距离越小”改成“分数越大”,而不检查底层操作符和索引。
六、Top K 到底是什么?
Top K 表示从候选结果中返回排名最靠前的 K 条。
查询向量 ↓ 计算候选相似度 ↓ 排序 ↓ 返回 Top K例如top_k=5,意味着系统返回前 5 条,不代表这 5 条都一定足够相关。
K 太小会怎样?
可能漏掉真正有用的片段,尤其是文档切分不理想或问题表达比较模糊时。
K 太大会怎样?
会带来:
- 更多无关内容进入上下文;
- Prompt 变长,成本上升;
- 大模型注意力被噪声分散;
- 后续生成延迟增加。
因此,常见做法不是简单地把 K 设置得很大,而是“先多召回,再重排序,再截取少量结果”。这就是下一篇 Reranker 文章会讨论的两阶段检索。
七、相似度阈值:为什么 Top K 还不够?
如果只使用 Top K,即使查询和所有文档都不相关,系统仍然会返回 K 条“相对最接近”的结果。
例如知识库里根本没有答案,但系统仍可能返回 5 个分数最高的无关片段,最后让模型据此编造答案。
因此可以增加阈值:
只保留 score >= threshold 的结果但阈值不是越高越安全:
- 阈值过高,会漏掉表达方式不同但实际相关的文档;
- 阈值过低,会让大量噪声进入 Prompt;
- 不同模型、不同数据集和不同指标的分数分布不同。
尤其要注意,余弦分数、IP 分数和 L2 距离不能直接使用同一个阈值。阈值必须在真实评测集上标定。
八、近似最近邻 ANN:为什么不直接比较全部向量?
如果只有几百个向量,逐个计算距离并不困难。但当向量数量达到百万、千万甚至更大规模时,每次查询都与所有向量比较,成本会很高。
向量数据库通常使用 ANN,也就是 Approximate Nearest Neighbor,近似最近邻搜索:
全部向量 ↓ 索引定位可能相关的候选区域 ↓ 只对候选进行精细比较 ↓ 返回近似 Top KANN 的目标不是在任何情况下保证数学意义上的绝对最优,而是在召回率、延迟和资源消耗之间取得平衡。
Milvus 的基础向量搜索文档说明,ANN 会先定位候选子集,再比较查询向量与候选向量,从而减少全量比较带来的开销。
常见索引包括:
- FLAT:接近暴力搜索,结果精确,适合数据量较小或评测基准;
- IVF:先把向量分到若干簇,只搜索部分簇;
- HNSW:通过图结构寻找近邻,常用于低延迟检索;
- DiskANN 等:面向更大规模和磁盘场景。
索引参数越激进,速度可能越快,但召回率可能下降。向量数据库的“快”通常不是没有代价的快。
九、过滤条件会改变检索结果
企业知识库不能只按向量相似度检索,还要增加元数据过滤:
向量相似度检索 + tenant_id = 当前租户 + department_id in 用户可访问部门 + document_status = published + version = 当前有效版本过滤可以发生在向量检索之前、检索过程中或检索之后,具体取决于数据库能力和实现方式。关键是不能先召回无权文档,再把它们交给大模型后才试图补救。
如果过滤条件过于严格,候选集合可能变小,召回率下降;如果过滤条件缺失,则会产生严重的数据泄露风险。
十、为什么检索分数不能直接当作“答案可信度”?
向量分数只表示查询向量与候选向量在某种指标下的接近程度,不等于:
- 文档一定包含答案;
- 文档内容一定正确;
- 文档对当前用户可见;
- 大模型一定能正确利用它;
- 最终回答一定没有幻觉。
一个高分片段可能只是与问题主题相近,却没有回答关键条件。一个相对低分的片段可能包含真正重要的限定语。
所以实际 RAG 系统还需要:
- 元数据过滤;
- 多路召回;
- Reranker 重排序;
- 答案引用和事实校验;
- 低置信度时拒答或转人工。
十一、一个简单的相似度计算示例
下面用 Python 展示余弦相似度的核心计算逻辑:
importmathdefcosine_similarity(a:list[float],b:list[float])->float:iflen(a)!=len(b):raiseValueError("向量维度不一致")dot=sum(x*yforx,yinzip(a,b))norm_a=math.sqrt(sum(x*xforxina))norm_b=math.sqrt(sum(y*yforyinb))ifnorm_a==0ornorm_b==0:raiseValueError("零向量不能计算余弦相似度")returndot/(norm_a*norm_b)生产环境不会用 Python 循环逐条扫描百万向量,而是交给向量数据库和索引完成。但理解这段逻辑,有助于排查维度错误、零向量和排序方向问题。
用执行计划确认数据库是否真的使用了向量索引
在 PostgreSQL + pgvector 中,可以用EXPLAIN (ANALYZE, BUFFERS)检查查询计划和实际耗时:
EXPLAIN(ANALYZE,BUFFERS)SELECTid,content,embedding<=>'[0.1,0.2,0.3]'::vectorASdistanceFROMknowledge_chunksWHEREtenant_id='tenant-a'ORDERBYembedding<=>'[0.1,0.2,0.3]'::vectorLIMIT5;如果增加过滤条件后召回数量明显下降,不要先假定距离公式错了。先分别检查过滤字段的选择性、索引是否被使用、候选是否足够,以及是否需要在数据库过滤后进行应用层重排。
十二、上线前的向量检索检查清单
- 文档向量和查询向量使用兼容模型;
- 所有向量维度一致;
- 距离指标与模型说明匹配;
- 如果使用 IP,明确是否做了归一化;
- 业务代码正确处理 L2 的“小分更好”;
- Top K 经过真实评测,不是随意设置;
- 阈值根据真实分数分布标定;
- 检索加入租户、权限和版本过滤;
- ANN 索引有召回率与延迟对比数据;
- 不把向量分数直接当作答案可信度。
结语:距离指标决定了系统如何理解“相似”
向量检索不是简单地“把文本放进数据库然后搜索”。它至少包含四个决定:
- 用什么模型生成向量;
- 用什么指标比较向量;
- 返回多少候选结果;
- 如何在速度、召回率和上下文长度之间取平衡。
余弦、内积和 L2 没有绝对的“最好”,只有与模型、数据和业务目标是否匹配。下一篇我们继续讨论:
《如何选择 Embedding 模型?从维度、中文能力到成本》
参考资料
- Milvus Documentation:Similarity Metrics
- Milvus Documentation:Basic Vector Search
- pgvector 官方项目:HNSW 与距离操作符
- Milvus Documentation:Index Explained
本文为“码海寻道”原创技术文章。具体指标、索引和参数应结合 Embedding 模型官方说明及真实数据集进行评测。