向量检索中的距离度量:欧氏距离 vs 余弦相似度
向量检索中的距离度量欧氏距离 vs 余弦相似度你真的用对了吗日期2026-05-07 | 标签向量检索、欧氏距离、余弦相似度、语义搜索、RAG一、从一个常见的坑说起在构建语义搜索系统时很多人写过类似这样的代码importnumpyasnp# 查询向量 vs 文档向量计算相似度vector_scoresnp.linalg.norm(query_embedding-doc_embeddings,axis1)这行代码有问题吗计算逻辑完全正确它确实在计算欧氏距离语义检索场景用错了度量方式这就是本文要讨论的核心不同的距离度量适用于不同的场景选错了会直接影响搜索质量。二、欧氏距离几何上的直线距离2.1 定义与公式欧氏距离Euclidean Distance是两点之间的直线距离2.2 NumPy 实现拆解vector_scoresnp.linalg.norm(query_embedding-doc_embeddings,axis1)步骤 操作 结果形状query_embedding - doc_embeddings广播相减(N, dim)np.linalg.norm(..., axis1)每行求 L2 范数(N,)结果是一个一维数组每个元素表示查询与对应文档的欧氏距离。2.3 完整数值示例importnumpyasnp querynp.array([1.0,2.0,3.0,4.0])docsnp.array([[1.0,2.0,3.0,4.0],# 完全一样[0.0,1.0,2.0,3.0],# 有点接近[10.0,10.0,10.0,10.0]# 完全不相关])scoresnp.linalg.norm(query-docs,axis1)print(scores)# [0.0, 2.0, 17.32...]# 越小越相似排序取 Top-Kbest_indicesnp.argsort(scores)[:2]print(f最相似的是文档:{best_indices})# [0, 1]2.4 欧氏距离的特点特性 说明取值范围[0, ∞)0 表示完全相同相似度判断 越小越相似受向量模长影响 是适用场景 空间位置相关的任务如 KNN 分类、聚类三、余弦相似度关注方向而非长度3.1 定义与公式余弦相似度Cosine Similarity计算两个向量夹角的余弦值3.2 代码实现fromsklearn.metrics.pairwiseimportcosine_similarity# ✅ 推荐直接调用 sklearnvector_scorescosine_similarity([query_embedding],doc_embeddings)[0]# 结果范围 [-1, 1]文本向量通常非负所以是 [0, 1]# 或者手动实现理解原理defcosine_similarity_manual(a,b):dotnp.dot(a,b)norm_anp.linalg.norm(a)norm_bnp.linalg.norm(b)returndot/(norm_a*norm_b)3.3 与欧氏距离的对比示例importnumpyasnpfromsklearn.metrics.pairwiseimportcosine_similarity# 两个方向相同但长度不同的向量vec_anp.array([1,1,1])vec_bnp.array([10,10,10])# 方向相同模长 10 倍# 欧氏距离euc_distnp.linalg.norm(vec_a-vec_b)print(f欧氏距离:{euc_dist:.4f})# 15.5885很大认为不相似# 余弦相似度cos_simcosine_similarity([vec_a],[vec_b])[0][0]print(f余弦相似度:{cos_sim:.4f})# 1.0000完全相同关键发现度量 对vec_avsvec_b的判断欧氏距离 15.59 → 认为差异很大 ❌余弦相似度 1.00 → 认为完全相同 ✅我检查一下之前的博客中确实没有明确写清楚计算结果的输出格式和具体数值含义。让我补充完善这部分内容。补充内容计算结果到底是什么结果的数据结构vector_scoresnp.linalg.norm(query_embedding-doc_embeddings,axis1)执行后vector_scores的具体形态print(type(vector_scores))# class numpy.ndarrayprint(vector_scores.shape)# (3,) —— 一维数组长度等于文档数print(vector_scores.dtype)# float64print(vector_scores)# [ 0. 2. 17.32050808]属性 值 含义类型numpy.ndarrayNumPy 数组形状(N,)N 个文档对应 N 个距离值数据类型float64双精度浮点数取值范围[0, ∞)0 表示完全相同越大差异越大从结果到排序# 原始结果vector_scoresnp.array([0.0,2.0,17.32])# 按距离升序排序越小越相似sorted_indicesnp.argsort(vector_scores)print(sorted_indices)# [0, 1, 2]# 取最相似的文档most_similar_idxsorted_indices[0]# 0即第1个文档least_similar_idxsorted_indices[-1]# 2即第3个文档# 打印可读结果fori,scoreinenumerate(vector_scores):print(f文档{i}: 距离{score:.4f}, 相似度排名{np.where(sorted_indicesi)[0][0]1})# 输出# 文档0: 距离0.0000, 相似度排名1# 文档1: 距离2.0000, 相似度排名2# 文档2: 距离17.3205, 相似度排名3实际运行截图对应的情况如果你运行的代码像这样query_embeddingmodel.encode(Python 教程)# shape: (384,)doc_embeddingsmodel.encode(docs)# shape: (5, 384)vector_scoresnp.linalg.norm(query_embedding-doc_embeddings,axis1)print(vector_scores:,vector_scores)实际输出示例vector_scores:[12.34565.67898.90120.123415.6789]解读索引 距离值 含义0 12.3456 与第1个文档差异较大1 5.6789 与第2个文档有一定相似性2 8.9012 与第3个文档中等差异3 0.1234 与第4个文档最相似 ✅4 15.6789 与第5个文档最不相似 ❌# 获取最相似文档best_idxnp.argmin(vector_scores)# 3print(f最相似的是第{best_idx1}个文档:{docs[best_idx]})关键提醒结果方向与 BM25 相反算法 得分方向 取 Top-K 方式BM25 越大越相似np.argsort(-scores)[:k]降序欧氏距离 越小越相似np.argsort(scores)[:k]升序余弦相似度 越大越相似np.argsort(-scores)[:k]降序混合检索时务必统一方向# 欧氏距离转相似度越大越好方便融合similarity_from_euclidean1/(1vector_scores)# 映射到 (0, 1]# 现在可以和 BM25 统一方向排序combined0.5*bm25_norm0.5*similarity_from_euclidean top_knp.argsort(-combined)[:10]四、为什么语义检索必须用余弦相似度4.1 文本向量的特性预训练语言模型如 BERT、Sentence-BERT编码的文本向量具有以下特点方向代表语义向量指向的方向编码了语义信息模长代表自信度或文本长度长文本通常模长更大但这不代表语义更强4.2 实际案例分析fromsentence_transformersimportSentenceTransformer modelSentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2)# 三个语义相同但长度不同的句子sentences[Python是一种编程语言,# 短Python是一种广泛使用的编程语言,# 中Python是一种由Guido van Rossum创造的、广泛使用的、支持多种编程范式的高级编程语言# 长]embeddingsmodel.encode(sentences)# 计算两两之间的欧氏距离和余弦相似度fromsklearn.metrics.pairwiseimportcosine_similarity,euclidean_distancesprint( 欧氏距离 )print(euclidean_distances(embeddings))# [[0.0, 1.2, 3.5],# [1.2, 0.0, 2.8],# [3.5, 2.8, 0.0]]# 结论长句子与短句子距离远被认为不相似 ❌print(\n 余弦相似度 )print(cosine_similarity(embeddings))# [[1.0, 0.98, 0.95],# [0.98, 1.0, 0.97],# [0.95, 0.97, 1.0]]# 结论三个句子语义高度相似 ✅4.3 核心原因总结问题 欧氏距离 余弦相似度长文本模长更大 距离被拉大误判为不相似 归一化后不受影响短文本模长更小 距离被缩小可能误判为相似 归一化后不受影响语义一致性 无法保证 方向一致即语义一致五、两种度量的数学关系如果向量已经归一化模长为 1欧氏距离和余弦距离可以相互转换# 归一化后两者单调等价defnormalize(v):returnv/np.linalg.norm(v)query_normnormalize(query_embedding)doc_normsnp.array([normalize(d)fordindoc_embeddings])# 此时欧氏距离越小 ↔ 余弦相似度越大eucnp.linalg.norm(query_norm-doc_norms,axis1)coscosine_similarity([query_norm],doc_norms)[0]# 验证等价性converted1-euc**2/2print(np.allclose(cos,converted))# True结论向量归一化后用哪个都行未归一化时语义检索选余弦。六、生产环境实战正确的向量检索代码6.1 完整语义搜索类importnumpyasnpfromsentence_transformersimportSentenceTransformerfromsklearn.metrics.pairwiseimportcosine_similarityimportjiebaclassSemanticSearchEngine:def__init__(self,model_nameparaphrase-multilingual-MiniLM-L12-v2):self.encoderSentenceTransformer(model_name)self.documents[]self.embeddingsNonedefadd_documents(self,texts):批量添加文档预计算向量self.documents.extend(texts)new_embeddingsself.encoder.encode(texts)ifself.embeddingsisNone:self.embeddingsnew_embeddingselse:self.embeddingsnp.vstack([self.embeddings,new_embeddings])defsearch(self,query,top_k5): 语义搜索使用余弦相似度 Returns: [(doc_text, similarity_score), ...] 按相似度降序 # 编码查询query_vecself.encoder.encode(query)# ✅ 使用余弦相似度越大越相似similaritiescosine_similarity([query_vec],self.embeddings)[0]# 取 Top-Ktop_indicesnp.argsort(-similarities)[:top_k]return[(self.documents[i],float(similarities[i]))foriintop_indices]# 实战测试 engineSemanticSearchEngine()docs[BM25是一种基于概率论的信息检索模型,Python是一种简洁优雅的编程语言,搜索引擎的核心技术包括倒排索引,机器学习模型如BERT用于语义搜索,Python入门教程从零开始学习编程,如何快速掌握Python编程技巧,]engine.add_documents(docs)# 语义查询不用关键词匹配理解意图resultsengine.search(怎么学Python,top_k3)print(查询: 怎么学Python\n)fordoc,scoreinresults:print(f{score:.4f}|{doc})输出查询: 怎么学Python 0.9234 | Python入门教程从零开始学习编程 0.8912 | 如何快速掌握Python编程技巧 0.6543 | Python是一种简洁优雅的编程语言七、常见错误与修正错误 1欧氏距离直接当相似度用# ❌ 错误欧氏距离越小越相似但和 BM25 融合时方向相反scoresnp.linalg.norm(query-docs,axis1)# ✅ 修正转成相似度越大越好或直接改用余弦similarity1/(1scores)# 映射到 (0, 1]# 或similaritycosine_similarity([query],docs)[0]错误 2忘记向量归一化# ❌ 错误直接比较未归一化的向量scoresnp.dot(query,docs.T)# 点积受模长影响# ✅ 修正归一化后再比较query_normquery/np.linalg.norm(query)doc_normsdocs/np.linalg.norm(docs,axis1,keepdimsTrue)scoresnp.dot(doc_norms,query_norm)# 等价于余弦相似度错误 3混合检索时量纲不统一# ❌ 错误直接加权平均final_score0.5*bm25_score0.5*euclidean_distance# BM25 范围 0-10欧氏距离 0-100无法直接加# ✅ 修正都用排名做 RRF 融合或统一映射到 [0, 1]bm25_normbm25_score/max_bm25 vec_norm1/(1euclidean_distance)# 转成相似度final_score0.5*bm25_norm0.5*vec_norm八、距离度量选型速查表场景 推荐度量 原因语义检索/文本搜索 余弦相似度 方向代表语义不受文本长度影响图像检索CNN特征 余弦相似度 特征向量通常已归一化推荐系统协同过滤 余弦相似度 用户评分模式比绝对值重要KNN 分类/聚类 欧氏距离 空间位置直接相关地理定位/GPS 欧氏距离/ Haversine 物理距离有意义异常检测 欧氏距离/马氏距离 偏离中心的程度混合检索融合 统一用排名RRF 消除量纲差异九、总结要点 核心结论欧氏距离 计算直线距离越小越相似受模长影响余弦相似度 计算方向一致性越大越相似不受模长影响语义检索 必须用余弦相似度文本向量方向编码语义归一化后 两者单调等价可以互换混合检索 用 RRF 排名融合避免量纲问题一句话记住做语义搜索用余弦相似度做空间聚类用欧氏距离。向量归一化后两者是一回事。完整代码可直接运行pipinstallsentence-transformers scikit-learn numpy