一、背景介绍

在构建基于向量数据库的语义检索系统时,一个常被忽视但至关重要的细节是:相似度算法的选择。同样的文档、同样的查询、同样的嵌入模型,仅仅因为采用了不同的距离度量方式,检索结果的排序和相关性分数就可能截然不同。

本文将通过一个可控的对比实验,在 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"} 一行配置即可完成,成本极低,但影响深远。理解背后的数学直觉,才能在调试检索效果时有的放矢。


六、参考文献

  1. ChromaDB 官方文档 - HNSW Configuration
  2. HNSW 算法原始论文
  3. LangChain Chroma 集成文档
  4. 向量相似度度量方法综述
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