Milvus Reranker重排 前言 什么是 Milvus Reranker? 在向量数据库的检索场景中,我们经常需要从多个维度对搜索结果进行综合评估。例如,在推荐系统中,我们可能需要同时考虑内容相似度、用户偏好、地理位置、时间新鲜度等多个因素。单一的向量相似度搜索往往无法满足这种复杂的业务需求。 Reranker(重排器) 就是为了解决这个问题而设计的。它的工作流程是: 首先通过向量检索获得初步的候选结果集 然后根据业务规则和多维度特征对这些结果重新打分 最后按照新的分数重新排序,得到最终的推荐列表 这种"先召回、后精排"的两阶段策略,在搜索引擎、推荐系统等领域被广泛应用。 Milvus Reranker 的功能特性 从 Milvus 2.
在向量数据库的检索场景中,我们经常需要从多个维度对搜索结果进行综合评估。例如,在推荐系统中,我们可能需要同时考虑内容相似度、用户偏好、地理位置、时间新鲜度等多个因素。单一的向量相似度搜索往往无法满足这种复杂的业务需求。
Reranker(重排器) 就是为了解决这个问题而设计的。它的工作流程是:
这种"先召回、后精排"的两阶段策略,在搜索引擎、推荐系统等领域被广泛应用。
从 Milvus 2.6 版本开始,系统内置了强大的 Reranker 功能,主要包括以下几种排名策略:
本教程将通过一个完整的"美食推荐系统"案例,带你逐步掌握每种 Reranker 的使用方法。每个部分都包含:
在开始之前,请确保你的电脑已安装 Milvus 2.6或更高版本的 Python SDK:
! pip install "pymilvus>=2.6.0"
为了让大家更好地理解各种 Reranker 的工作原理,我们将构建一个美食推荐系统作为演示案例。
想象你正在开发一个美食 App,用户可以通过多种方式搜索餐厅:
这个场景完美地展示了多维度检索和重排的需求。在实际应用中,单纯依靠向量相似度往往不够,我们需要综合多个因素来给出最佳推荐结果。
我们的数据集包含以下字段:
| 字段名 | 类型 | 说明 | 用途 |
|---|---|---|---|
| id | INT64 | 餐厅ID | 主键标识 |
| desc_vector | FLOAT_VECTOR(2) | 口味描述向量 | 用于文本相似度搜索 |
| pic_vector | FLOAT_VECTOR(2) | 图片外观向量 | 用于图像相似度搜索 |
| distance | FLOAT | 距离(公里) | 用于衰变排名演示 |
| rating | FLOAT | 用户评分 | 用于加权排名演示 |
注意:为了简化演示,这里使用 2 维向量。在实际应用中,向量维度通常是768、1024 等更高维度。
我们将插入 15 条测试数据,按相似度分为四个组:
高度相似组(内积 > 0.7):
中度相似组(内积 0.4-0.7):ID=5,6,7,8
低度相似组(内积 < 0.4):ID=9,10,11
**极低相似组**(内积接近 0):ID=12,13,14,15
通过这些数据,我们可以清晰地观察:
请先运行以下代码,创建集合并插入测试数据:
from pymilvus import MilvusClient, DataType # 1. 连接到本地的一个临时数据库文件(使用 Milvus Lite) client = MilvusClient("milvus_demo.db") # 2. 如果集合已经存在,先删除,保证每次运行都是新的 col_name = "food_collection" if client.has_collection(col_name): client.drop_collection(col_name) # 3. 创建表结构(Schema) schema = client.create_schema(auto_id=False, enable_dynamic_field=True) schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True) schema.add_field(field_name="desc_vector", datatype=DataType.FLOAT_VECTOR, dim=2) # 描述向量 schema.add_field(field_name="pic_vector", datatype=DataType.FLOAT_VECTOR, dim=2) # 图片向量 schema.add_field(field_name="distance", datatype=DataType.FLOAT) # 距离(km) schema.add_field(field_name="rating", datatype=DataType.FLOAT) # 评分 # 4. 创建索引(向量搜索必须要有索引) # 注意:使用 IP (内积) 作为度量类型,值越大表示越相似 # 这样与 WeightedRanker 的"分数越大越好"逻辑一致 index_params = client.prepare_index_params() index_params.add_index(field_name="desc_vector", index_type="FLAT", metric_type="IP") index_params.add_index(field_name="pic_vector", index_type="FLAT", metric_type="IP") client.create_collection(col_name, schema=schema, index_params=index_params) # 5. 插入模拟数据(扩展到 15 条,充分展示 Reranker 效果) data = [ # 高度相似组(与查询向量 [1.0, 0.0] 的内积 > 0.7) {"id": 1, "desc_vector": [1.0, 0.0], "pic_vector": [1.0, 0.0], "distance": 100.0, "rating": 5.0}, # 完美匹配,但很远 {"id": 2, "desc_vector": [0.9, 0.1], "pic_vector": [0.8, 0.2], "distance": 5.0, "rating": 4.8}, # 很匹配,较近 {"id": 3, "desc_vector": [0.85, 0.15], "pic_vector": [0.85, 0.15], "distance": 25.0, "rating": 4.2}, # 很匹配,中等距离 {"id": 4, "desc_vector": [0.8, 0.2], "pic_vector": [0.7, 0.3], "distance": 0.0, "rating": 3.5}, # 匹配,最近 # 中度相似组(内积在 0.4-0.7 之间) {"id": 5, "desc_vector": [0.6, 0.4], "pic_vector": [0.5, 0.5], "distance": 2.0, "rating": 4.5}, {"id": 6, "desc_vector": [0.55, 0.45], "pic_vector": [0.6, 0.4], "distance": 15.0, "rating": 3.8}, {"id": 7, "desc_vector": [0.5, 0.5], "pic_vector": [0.5, 0.5], "distance": 50.0, "rating": 4.0}, {"id": 8, "desc_vector": [0.45, 0.55], "pic_vector": [0.4, 0.6], "distance": 8.0, "rating": 3.2}, # 低度相似组(内积 < 0.4) {"id": 9, "desc_vector": [0.3, 0.7], "pic_vector": [0.3, 0.7], "distance": 1.0, "rating": 2.8}, # 不匹配,但很近 {"id": 10, "desc_vector": [0.2, 0.8], "pic_vector": [0.2, 0.8], "distance": 30.0, "rating": 2.5}, {"id": 11, "desc_vector": [0.15, 0.85], "pic_vector": [0.1, 0.9], "distance": 60.0, "rating": 2.0}, # 极低相似组(内积接近 0) {"id": 12, "desc_vector": [0.0, 1.0], "pic_vector": [0.0, 1.0], "distance": 10.0, "rating": 1.5}, # 完全不匹配 {"id": 13, "desc_vector": [0.1, 0.9], "pic_vector": [0.1, 0.9], "distance": 0.5, "rating": 1.8}, {"id": 14, "desc_vector": [-0.1, 0.9], "pic_vector": [0.0, 1.0], "distance": 40.0, "rating": 1.2}, {"id": 15, "desc_vector": [0.05, 0.95], "pic_vector": [0.05, 0.95], "distance": 20.0, "rating": 1.0}, ] client.insert(col_name, data) print(f"✅ 准备工作完成!已插入 {len(data)} 条测试数据。")
代码说明:
MilvusClient 连接到本地 Milvus Lite 数据库(无需单独安装 Milvus 服务)加权排名(Weighted Ranker) 是最直观的多路召回融合方法。它的核心思想是:为每个检索维度分配一个权重系数,然后将各维度的得分按权重加权求和,得到最终分数。
数学公式:
最终分数 = w1 × score1 + w2 × score2 + ... + wn × scoren
其中:
w1, w2, ..., wn 是各维度的权重(通常要求权重之和为 1)score1, score2, ..., scoren 是各维度的原始得分加权排名适合以下场景:
在我们的美食推荐系统中,假设用户更看重文字描述的匹配度(比如菜品口味),而图片外观相对次要。我们可以设置:
from pymilvus import AnnSearchRequest, WeightedRanker # 模拟用户的搜索向量(用户想找 [1.0, 0.0] 这种特征的餐厅) query_vec = [[1.0, 0.0]] # ===== 先运行基线对比:单一向量搜索(无 Reranker)===== print("=== 基线:仅使用描述向量搜索(无 Reranker)===") baseline_res = client.search( collection_name="food_collection", data=query_vec, anns_field="desc_vector", limit=5 ) for hits in baseline_res: for i, hit in enumerate(hits): print(f"排名 {i+1}: ID={hit.id}, 内积分数={hit.score:.4f}, " f"距离={hit.entity.get('distance')}km, 评分={hit.entity.get('rating')}") # ===== 1. 准备两个搜索请求 ===== # 请求A:在文字描述向量字段中搜索(使用 IP 内积,分数越大越相似) req_desc = AnnSearchRequest( data=query_vec, anns_field="desc_vector", param={"metric_type": "IP"}, # IP: 内积,值越大越相似 limit=10 ) # 请求B:在图片外观向量字段中搜索 req_pic = AnnSearchRequest( data=query_vec, anns_field="pic_vector", param={"metric_type": "IP"}, limit=10 ) # ===== 2. 执行混合搜索,使用 WeightedRanker ===== res = client.hybrid_search( collection_name="food_collection", reqs=[req_desc, req_pic], # 列表里有两个请求 ranker=WeightedRanker(0.8, 0.2), # 权重分别对应 req_desc 和 req_pic limit=5 ) print("\n=== 加权排名结果(描述80% + 图片20%)===") for hits in res: for i, hit in enumerate(hits): print(f"排名 {i+1}: ID={hit.id}, 加权分数={hit.score:.4f}, " f"距离={hit.entity.get('distance')}km, 评分={hit.entity.get('rating')}")
运行上述代码,你会观察到:
基线结果(仅描述向量):
加权排名结果:
对比观察:
RRF(Reciprocal Rank Fusion,倒数排名融合) 是一种不依赖具体分数值的排名融合算法。它的核心思想是:只关注每个结果在各个检索路径中的排名位置,而不关心具体的分数值。
数学公式:
RRF_score = Σ (1 / (k + rank_i))
其中:
rank_i 是该结果在第 i 个检索路径中的排名(从 1 开始)k 是平滑系数(通常设为 60),用于避免排名靠前的结果得分差距过大在多路召回场景中,不同检索路径的分数可能不在同一量级:
RRF 通过只看排名位置,巧妙地解决了这个问题,实现了真正的"公平投票"。
RRF 适合以下场景:
在我们的美食推荐系统中,如果我们不确定文字和图片哪个更重要,或者想要一个公平的融合策略,就可以使用 RRF。
from pymilvus import RRFRanker # 使用前面定义的 req_desc 和 req_pic # 这里不需要设置权重,只需要给一个 k 值(平滑系数,官方推荐 60) res = client.hybrid_search( collection_name="food_collection", reqs=[req_desc, req_pic], ranker=RRFRanker(k=60), # k 值越大,排名靠前和靠后的结果得分差距越小 limit=3 ) print("=== RRF 排名结果 ===") for hits in res: for i, hit in enumerate(hits): print(f"排名 {i+1}: ID={hit.id}, RRF得分={hit.score:.6f}, 距离={hit.entity.get('distance')}km")
运行上述代码,你会发现:
衰变排名(Decay Ranker) 是一种基于数值字段的打分调整策略。它的核心思想是:某些数值字段(如距离、时间)会影响结果的价值,我们需要根据这些字段对原始分数进行"衰减"。
这里的衰减是在向量相似度基础上乘以一个动态权重(衰减因子),而不同的衰减函数类型决定了这个权重如何随数值字段(如时间、距离)变化而变化。
或者我们用更加大白话的逻辑去描述什么是衰变排名:就是在加权重排的基础上,增加一个控制权重变化的值k,而高斯衰减和线性或者指数衰减就是控制这个k的变化趋势的快慢平滑
衰变函数的通用形式:
最终分数 = 原始分数 × decay_factor
其中 decay_factor 是根据数值字段计算出的衰减系数(0 到 1 之间)。
在使用衰变排名时,需要理解三个核心参数:
Origin(原点):最理想的数值,衰减系数为 1(不衰减)
Scale(范围):衰减的尺度单位
Decay(衰减值):在 Origin + Scale 位置处的衰减系数
2.Scale范围和Decay衰减值必须要成对理解,这一对共同组成了一个路标,表示当距离到达这么远的时候,分数应该掉到了多少。
- 例子:
scale = 10(公里),decay = 0.5
意思就是:“嘿,兄弟,当你走到 10公里 (Scale) 的地方时,分数要正好打 5折 (Decay),变成 0.5分。”
综合演示:把参数串起来看
我们假设参数如下:
origin = 0offset = 1 (1公里内不扣分)scale = 10 (路标插在10公里处)decay = 0.5 (路标处剩0.5分)你的打分心理活动如下:
gauss_ranker = Function( name="rent_house_score", input_field_names=["distance"], function_type=FunctionType.RERANK, params={ "reranker": "decay", "function": "gauss", "origin": 0.0, # 完美位置:公司楼下 "offset": 1.0, # 宽容度:1公里内我都觉得跟住公司楼下没区别(满分) # 下面两行定了一个基准点: "scale": 10.0, # 当距离达到 10公里 时... "decay": 0.5 # ...分数要降到 0.5 (一半) } )
衰变排名适合以下场景:
Milvus 提供了三种衰减函数,它们的衰减速度和形状各不相同:
| 衰减类型 | 衰减速度 | 曲线形状 | 适用场景 |
|---|---|---|---|
| 高斯衰减 | 中等,平滑 | 钟形曲线 | 地理位置、用户偏好 |
| 指数衰减 | 快速,激进 | 指数曲线 | 时间敏感、热度排名 |
| 线性衰减 | 均匀,可预测 | 直线 | 硬性过滤、倒计时 |
高斯衰减采用正态分布(钟形曲线)的衰减方式,具有以下特点:
1.0 -> 0.99 -> 0.95 -> (突然加速) -> 0.5 -> 0.1decay_factor = exp(-((distance - origin) / scale)² / ln(1/decay))
验证:当 distance = origin + scale 时:
decay_factor = exp(-(scale/scale)² / ln(1/decay)) = exp(-1 / ln(1/decay)) = decay
例如:origin=0, scale=50, decay=0.5
from pymilvus import Function, FunctionType # 定义高斯衰减函数 gauss_ranker = Function( name="gauss_distance_decay", # 函数名称(自定义) function_type=FunctionType.Rerank, # 指定为 Rerank 类型 params={ "origin": 0.0, # 完美距离:0km(衰减系数=1) "scale": 50.0, # 衰减范围:50km "decay": 0.5, # 在 50km 处,衰减系数=0.5 "field_name": "distance" # 对哪个字段进行衰减 } ) # 执行混合搜索(需要先定义 req_desc) # 先定义搜索请求 query_vec = [[1.0, 0.0]] req_desc = AnnSearchRequest( data=query_vec, anns_field="desc_vector", param={"metric_type": "IP"}, limit=10 ) # 使用高斯衰减进行搜索 res = client.hybrid_search( collection_name="food_collection", reqs=[req_desc], ranker=gauss_ranker, limit=5 ) print("=== 高斯衰减结果(考虑距离)===") for hits in res: for i, hit in enumerate(hits): print(f"排名 {i+1}: ID={hit.id}, 衰减后分数={hit.score:.4f}, " f"距离={hit.entity.get('distance')}km, 评分={hit.entity.get('rating')}")
运行上述代码,你会观察到:
对比基线(纯向量搜索):
应用高斯衰减后:
这展示了距离衰减如何平衡相似度和地理位置:近但不太完美的餐厅可以超过远但完美的餐厅。
指数衰减采用指数函数的衰减方式,具有以下特点:
1.0 -> 0.5 -> 0.1 -> 0.01decay_factor = decay^(|distance - origin| / scale)
验证:当 distance = origin + scale 时:
decay_factor = decay^(scale/scale) = decay^1 = decay
例如:origin=0, scale=50, decay=0.5
# 定义指数衰减函数 exp_ranker = Function( name="exp_distance_decay", function_type=FunctionType.Rerank, params={ "origin": 0.0, "scale": 50.0, "decay": 0.5, "field_name": "distance" } ) # 执行混合搜索(使用指数衰减) res = client.hybrid_search( collection_name="food_collection", reqs=[req_desc], ranker=exp_ranker, limit=5 ) print("=== 指数衰减结果(激进的距离衰减)===") for hits in res: for i, hit in enumerate(hits): print(f"排名 {i+1}: ID={hit.id}, 衰减后分数={hit.score:.4f}, " f"距离={hit.entity.get('distance')}km, 评分={hit.entity.get('rating')}")
运行上述代码,你会发现:
与高斯衰减对比:
适用场景判断:
假设 origin=0, scale=50, decay=0.5:
关键区别:
线性衰减采用线性函数的衰减方式,具有以下特点:
1.0 -> 0.9 -> 0.8 ... -> 0decay_factor = max(0, 1 - (1 - decay) × |distance - origin| / scale)
# 定义线性衰减函数 linear_ranker = Function( name="linear_distance_decay", function_type=FunctionType.Rerank, params={ "origin": 0.0, "scale": 50.0, "decay": 0.5, "field_name": "distance" } ) # 执行混合搜索(使用线性衰减) res = client.hybrid_search( collection_name="food_collection", reqs=[req_desc], ranker=linear_ranker, limit=5 ) print("=== 线性衰减结果(硬性范围限制)===") for hits in res: for i, hit in enumerate(hits): print(f"排名 {i+1}: ID={hit.id}, 衰减后分数={hit.score:.4f}, " f"距离={hit.entity.get('distance')}km, 评分={hit.entity.get('rating')}")
运行上述代码,你会发现:
线性衰减的特点:
硬性截断效果:
根据参数 origin=0, scale=50, decay=0.5:
截断距离 = origin + scale × 2 = 0 + 50 × 2 = 100km
三种衰减函数的选择建议:
| 场景类型 | 推荐算法 | 理由 |
|---|---|---|
| 地理位置推荐(餐厅、房源) | 高斯衰减 | 平滑过渡,用户体验好 |
| 时间敏感内容(新闻、动态) | 指数衰减 | 强调新鲜度,快速淘汰旧内容 |
| 硬性范围限制(配送、服务) | 线性衰减 | 明确边界,超出范围直接过滤 |
| 价格偏好 | 高斯衰减 | 允许一定偏差,但不会完全排除 |
| 库存预警 | 指数衰减 | 库存越少,优先级下降越快 |
Booster Reranker 并不是 Milvus 中的一个独立类,而是一种设计思想:通过某种机制"提升"(Boost)特定结果的排名。
在 Milvus 2.6 中,实现 Booster 效果的主要方式有:
Booster 适合以下场景:
最简单的 Booster 实现就是利用加权排名,给需要提升的维度分配更高的权重。
from pymilvus import AnnSearchRequest, WeightedRanker # 假设我们想要"提升"评分高的餐厅 # 我们可以创建一个基于 rating 字段的虚拟搜索请求 # 方式1:通过调整权重来 Boost 某个维度 # 给文字描述权重 0.6,图片权重 0.4(相比之前的 0.8, 0.2,图片权重提升了) res = client.hybrid_search( collection_name="food_collection", reqs=[req_desc, req_pic], ranker=WeightedRanker(0.6, 0.4), # 提升了图片的权重 limit=3 ) print("=== Booster 效果(提升图片权重)===") for hits in res: for i, hit in enumerate(hits): print(f"排名 {i+1}: ID={hit.id}, 分数={hit.score:.4f}, 评分={hit.entity.get('rating')}")
虽然 Milvus 的 hybrid_search 一次只能使用一个 Reranker,但我们可以通过多次搜索来实现组合效果:
# 第一步:使用向量搜索召回 res = client.hybrid_search( collection_name="food_collection", reqs=[req_desc, req_pic], ranker=WeightedRanker(0.8, 0.2), limit=10 # 先召回更多结果 ) # 第二步:在应用层根据 rating 进行 Boost for hits in res: # 将结果转换为列表,方便排序 results = [] for hit in hits: # Boost 公式:最终分数 = 原始分数 × (1 + rating / 10) rating = hit.entity.get('rating', 0) boosted_score = hit.score * (1 + rating / 10) results.append({ 'id': hit.id, 'original_score': hit.score, 'rating': rating, 'boosted_score': boosted_score }) # 按 boosted_score 重新排序 results.sort(key=lambda x: x['boosted_score'], reverse=True) print("=== Booster 效果(评分加权)===") for i, r in enumerate(results[:3]): print(f"排名 {i+1}: ID={r['id']}, 原始分数={r['original_score']:.4f}, " f"评分={r['rating']}, Boost后分数={r['boosted_score']:.4f}")
在实际应用中,我们经常需要组合使用多种策略。下面是一个完整的示例,展示如何在一个推荐流程中综合运用多种 Reranker。
用户在美食 App 中搜索"川菜",我们需要:
from pymilvus import AnnSearchRequest, WeightedRanker, Function, FunctionType # 用户搜索向量 query_vec = [[1.0, 0.0]] # 步骤1:使用加权排名进行多路召回 req_desc = AnnSearchRequest(data=query_vec, anns_field="desc_vector", param={"metric_type": "IP"}, limit=10) req_pic = AnnSearchRequest(data=query_vec, anns_field="pic_vector", param={"metric_type": "IP"}, limit=10) res_step1 = client.hybrid_search( collection_name="food_collection", reqs=[req_desc, req_pic], ranker=WeightedRanker(0.7, 0.3), # 文字权重 70%,图片权重 30% limit=10 ) print("=== 步骤1:加权排名结果(不考虑距离)===") for hits in res_step1: for i, hit in enumerate(hits[:5]): print(f"排名 {i+1}: ID={hit.id}, 加权分数={hit.score:.4f}, 距离={hit.entity.get('distance')}km") # 步骤2:对结果应用距离衰减(高斯衰减) # 注意:这里我们需要重新搜索,因为 hybrid_search 一次只能用一个 ranker gauss_ranker = Function( name="gauss_distance_decay", function_type=FunctionType.Rerank, params={ "origin": 0.0, "scale": 30.0, # 30km 作为衰减单位 "decay": 0.5, "field_name": "distance" } ) res_step2 = client.hybrid_search( collection_name="food_collection", reqs=[req_desc, req_pic], ranker=gauss_ranker, limit=10 ) print("\n=== 步骤2:距离衰减结果(高斯衰减)===") for hits in res_step2: for i, hit in enumerate(hits[:5]): print(f"排名 {i+1}: ID={hit.id}, 衰减后分数={hit.score:.4f}, " f"距离={hit.entity.get('distance')}km, 评分={hit.entity.get('rating')}") # 步骤3:在应用层应用评分 Boost results = [] for hits in res_step2: for hit in hits: rating = hit.entity.get('rating', 0) distance = hit.entity.get('distance', 0) # 综合分数 = 衰减后分数 × (1 + rating / 10) final_score = hit.score * (1 + rating / 10) results.append({ 'id': hit.id, 'score': hit.score, 'rating': rating, 'distance': distance, 'final_score': final_score }) # 按最终分数排序 results.sort(key=lambda x: x['final_score'], reverse=True) print("\n=== 步骤3:最终排名(应用评分 Boost)===") for i, r in enumerate(results[:5]): print(f"排名 {i+1}: ID={r['id']}, 最终分数={r['final_score']:.4f}, " f"评分={r['rating']}, 距离={r['distance']}km")
通过这个组合策略,我们实现了:
这种多阶段的排名策略在实际推荐系统中非常常见,可以根据业务需求灵活调整。
| 需求 | 推荐方案 | 原因 |
|---|---|---|
| 明确知道各因素权重 | WeightedRanker | 精确控制,效果可预测 |
| 不确定权重配置 | RRFRanker | 无需调参,公平融合 |
| 需要考虑距离/时间 | Decay Ranker | 自动处理数值衰减 |
| 需要商业化排名 | Booster + 其他 | 灵活组合,满足业务需求 |
limit 参数不宜过大,建议 10-100 之间可能原因:
解决方案:
可能原因:
解决方案:
可能原因:
解决方案:
WeightedRanker(加权排名)
RRFRanker(倒数排名融合)
Decay Ranker(衰变排名)
Booster Reranker(提升排名)
恭喜你完成了 Milvus Reranker 的学习!
现在你已经掌握了多种重排策略,可以根据实际业务需求灵活选择和组合使用。记住,没有最好的 Reranker,只有最适合的 Reranker。多实验、多对比,找到最适合你的排名策略!