跳到主要内容

2 篇博文 含有标签「Milvus」

查看所有标签

修复 Milvus 混合检索 RRF 分数与相似度阈值不兼容

· 阅读需 3 分钟

在 RAG 知识库项目中调试混合检索评分问题,以下是完整排查过程。

TL;DR

Milvus 混合检索的加权融合分数 = 0.7 * dense_score + 0.3 * sparse_score,理论最大值约 0.7。如果用 min_similarity=0.7 过滤,结果几乎全被剔除。解决方案:将阈值降到 0.3,或根据融合策略动态调整。

问题现象

混合检索返回空结果,即使数据库中明确存在相关文档:

# 调用混合检索
results = await milvus_service.hybrid_search(
collection_name="knowledge_base",
query_dense=dense_vector,
query_sparse=sparse_vector,
top_k=5,
min_similarity=0.7 # 问题根源
)

# 返回空数组 []
print(results) # {"documents": [[]], "metadatas": [[]], "distances": [[]]}

日志显示检索到了结果,但过滤后为空:

fused_results before filter: 10, scores: [0.52, 0.48, 0.45, ...]
min_similarity threshold: 0.7
fused_results after filter: 0, scores: []

根因

混合检索使用加权融合(Weighted Fusion)而非 Reciprocal Rank Fusion(RRF):

def _fuse_and_rank(self, dense_results, sparse_results, top_k):
semantic_weight = 0.7 # 语义权重
keyword_weight = 0.3 # 关键词权重

for result in dense_results:
similarity = 1 - distance
score = similarity * semantic_weight # 0.7 * score

for result in sparse_results:
similarity = 1 - distance
score = similarity * keyword_weight # 0.3 * score

# 同一文档的分数相加
final_score = dense_score + sparse_score

数学分析

  • 假设 dense 和 sparse 的相似度最大值都是 1.0
  • 融合分数最大值 = 0.7 * 1.0 + 0.3 * 1.0 = 1.0
  • 但实际中 sparse 分数通常较低(0.3-0.5),因为关键词很难完全匹配
  • 实际最大分数约 0.5-0.7

min_similarity=0.7 过滤,相当于要求"完美匹配",结果自然为空。

解决方案

方案一:降低阈值(推荐)

# config.py
class Settings(BaseSettings):
rag_min_similarity: float = 0.3 # 混合搜索分数阈值(加权分数通常较低)

方案二:动态阈值

根据检索类型使用不同阈值:

# 混合检索用较低阈值
if search_type == "hybrid":
min_similarity = 0.3
else:
min_similarity = 0.7 # 纯语义检索可用较高阈值

方案三:归一化融合分数

将融合分数归一化到 [0, 1]:

def _fuse_and_rank(self, dense_results, sparse_results, top_k):
# ... 融合逻辑 ...

# 归一化:除以权重和
max_possible_score = self.semantic_weight + self.keyword_weight # 1.0
for doc in doc_scores.values():
doc["score"] = doc["score"] / max_possible_score

return sorted_docs[:top_k]

FAQ

Q: 为什么混合检索分数比纯语义检索低?

A: 混合检索的分数是加权和,而非单纯的相似度。语义检索返回的是 0-1 的余弦相似度,而混合检索的分数是 0.7*dense + 0.3*sparse,即使两部分都是 1.0,最终也只有 1.0。但实际中 sparse 分数通常较低,导致总分偏低。

Q: RRF(Reciprocal Rank Fusion)和加权融合有什么区别?

A: RRF 基于排名位置计算:score = 1/(k+rank),与原始相似度无关。加权融合直接用相似度分数加权,更直观但需要调整阈值。Milvus 原生支持加权融合,RRF 需要自己实现。

Q: 阈值设为 0.3 会不会引入低质量结果?

A: 需要结合业务场景测试。0.3 是一个经验值,如果发现结果质量下降,可以:

  1. 提高到 0.4-0.5
  2. 在应用层做二次过滤
  3. 使用 LLM 对结果做相关性打分

修复 Milvus 混合搜索的四个常见坑

· 阅读需 3 分钟

在 RAG 知识库项目中调试混合检索评分问题,以下是完整排查过程。

TL;DR

