基于Chroma实现Hybrid-Semantic混合语义检索的架构设计与实战

1次阅读
没有评论

共计 2541 个字符,预计需要花费 7 分钟才能阅读完成。

image.webp

背景痛点

在实际应用中,我们发现纯语义检索和传统关键词检索各有优劣。纯语义检索(如基于 BERT 的稠密向量检索)虽然能很好地理解查询意图,但在处理长尾查询和专业术语时表现不佳。例如:

基于 Chroma 实现 Hybrid-Semantic 混合语义检索的架构设计与实战

  • 当用户搜索非常冷门的专业术语时,语义模型可能无法准确理解其含义
  • 对拼写错误或缩写词的容错能力较差

而传统关键词检索(如 BM25)虽然能精确匹配这些术语,但完全无法理解语义相关性。比如搜索 ” 苹果 ” 时,无法区分是指水果还是科技公司。

技术选型

在构建混合检索系统时,我们对比了几种主流向量数据库:

  • Faiss:专注于向量检索,性能优异但功能单一
  • Weaviate:功能全面但资源消耗较大
  • Chroma:轻量级(仅 300KB 内存占用),原生支持多模态,API 设计简洁

最终选择 Chroma 的主要考虑因素包括:

  1. 开箱即用的混合检索支持
  2. 极低的资源开销
  3. 活跃的社区和持续更新
  4. 简洁的 Python API

混合架构实现

整体流程

混合检索的核心思想是同时利用两种检索方式的结果,通过加权打分进行融合。具体流程如下:

  1. 对查询同时进行稠密向量转换和关键词提取
  2. 分别执行向量相似度搜索和关键词匹配
  3. 对两组结果进行分数归一化和加权融合
  4. 按最终分数排序返回结果

代码实现

以下是核心代码示例(Python 3.10+):

from sentence_transformers import SentenceTransformer
from rank_bm25 import BM25Okapi
import chromadb

# 初始化组件
embedder = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
chroma_client = chromadb.Client()
collection = chroma_client.create_collection("hybrid_search")

# 索引文档时同时存储原始文本和向量
def index_documents(docs: list[str]):
    # 生成稠密向量
    embeddings = embedder.encode(docs)

    # 构建 BM25 索引
    tokenized_docs = [doc.split() for doc in docs]
    bm25 = BM25Okapi(tokenized_docs)

    # 存储到 Chroma
    collection.add(
        documents=docs,
        embeddings=embeddings.tolist(),
        ids=[str(i) for i in range(len(docs))]
    )
    return bm25

# 混合检索函数
def hybrid_search(query: str, bm25_index, dense_weight=0.7, top_k=10):
    try:
        # 稠密向量检索
        query_embedding = embedder.encode(query)
        dense_results = collection.query(query_embeddings=[query_embedding.tolist()],
            n_results=top_k
        )

        # 关键词检索
        tokenized_query = query.split()
        bm25_scores = bm25_index.get_scores(tokenized_query)
        bm25_scores = (bm25_scores - min(bm25_scores)) / (max(bm25_scores) - min(bm25_scores))  # 归一化

        # 合并结果
        combined_scores = {}
        for doc_id, dense_score in zip(dense_results['ids'][0], dense_results['distances'][0]):
            normalized_dense = 1 - (dense_score / max(dense_results['distances'][0]))
            bm25_score = bm25_scores[int(doc_id)]
            combined_scores[doc_id] = dense_weight * normalized_dense + (1 - dense_weight) * bm25_score

        # 排序返回
        sorted_results = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True)
        return sorted_results[:top_k]

    except Exception as e:
        print(f"检索失败: {str(e)}")
        return []

性能优化

索引分片策略

  1. 按文档类型或主题进行分片,减少单次查询范围
  2. 实现动态分片加载,只加载必要的数据

ANN 参数调优

Chroma 使用 HNSW(Hierarchical Navigable Small World)算法进行近似最近邻搜索,关键参数包括:

  • ef_construction:影响索引构建质量和内存使用(建议值 100-200)
  • M:影响图的连通性和搜索速度(建议值 16-32)

缓存设计

  1. 查询缓存:对热门查询结果缓存 30 秒
  2. 向量缓存:对常用文档向量进行预加载
  3. 使用 LRU 策略管理缓存大小

避坑指南

向量维度对齐

常见错误是混合使用不同模型生成的向量。必须确保:

  • 所有文档使用相同模型编码
  • 查询时使用相同模型生成向量

权重系数调参

通过 A / B 测试确定最佳权重:

  1. 准备验证集包含各类查询(术语型、语义型、混合型)
  2. 测试不同权重下的召回率和准确率
  3. 生产环境中可动态调整(如对专业术语查询降低语义权重)

分布式部署

确保一致性的方法:

  1. 使用 Zookeeper 管理配置
  2. 实现向量索引的版本控制
  3. 采用两阶段提交更新索引

验证指标

在 MSMARCO 数据集上的测试结果:

方法 Recall@10 平均延迟 (ms)
纯语义 0.72 120
纯关键词 0.65 45
混合检索 0.83 80

总结与思考

混合检索系统在实际应用中表现出色,但仍有一些开放性问题值得探讨:当查询语义与关键词完全冲突时(如搜索 ”Apple fruit” 但文档中使用的是 ” 苹果公司 ”),该如何设计降级策略?一种可能的方案是引入用户画像和历史行为数据来动态调整权重,但这又会带来新的复杂性和隐私考量。期待与大家一起探讨这个有趣的挑战。

正文完
 0
评论(没有评论)