向量数据库实战(Milvus/Chroma)
从 Embedding 的第一个维度到 RAG 系统的最后一公里——手把手带你玩转向量数据库。
7. 混合检索——当语义搜索遇上关键词搜索
前两章我们分别掌握了 Chroma 和 Milvus 的核心操作。但在实际 RAG 项目中,你很快会遇到一个问题:纯语义搜索有盲区。
这一章,我们引入混合检索(Hybrid Search) 和 Reranker 重排序,把检索精度再提升一个台阶。
7.1 语义搜索的盲区
语义搜索很强大,但它不是万能的。有几类场景它会"翻车":
语义搜索翻车案例:
案例 1:精确术语 / ID 号码
─────────────────────────────────────
用户问:"ERROR_CODE_4032 是什么意思?"
语义搜索结果:
→ "常见错误代码汇总"(相似度 0.72)
→ "HTTP 状态码 403 详解"(相似度 0.68) ← 错了!
→ "应用程序错误处理指南"(相似度 0.65)
问题:
→ 用户要的是精确的 ERROR_CODE_4032
→ 语义搜索把它理解成"错误代码"这个概念
→ 找到了语义相关但不精确的结果
案例 2:专有名词 / 缩写
─────────────────────────────────────
用户问:"BGE-M3 模型的参数量是多少?"
语义搜索结果:
→ "大语言模型参数量对比"(相似度 0.75)
→ "BERT 模型架构详解"(相似度 0.70) ← 不对
→ "如何选择 Embedding 模型"(相似度 0.68)
问题:
→ BGE-M3 是一个精确的模型名称
→ 语义搜索把它理解成"模型"这个主题
→ 找到了关于"模型"的泛结果,而非 BGE-M3 专属文档
案例 3:带数字的精确查询
─────────────────────────────────────
用户问:"Python 3.12 新增了什么特性?"
语义搜索结果:
→ "Python 新特性速览"(相似度 0.82)
→ "Python 3.11 性能优化"(相似度 0.79) ← 版本不对
→ "Python 3.10 的结构化模式匹配"(0.76) ← 版本不对
问题:
→ 语义搜索不擅长区分 3.10 / 3.11 / 3.12
→ 它认为这些都是"Python 新特性",语义高度相似根本原因:语义搜索(Dense Retrieval)擅长理解"大致意思",但不擅长精确匹配特定的词汇、编号、版本号。而传统的关键词搜索(BM25)恰恰擅长这些。
7.2 Dense + Sparse 混合检索原理
解决方案很直觉——两种搜索方式的优势互补:
混合检索 = Dense(语义) + Sparse(关键词)
Dense Retrieval(密集检索)
─────────────────────────────────────
• 方式:Embedding 向量 + 余弦相似度
• 擅长:理解语义、同义词匹配、概念关联
• 不擅长:精确术语、ID、数字
Sparse Retrieval(稀疏检索)
─────────────────────────────────────
• 方式:BM25 / TF-IDF / 稀疏向量
• 擅长:精确关键词匹配、术语、ID
• 不擅长:同义词、概念理解
混合检索:取两者之长
─────────────────────────────────────
• 用 Dense 找"语义相关"的文档
• 用 Sparse 找"关键词精确匹配"的文档
• 合并两组结果,取并集
• 用加权分数排序什么是 Sparse Embedding
Dense vs Sparse 向量对比:
Dense 向量(语义向量):
"Python 性能优化" → [0.82, 0.15, 0.93, -0.41, ..., 0.23]
↑ 每个维度都有值
↑ 1024 维,全是非零浮点数
↑ 维度没有可解释的含义
Sparse 向量(稀疏向量):
"Python 性能优化" → {
"Python": 2.31, ← 词汇 ID → 权重
"性能": 1.87,
"优化": 1.92,
...其他 30000 个维度都是 0
}
↑ 只有出现的词有值
↑ 30000+ 维,但 99% 是零
↑ 每个维度对应一个词汇
关键区别:
Dense:编码语义信息 → "跑得更快" ≈ "性能优化"
Sparse:编码词汇信息 → "Python 3.12" 只匹配含 "3.12" 的文档混合检索的融合策略
两组结果怎么合并?——RRF(Reciprocal Rank Fusion)
Dense 搜索结果(按语义排序):
排名 1: 文档 A(语义最相关)
排名 2: 文档 C
排名 3: 文档 B
Sparse 搜索结果(按关键词匹配排序):
排名 1: 文档 B(关键词最匹配)
排名 2: 文档 A
排名 3: 文档 D
RRF 融合公式:
score(doc) = Σ 1 / (k + rank_i)
k = 60(常用值)
文档 A:1/(60+1) + 1/(60+2) = 0.0164 + 0.0161 = 0.0325
文档 B:1/(60+3) + 1/(60+1) = 0.0159 + 0.0164 = 0.0323
文档 C:1/(60+2) + 0 = 0.0161
文档 D:0 + 1/(60+3) = 0.0159
最终排序:A > B > C > D
→ 同时在两个列表中排名靠前的文档(A)得分最高
→ 只在一个列表中出现的文档(C、D)得分低核心思想:如果一个文档既"语义相关"又"关键词匹配",它几乎一定是最好的答案。RRF 正是基于这个直觉,让两种搜索互相验证。
7.3 Milvus 混合检索实战
Milvus 从 2.4 版本开始原生支持混合检索。我们来完整实现一个 Dense + Sparse 的混合搜索。
Schema 设计:双向量字段
python
from pymilvus import (
connections, Collection, CollectionSchema,
FieldSchema, DataType, AnnSearchRequest,
RRFRanker, WeightedRanker
)
connections.connect(host="localhost", port="19530")
# 关键:同时包含 Dense 和 Sparse 两个向量字段
fields = [
FieldSchema(name="id", dtype=DataType.INT64,
is_primary=True, auto_id=True),
FieldSchema(name="title", dtype=DataType.VARCHAR, max_length=512),
FieldSchema(name="content", dtype=DataType.VARCHAR, max_length=2000),
# Dense 向量:语义搜索用
FieldSchema(name="dense_embedding", dtype=DataType.FLOAT_VECTOR, dim=1024),
# Sparse 向量:关键词搜索用
FieldSchema(name="sparse_embedding", dtype=DataType.SPARSE_FLOAT_VECTOR),
]
schema = CollectionSchema(fields, description="混合检索知识库")
collection = Collection("hybrid_docs", schema)生成 Dense 和 Sparse 向量
python
from pymilvus.model.hybrid import BGEM3EmbeddingFunction
# BGE-M3 模型可以同时生成 Dense 和 Sparse 向量!
bge_m3 = BGEM3EmbeddingFunction(
model_name="BAAI/bge-m3",
use_fp16=False,
device="cpu"
)
# 准备文档
docs = [
"Python 3.12 的 per-interpreter GIL 特性详解",
"Python 异步编程 asyncio 完全指南",
"ERROR_CODE_4032:数据库连接超时的排查方法",
"BGE-M3 模型的架构设计与训练方法",
"如何优化 Python 程序的运行性能",
"FastAPI 框架中的异步请求处理",
]
# 一次调用同时生成两种向量
embeddings = bge_m3.encode_documents(docs)
print(f"Dense 向量维度:{len(embeddings['dense'][0])}") # 1024
print(f"Sparse 向量非零元素:{len(embeddings['sparse'][0])}") # ~50-200插入数据与建索引
python
# 插入数据
collection.insert([
[d[:100] for d in docs], # title(截取前100字)
docs, # content
embeddings["dense"], # dense_embedding
embeddings["sparse"], # sparse_embedding
])
collection.flush()
# 分别为两个向量字段建索引
# Dense 索引
collection.create_index(
"dense_embedding",
{"metric_type": "COSINE", "index_type": "HNSW",
"params": {"M": 16, "efConstruction": 200}}
)
# Sparse 索引
collection.create_index(
"sparse_embedding",
{"metric_type": "IP", "index_type": "SPARSE_INVERTED_INDEX",
"params": {"drop_ratio_build": 0.2}} # 丢弃权重最低的 20% 词汇
)
collection.load()执行混合搜索
python
# 准备查询
query = "BGE-M3 模型的参数量是多少?"
query_embeddings = bge_m3.encode_queries([query])
# 构建两个搜索请求
dense_req = AnnSearchRequest(
data=query_embeddings["dense"],
anns_field="dense_embedding",
param={"metric_type": "COSINE", "params": {"ef": 128}},
limit=10
)
sparse_req = AnnSearchRequest(
data=query_embeddings["sparse"],
anns_field="sparse_embedding",
param={"metric_type": "IP", "params": {}},
limit=10
)
# 混合搜索 + RRF 融合
results = collection.hybrid_search(
reqs=[dense_req, sparse_req],
ranker=RRFRanker(k=60), # RRF 融合,k=60
limit=5,
output_fields=["title", "content"]
)
# 打印结果
for hit in results[0]:
print(f" 标题: {hit.entity.get('title')}")
print(f" 融合分数: {hit.distance:.4f}")
print(f" ---")混合搜索 vs 纯语义搜索的效果对比:
查询:"BGE-M3 模型的参数量是多少?"
纯语义搜索 Top-3:
1. "如何优化 Python 程序的运行性能" ← ❌ 不相关
2. "BGE-M3 模型的架构设计与训练方法" ← ✅ 相关
3. "Python 异步编程 asyncio 完全指南" ← ❌ 不相关
混合搜索 Top-3:
1. "BGE-M3 模型的架构设计与训练方法" ← ✅ 精确匹配
2. "如何优化 Python 程序的运行性能" ← 语义相关
3. "ERROR_CODE_4032:数据库连接超时" ← (被过滤)
→ 混合搜索通过 Sparse 的精确匹配
把包含 "BGE-M3" 关键词的文档提到了第一位加权融合:如果你觉得语义搜索更重要,可以用
WeightedRanker(0.7, 0.3)替代RRFRanker,给 Dense 搜索 70% 的权重、Sparse 搜索 30% 的权重。
7.4 Reranker 重排序:精度的最后一公里
混合检索已经比纯语义搜索好很多了。但还有一个杀手锏——Reranker(重排序模型)。
为什么需要重排序
检索 vs 重排序——两阶段流水线:
阶段 1:检索(Retrieval)—— 快速筛选
─────────────────────────────────────
• 从百万文档中找出 Top-20 候选
• 要求:速度快(毫秒级)
• 方式:向量相似度(ANN 搜索)
• 精度:还行,但有噪声
阶段 2:重排序(Reranking)—— 精细排序
─────────────────────────────────────
• 对 Top-20 候选做精确的相关性判断
• 要求:精度高(可以稍慢)
• 方式:交叉编码器(Cross-Encoder)
• 精度:显著提升
为什么不直接用 Reranker 搜索全量数据?
→ 因为太慢了!
→ Reranker 是逐对比较(query, doc),无法利用索引
→ 100 万条 × 逐一比较 = 几小时
→ 但对 20 条候选做 Rerank = 50msBi-Encoder vs Cross-Encoder
两种模型的区别:
Bi-Encoder(双编码器,用于检索)
─────────────────────────────────────
Query: "Python 性能优化" ──→ [向量 Q]
Document: "提速 Python 程序" ──→ [向量 D]
→ Query 和 Document 分别独立编码
→ 可以预先计算文档向量
→ 搜索时只需计算 Query 向量 + 向量距离
→ 速度:极快 ✅ 精度:中等 ⚠️
Cross-Encoder(交叉编码器,用于重排序)
─────────────────────────────────────
输入: ["Python 性能优化", "提速 Python 程序"]
↓
Transformer 联合编码
↓
相关性分数:0.92
→ Query 和 Document 拼接在一起送入模型
→ 模型能看到两者的交互关系
→ 不能预计算,必须逐对比较
→ 速度:慢 ❌ 精度:极高 ✅Reranker 实战代码
python
# pip install sentence-transformers
from sentence_transformers import CrossEncoder
# 加载重排序模型(中文推荐 bge-reranker)
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3", max_length=512)
# 假设混合检索返回了这些候选文档
query = "BGE-M3 模型的参数量是多少?"
candidates = [
"BGE-M3 模型的架构设计与训练方法",
"如何优化 Python 程序的运行性能",
"Python 异步编程 asyncio 完全指南",
"大语言模型参数量对比分析",
"Embedding 模型选型指南",
]
# 构建 query-document 对
pairs = [[query, doc] for doc in candidates]
# 计算相关性分数
scores = reranker.predict(pairs)
# 按分数重排序
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
for doc, score in ranked:
print(f" [{score:.4f}] {doc}")
# 输出:
# [0.9847] BGE-M3 模型的架构设计与训练方法 ← 🎯 最相关
# [0.7231] 大语言模型参数量对比分析 ← 也相关
# [0.3012] Embedding 模型选型指南 ← 有关联
# [0.0234] 如何优化 Python 程序的运行性能 ← 不相关
# [0.0089] Python 异步编程 asyncio 完全指南 ← 不相关完整的两阶段检索流程
python
def hybrid_search_with_rerank(
query: str,
collection,
bge_m3,
reranker,
top_k_retrieval: int = 20,
top_k_final: int = 5,
) -> list[dict]:
"""混合检索 + Reranker 两阶段流水线"""
# === 阶段 1:混合检索,快速获取候选集 ===
query_emb = bge_m3.encode_queries([query])
dense_req = AnnSearchRequest(
data=query_emb["dense"], anns_field="dense_embedding",
param={"metric_type": "COSINE", "params": {"ef": 128}},
limit=top_k_retrieval
)
sparse_req = AnnSearchRequest(
data=query_emb["sparse"], anns_field="sparse_embedding",
param={"metric_type": "IP", "params": {}},
limit=top_k_retrieval
)
candidates = collection.hybrid_search(
reqs=[dense_req, sparse_req],
ranker=RRFRanker(k=60),
limit=top_k_retrieval,
output_fields=["title", "content"]
)
# === 阶段 2:Reranker 精排 ===
docs = [hit.entity.get("content") for hit in candidates[0]]
pairs = [[query, doc] for doc in docs]
scores = reranker.predict(pairs)
# 按 Reranker 分数排序,取 Top-K
results = sorted(
zip(candidates[0], scores),
key=lambda x: x[1], reverse=True
)[:top_k_final]
return [
{"title": hit.entity.get("title"),
"score": float(score),
"content": hit.entity.get("content")}
for hit, score in results
]性能参考:20 条候选的 Reranker 处理时间约 30-50ms(CPU)或 5-10ms(GPU)。这个延迟在大多数应用中完全可接受。
本章小结
| 知识点 | 要点 |
|---|---|
| 语义搜索盲区 | 不擅长精确术语、ID 号码、版本号等精确匹配 |
| Dense 检索 | 语义向量搜索,擅长同义词和概念理解 |
| Sparse 检索 | 稀疏向量/BM25,擅长精确关键词匹配 |
| 混合检索 | Dense + Sparse 互补,用 RRF 或加权融合结果 |
| BGE-M3 | 同时生成 Dense 和 Sparse 向量,一步到位 |
| Reranker | 交叉编码器,对候选集精细重排序,精度最高 |
| 两阶段流水线 | 检索(快+粗)→ 重排序(慢+精),工业界标准做法 |
| 推荐模型 | bge-reranker-v2-m3(中文重排序首选) |
下一章预告:实战:构建完整的 RAG 知识库问答系统。我们会串联前 7 章的所有知识,从文档解析→分块→向量化→存储→混合检索→Rerank→LLM 生成,构建一个完整可运行的知识库问答应用。