Milvus 混合搜索(Dense + Sparse)有四个常见坑:空稀疏向量报错、Collection 未加载、sparse 格式错误、阈值过高。本文给出每个问题的最小修复代码。

问题现象

坑 1:空稀疏向量插入失败

MilvusException: (code=65535, message=empty sparse float vector row)

坑 2:Collection 未加载

MilvusException: (code=101, message=failed to search: collection not loaded[collection=xxx])

坑 3:Sparse 向量格式错误

ParamError: (code=1, message=`search_data` value [{0: {81705: 1.3486}}] is illegal)

坑 4:搜索无结果(分数被过滤)

{"answer": "抱歉,知识库中没有相关内容", "similarity": 0.0}

根因

坑 1:Milvus 的 SPARSE_FLOAT_VECTOR 类型不接受空字典 {},必须有至少一个键值对。

坑 2:Milvus 2.4+ 要求搜索前显式调用 load_collection(),否则报 collection not loaded。

坑 3:DashScope API 返回的 sparse 格式是 {text_index: sparse_vec},搜索时需要提取 sparse_vec 本身,而非整个嵌套结构。

坑 4:混合搜索的分数是加权组合(如 0.7 * dense_score + 0.3 * sparse_score),通常在 0.3-0.5 之间。如果阈值设为 0.7,所有结果都会被过滤。

解决方案

坑 1:为空稀疏向量添加占位符

# 获取稀疏向量,如果为空则使用最小占位符
sparse_vec = sparse_vectors.get(chunk_idx, {})
if not sparse_vec:
sparse_vec = {0: 0.0} # Milvus 不接受空 sparse vector

data = {
"dense_vector": dense_embeddings[chunk_idx],
"sparse_vector": sparse_vec, # 保证非空
"text": chunk,
"doc_id": doc_id,
"metadata": metadata
}

坑 2:搜索前加载 Collection

async def hybrid_search(self, collection_name: str, ...):
self.get_or_create_collection(collection_name)

# Milvus 2.4+ 要求:搜索前必须加载
self.client.load_collection(collection_name=collection_name)

dense_results = self.client.search(...)
sparse_results = self.client.search(...)

坑 3:正确提取 Sparse 向量

async def embed_query(self, text: str) -> dict:
result = await self._embed_batch([text], text_type="query", use_instruct=True)
# _embed_batch 返回 {"sparse": {0: sparse_vec}}
# 需要提取 index 0 的向量本身
return {
"dense": result["dense"][0],
"sparse": result["sparse"].get(0, {}) # 提取 sparse_vec
}

坑 4:调整混合搜索阈值

# config.py 或环境变量
rag_min_similarity: float = 0.3 # 过滤阈值(原 0.7 过高)
rag_refuse_similarity: float = 0.3 # 拒答阈值(原 0.5 过高)

混合搜索分数计算公式:

# 典型分数范围:0.3 - 0.5
score = dense_similarity * 0.7 + sparse_similarity * 0.3

FAQ

Q: Milvus 为什么不接受空的稀疏向量?

A: Milvus 的 SPARSE_FLOAT_VECTOR 类型要求每行至少有一个非零元素。空字典 {} 无法确定向量维度,会触发 empty sparse float vector row 错误。使用 {0: 0.0} 作为占位符即可绕过。

Q: Milvus 2.4 搜索前必须调用 load_collection 吗?

A: 是的。Milvus 2.4+ 默认不自动加载 Collection 到内存,必须显式调用 client.load_collection(collection_name) 后才能搜索。这是性能优化设计,避免不用的 Collection 占用内存。

Q: 混合搜索的分数为什么通常只有 0.3-0.5?

A: 混合搜索分数是加权组合(如 0.7 * dense + 0.3 * sparse)。即使两个检索都完美匹配(1.0),加权后最高也只有 1.0。实际场景中 dense 和 sparse 分数很少同时为 1.0,所以典型分数在 0.3-0.5。阈值应设为 0.3 左右,而非 0.7。

Q: DashScope sparse embedding 返回什么格式?

A: DashScope 返回 {"embeddings": [{"sparse_embedding": [{"index": 123, "value": 0.5}, ...]}]}。批量调用时,转换后格式为 {text_index: {dim_index: value}}。搜索时需要用 .get(0, {}) 提取第一条的 sparse 向量。