共计 2541 个字符,预计需要花费 7 分钟才能阅读完成。
背景痛点
在实际应用中,我们发现纯语义检索和传统关键词检索各有优劣。纯语义检索(如基于 BERT 的稠密向量检索)虽然能很好地理解查询意图,但在处理长尾查询和专业术语时表现不佳。例如:

- 当用户搜索非常冷门的专业术语时,语义模型可能无法准确理解其含义
- 对拼写错误或缩写词的容错能力较差
而传统关键词检索(如 BM25)虽然能精确匹配这些术语,但完全无法理解语义相关性。比如搜索 ” 苹果 ” 时,无法区分是指水果还是科技公司。
技术选型
在构建混合检索系统时,我们对比了几种主流向量数据库:
- Faiss:专注于向量检索,性能优异但功能单一
- Weaviate:功能全面但资源消耗较大
- Chroma:轻量级(仅 300KB 内存占用),原生支持多模态,API 设计简洁
最终选择 Chroma 的主要考虑因素包括:
- 开箱即用的混合检索支持
- 极低的资源开销
- 活跃的社区和持续更新
- 简洁的 Python API
混合架构实现
整体流程
混合检索的核心思想是同时利用两种检索方式的结果,通过加权打分进行融合。具体流程如下:
- 对查询同时进行稠密向量转换和关键词提取
- 分别执行向量相似度搜索和关键词匹配
- 对两组结果进行分数归一化和加权融合
- 按最终分数排序返回结果
代码实现
以下是核心代码示例(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 []
性能优化
索引分片策略
- 按文档类型或主题进行分片,减少单次查询范围
- 实现动态分片加载,只加载必要的数据
ANN 参数调优
Chroma 使用 HNSW(Hierarchical Navigable Small World)算法进行近似最近邻搜索,关键参数包括:
ef_construction:影响索引构建质量和内存使用(建议值 100-200)M:影响图的连通性和搜索速度(建议值 16-32)
缓存设计
- 查询缓存:对热门查询结果缓存 30 秒
- 向量缓存:对常用文档向量进行预加载
- 使用 LRU 策略管理缓存大小
避坑指南
向量维度对齐
常见错误是混合使用不同模型生成的向量。必须确保:
- 所有文档使用相同模型编码
- 查询时使用相同模型生成向量
权重系数调参
通过 A / B 测试确定最佳权重:
- 准备验证集包含各类查询(术语型、语义型、混合型)
- 测试不同权重下的召回率和准确率
- 生产环境中可动态调整(如对专业术语查询降低语义权重)
分布式部署
确保一致性的方法:
- 使用 Zookeeper 管理配置
- 实现向量索引的版本控制
- 采用两阶段提交更新索引
验证指标
在 MSMARCO 数据集上的测试结果:
| 方法 | Recall@10 | 平均延迟 (ms) |
|---|---|---|
| 纯语义 | 0.72 | 120 |
| 纯关键词 | 0.65 | 45 |
| 混合检索 | 0.83 | 80 |
总结与思考
混合检索系统在实际应用中表现出色,但仍有一些开放性问题值得探讨:当查询语义与关键词完全冲突时(如搜索 ”Apple fruit” 但文档中使用的是 ” 苹果公司 ”),该如何设计降级策略?一种可能的方案是引入用户画像和历史行为数据来动态调整权重,但这又会带来新的复杂性和隐私考量。期待与大家一起探讨这个有趣的挑战。
正文完
发表至: 未分类
近三天内
