发布于 2026-06-22
本文对比了稠密向量与稀疏向量的几何特征,并结合 PyMilvus SDK 提供了双路混合检索(加权分数融合与倒数排名融合 RRF 算法)从特征生成、多特征建表索引到单路与多路检索调用的开发实战。
在 AI 与 NLP 领域,一段文本经过嵌入模型(Embedding Model)处理后,会被映射为一个包含数百乃至上千维度的浮点数组。这个数组在高维空间中构成一条有方向、有长度的"箭头":
通过对比这些高维向量的夹角或几何距离,机器便能量化文本之间的语义相似度。
关键特性:值域有界。 余弦相似度的值域被限制在 [-1, 1] 之间,值越接近 1 表示向量方向越一致(语义越相近)。相比于内积等值域无界的度量,余弦分数具备明确的几何含义,在同一模型、同一任务、同一语料分布下,可以通过标定获得相对稳定的业务阈值。但需注意:不同模型对同一组文本的余弦相似度打分区间存在系统性差异,阈值在切换模型、更换业务场景后必须重新标定。
0,只有文本中实际出现的词对应维度为非零值。关键特性:值域无界。 内积得分等于匹配词权重的累加,其上限完全取决于分词器设计和文本长度,可能是 [0, 1],也可能是 [0, 50] 甚至更高。不同模型、不同分词器产出的稀疏分数量级可能天差地别,无法直接跨模型比较。
L2 归一化(L2 Normalization,又称欧几里得范数归一化)是向量空间中一种常见的缩放操作。
1.0。在几何空间中,这意味着把所有长短不一的向量都等比例缩放,使其终点刚好落在半径为 1 的超球面上。这样就只保留了向量的方向(语义关系),而抹平了因为长度或信息强度差异带来的影响。稠密向量的 L2 归一化优化:
1.0)。这可以将开销较大的余弦相似度计算等价转化为高效的内积(点积)计算,从而大幅提升检索吞吐。text-embedding-v4)等托管嵌入服务返回的 dense embedding 经实测接近单位向量,可直接用于 Cosine/IP 检索。对于 BM25 及其变体等传统稀疏检索,不建议额外进行 L2 归一化,因为会改变原有的长度归一化机制和权重分布。如果强行对这类稀疏特征做 L2 归一化,会产生长文本稀释效应:
因此,此类稀疏匹配通常采用非归一化的点积累加,其分值上限在数学上不可控。(注:部分神经稀疏模型如 SPLADE 在数学上可以做归一化,此处主要指 BM25 类的传统稀疏表示)
在 RAG 检索系统中,我们需要为文档数据同时生成稠密特征(Dense)与稀疏特征(Sparse),并利用 PyMilvus SDK 的原生接口定义支持混合检索的多列索引集合。
我们可以利用 DashScope 的 text-embedding-v4 模型同时生成两路特征。以下是调用 SDK 获取特征并转换为键值映射字典的 Python 示例:
import dashscope
def generate_hybrid_vectors(text: str, api_key: str) -> tuple[list[float], dict[int, float]]:
"""
生成输入文本的稠密向量与稀疏向量特征
"""
response = dashscope.TextEmbedding.call(
model="text-embedding-v4",
input=[text],
output_type="dense&sparse", # 同时获取两种表征
api_key=api_key
)
emb_data = response.output["embeddings"][0]
# 稠密向量为 1024 维浮点数列表
dense_vector = emb_data["embedding"]
# 将稀疏结果映射为 {token_id: weight} 的稀疏字典形式
raw_sparse = emb_data["sparse_embedding"]
sparse_vector = {item["index"]: item["value"] for item in raw_sparse}
return dense_vector, sparse_vector
有了特征向量后,我们使用 MilvusClient 的原生 API 创建带有双路特征字段的 Schema,分别为其建立专属索引,并完成数据写入。
from pymilvus import MilvusClient, DataType
# 1. 初始化客户端(支持本地 SQLite 数据库或远程连接)
milvus_client = MilvusClient(uri="./milvus_test.db")
collection_name = "travel_hybrid_docs"
# 模拟已生成的稠密与稀疏特征向量(实际应用中由上述 generate_hybrid_vectors 生成)
dense_vector = [0.1] * 1024
sparse_vector = {101: 0.9, 202: 0.7}
# 2. 定义支持多特征检索的集合结构 (Schema)
schema = milvus_client.create_schema(auto_id=True, enable_dynamic_field=True)
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True)
schema.add_field(field_name="text_content", datatype=DataType.VARCHAR, max_length=65535)
schema.add_field(field_name="dense_vector", datatype=DataType.FLOAT_VECTOR, dim=1024)
schema.add_field(field_name="sparse_vector", datatype=DataType.SPARSE_FLOAT_VECTOR)
# 3. 分别为稠密向量和稀疏向量字段配置索引参数
index_params = milvus_client.prepare_index_params()
# 稠密索引使用 COSINE 余弦度量
index_params.add_index(
field_name="dense_vector",
index_name="dense_index",
metric_type="COSINE",
index_type="FLAT"
)
# 稀疏索引使用 IP 内积度量
index_params.add_index(
field_name="sparse_vector",
index_name="sparse_index",
metric_type="IP",
index_type="SPARSE_INVERTED_INDEX"
)
# 4. 创建集合 (Collection)
if not milvus_client.has_collection(collection_name):
milvus_client.create_collection(
collection_name=collection_name,
schema=schema,
index_params=index_params
)
# 5. 混合写入稠密与稀疏特征向量
data_to_insert = [{
"text_content": "北京故宫门票必须提前7天在线预约,入园时请务必刷身份证凭证。",
"dense_vector": dense_vector, # 1024 维稠密特征向量
"sparse_vector": sparse_vector # 稀疏特征字典 {token_id: weight}
}]
milvus_client.insert(
collection_name=collection_name,
data=data_to_insert
)
混合检索的核心是并发执行稠密向量与稀疏向量检索,并将各自召回的文档集合融合成单一的排序队列,以达到在单次检索中平衡语义泛化与精确匹配的目的。
https://milvus.io/docs/zh/weighted-ranker.md
加权分数融合通过为不同的检索通道分配不同的权重比例,将各通道得到的相似度得分进行线性加权求和,以此计算出最终的融合得分并进行重排。在执行时,系统通常先对两路分数进行归一化映射以消除量纲差异,然后再进行权重的乘积与累加。在 Milvus 中,我们通过原生构建两路子请求,并指定 WeightedRanker 重排器来实现这一融合策略。
from pymilvus import MilvusClient, AnnSearchRequest, WeightedRanker
milvus_client = MilvusClient(uri="./milvus_test.db")
# 模拟查询所对应的稠密与稀疏特征向量
query_dense = [0.1] * 1024
query_sparse = {101: 0.85, 202: 0.65}
# 1. 构造稠密与稀疏两路检索子请求
req_dense = AnnSearchRequest(
data=[query_dense], # 稠密查询特征
anns_field="dense_vector",
param={"metric_type": "COSINE"}, # 稠密检索使用余弦相似度
limit=5
)
req_sparse = AnnSearchRequest(
data=[query_sparse], # 稀疏查询特征
anns_field="sparse_vector",
param={"metric_type": "IP"}, # 稀疏检索使用内积相似度
limit=5
)
# 2. 调用 hybrid_search 执行两路检索融合,并使用 WeightedRanker 进行加权融合重排
results = milvus_client.hybrid_search(
collection_name="travel_hybrid_docs",
reqs=[req_dense, req_sparse],
ranker=WeightedRanker(0.7, 0.3, norm_score=True), # 分别指定稠密(0.7)和稀疏(0.3)的权重
limit=5,
output_fields=["text_content"]
)
https://milvus.io/docs/rrf-ranker.md
如果需要规避各检索通道分数体系和量纲差异的影响,可以使用倒数排名融合(Reciprocal Rank Fusion, RRF)算法。RRF 不直接依赖原始的分数,而只关注文档在各检索通道中的排名位次。它通过累计各通道排名的倒数得分计算最终的重排得分,能够规避不同通道的分值量纲差异,算法表现最为稳健。
from pymilvus import MilvusClient, AnnSearchRequest, RRFRanker
milvus_client = MilvusClient(uri="./milvus_test.db")
# 模拟查询所对应的稠密与稀疏特征向量
query_dense = [0.1] * 1024
query_sparse = {101: 0.85, 202: 0.65}
# 1. 构造稠密与稀疏两路检索子请求
req_dense = AnnSearchRequest(
data=[query_dense], # 稠密查询特征
anns_field="dense_vector",
param={"metric_type": "COSINE"}, # 稠密检索使用余弦相似度
limit=5
)
req_sparse = AnnSearchRequest(
data=[query_sparse], # 稀疏查询特征
anns_field="sparse_vector",
param={"metric_type": "IP"}, # 稀疏检索使用内积相似度
limit=5
)
# 2. 调用 hybrid_search 执行两路检索融合,并使用 RRFRanker 进行无分数排名融合
results = milvus_client.hybrid_search(
collection_name="travel_hybrid_docs",
reqs=[req_dense, req_sparse],
ranker=RRFRanker(k=60), # 使用 RRF 融合,k 为平滑常数(默认推荐值为 60)
limit=5,
output_fields=["text_content"]
)
norm_score=True)抹平物理量纲,能在融合中保留局部的相对分数梯度,但对跨查询(Cross-Query)的阈值卡点依然比较敏感,常作为第二阶段重排前的数据初筛手段。