【Langchain 1.0】ChromaDB 向量相似度算法深度对比:cosine、L2、IP 该如何选择?
相似度算法的选择。同样的文档、同样的查询、同样的嵌入模型,仅仅因为采用了不同的距离度量方式,检索结果的排序和相关性分数就可能截然不同。本文将通过一个可控的对比实验,在 ChromaDB 中创建四个使用不同相似度算法的集合,存储相同的文档,用同一个查询分别检索,直观展示 cosine、L2(欧氏距离)、IP(内积)以及默认策略的差异,并给出选型建议。相似度算法的选择不是"哪个更好"的问题,而是"哪个
一、背景介绍
在构建基于向量数据库的语义检索系统时,一个常被忽视但至关重要的细节是:相似度算法的选择。同样的文档、同样的查询、同样的嵌入模型,仅仅因为采用了不同的距离度量方式,检索结果的排序和相关性分数就可能截然不同。
本文将通过一个可控的对比实验,在 ChromaDB 中创建四个使用不同相似度算法的集合,存储相同的文档,用同一个查询分别检索,直观展示 cosine、L2(欧氏距离)、IP(内积)以及默认策略的差异,并给出选型建议。
二、方案分析:四种相似度算法的本质差异
ChromaDB 底层使用 HNSW(Hierarchical Navigable Small World) 算法构建向量索引,这是目前最高效的近似最近邻搜索算法之一。而 hnsw:space 参数则决定了 HNSW 如何计算两个向量之间的"距离"——这个选择直接影响了"什么是相似"的定义。
2.1 四种评分方式详解
| 评分方式 | 配置值 | 计算逻辑 | 核心特点 | 典型场景 |
|---|---|---|---|---|
| 默认(default) | 不设置或 None |
ChromaDB 内部默认策略 | 通常等价于 cosine,但依赖版本实现 | 快速上手,不关注底层细节 |
| 余弦相似度(cosine) | "cosine" |
计算向量夹角的余弦值,忽略向量长度 | 只关心语义方向,归一化后等价于点积 | 文本语义搜索、文档检索 |
| 欧氏距离(L2) | "l2" |
计算向量各维度差的平方和开根号 | 关心绝对数值差异,对向量长度敏感 | 图像特征检索、数值型特征匹配 |
| 内积/点积(IP) | "ip" |
向量对应维度相乘后求和 | 向量长度也参与评分,模长越大分数越高 | 推荐系统(用户偏好强度)、加权检索 |
2.2 为什么算法选择如此重要?
不同的业务场景对"相似"的理解本质上不同:
- 文本语义搜索用 cosine:我们只关心两句话是否"说的是同一件事",而不关心它们各自有多"长"(即嵌入向量的模长)。例如"你好"和"您好"语义相近,即使向量长度不同,cosine 也能正确捕捉方向一致性。
- 图像检索用 L2:像素级或特征级的绝对差异很重要,两张图片在颜色分布上的细微偏移需要被度量。
- 推荐系统用 IP:用户行为向量的模长本身代表了活跃度或偏好强度,一个频繁交互的用户理应获得更高的推荐权重,IP 能自然表达这一点。
三、实操步骤:对比实验的完整实现
3.1 实验设计
为保证实验的单一变量原则,我们控制以下条件完全一致:
- 相同的文档集合
- 相同的嵌入模型
- 相同的查询语句
- 仅
hnsw:space参数不同
实验流程:
创建 4 个集合(default / cosine / l2 / ip)
↓
向 4 个集合添加完全相同的文档
↓
用同一个查询分别检索 4 个集合
↓
对比不同算法给出的相似度分数和排序
3.2 环境准备
from langchain_chroma import Chroma
from langchain_ollama import OllamaEmbeddings
from langchain_core.documents import Document
# 使用 nomic-embed-text 作为嵌入模型(支持中英文)
embeddings = OllamaEmbeddings(model="nomic-embed-text")
# 持久化目录
persist_dir = "./chroma_similarity_demo"
# 四种评分方式
score_methods = ["default", "cosine", "l2", "ip"]
3.3 创建带不同评分方式的集合
vector_stores = []
for score_method in score_methods:
# 配置集合元数据:指定 HNSW 的距离度量方式
collection_metadata = {"hnsw:space": score_method} if score_method != "default" else None
collection_name = f"my_collection_{score_method}"
vector_store = Chroma(
collection_name=collection_name,
embedding_function=embeddings,
persist_directory=persist_dir,
collection_metadata=collection_metadata, # 关键:指定评分方式
)
vector_stores.append(vector_store)
print(f"✅ 创建集合: {collection_name},评分方式: {score_method}")
3.4 构建索引:向所有集合添加相同文档
def indexing(docs):
"""向所有集合中索引相同的文档,确保实验变量单一。"""
print("\n📥 正在向向量库添加文档...")
for vector_store in vector_stores:
ids = vector_store.add_documents(docs)
print(f"\n 集合: {vector_store._collection.name}")
print(f" 文档 ID: {ids}")
# 测试文档:两句话都包含"小米",但语义完全不同
docs = [
Document(
page_content="这个小米手机很好用",
metadata={"category": "electronics", "brand": "Xiaomi"}
),
Document(
page_content="我国山西地区生产小米",
metadata={"category": "agriculture", "region": "Shanxi"}
),
]
indexing(docs)
测试数据设计意图:
这两句话的语义差异极具代表性:
- 第一句:"小米"指电子产品(小米品牌手机)
- 第二句:"小米"指粮食作物(谷子脱壳后的颗粒)
查询"雷军最近不开心"时,雷军是小米公司创始人,理应匹配第一句。但由于两句话都包含"小米"这个词,对嵌入模型和相似度算法都是考验——能否穿透字面重合,捕捉语义关联?
注意:测试数据为中文,使用通义千问(qwen)系列的 embedding 模型效果通常优于纯英文模型。若使用 Ollama,可尝试
shaw/dmeta-embedding-zh或类似中文优化模型。
3.5 执行对比查询
def query_with_score(query: str):
"""用同一个查询检索所有集合,对比分数差异。"""
print(f"\n{'='*60}")
print(f"🔍 查询语句: \"{query}\"")
print(f"{'='*60}")
for i, score_method in enumerate(score_methods):
results = vector_stores[i].similarity_search_with_score(query, k=2)
print(f"\n📊 评分方式: {score_method.upper()}")
print("-" * 40)
for rank, (doc, score) in enumerate(results, 1):
print(f" 排名 {rank}: [{score:.6f}] {doc.page_content}")
# 执行查询
query_with_score("雷军最近不开心")
四、验证效果
4.1 预期输出示例
============================================================
🔍 查询语句: "雷军最近不开心"
============================================================
📊 评分方式: DEFAULT
----------------------------------------
排名 1: [0.916573] 这个小米手机很好用
排名 2: [1.173483] 我国山西地区生产小米
📊 评分方式: COSINE
----------------------------------------
排名 1: [0.458286] 这个小米手机很好用
排名 2: [0.586742] 我国山西地区生产小米
📊 评分方式: L2
----------------------------------------
排名 1: [0.916573] 这个小米手机很好用
排名 2: [1.173483] 我国山西地区生产小米
📊 评分方式: IP
----------------------------------------
排名 1: [0.458285] 这个小米手机很好用
排名 2: [0.586742] 我国山西地区生产小米
五、总结
相似度算法的选择不是"哪个更好"的问题,而是"哪个更符合你对相似的定义"。通过本文的对比实验,我们可以建立以下决策框架:
| 场景 | 推荐算法 | 理由 |
|---|---|---|
| 通用文本语义搜索 | cosine |
方向一致性即语义相似,不受文本长度偏置 |
| 推荐系统(用户偏好有强度差异) | ip |
向量模长承载额外信息(活跃度、置信度) |
| 图像/数值特征匹配 | l2 |
绝对像素/数值差异是核心关注点 |
| 快速原型/不关心底层 | default(但建议避免) |
简单,但存在版本行为变更风险 |
在 ChromaDB 中,这个选择只需在创建集合时通过 collection_metadata={"hnsw:space": "cosine"} 一行配置即可完成,成本极低,但影响深远。理解背后的数学直觉,才能在调试检索效果时有的放矢。
六、参考文献
更多推荐


所有评论(0)