【混合架构02】稠密向量+稀疏检索+图关系混合架构协议层:Milvus 2.5混合检索引擎深度实现
稠密向量稀疏检索图关系混合架构协议层Milvus 2.5/2.6 混合检索引擎深度实现本文是系列第2篇聚焦 Milvus 作为混合检索引擎的协议层实现。从 Collection Schema 设计到双向量字段、从 BM25 全文检索到混合查询语法手把手带你搭建一套可落地的三模态检索管道。一、为什么选 Milvus 做混合检索引擎2024 年以前要实现稠密稀疏混合检索通常需要维护两套系统Milvus 做向量检索Elasticsearch 做 BM25 关键词检索再用 Python 脚本合并两路结果。这套方案能用但运维复杂度翻倍延迟也难控制。Milvus 2.5 做了一个关键决策——把全文检索引擎 Tantivy 直接内置到存储层并在 2.6 进一步增强了 Sparse-BM25 索引和冷热分层存储。这意味着你现在可以用一个 Collection 同时承载稠密向量、稀疏向量、标量过滤和全文检索一条查询搞定全部。Milvus 2.5/2.6 混合检索核心能力能力版本说明原生全文检索2.5集成 Tantivy支持 BM25 算法稀疏向量类型2.4SPARSE_FLOAT_VECTOR 类型原生支持Sparse-BM25 索引2.6检索速度比 ES 快 3~7 倍索引体积压缩至 1/3双向量字段2.4同一 Collection 支持多种向量类型WeightedRanker2.4多路检索结果的加权融合冷热分层存储2.6热数据 SSD 冷数据对象存储成本减半RaBitQ 1bit 量化2.6内存占用降至 1/32QPS 提升 4 倍动态字段2.6运行时添加新字段无需停机二、混合检索 Collection Schema 设计Schema 设计是混合检索的地基。核心思想是一个 Document 同时携带稠密向量、稀疏向量和元数据。2.1 完整 Schema 定义frompymilvusimport(MilvusClient,DataType,CollectionSchema,FieldSchema,AnnSearchRequest,WeightedRanker,RRFRanker,SparseVectorSearchParam)# 字段定义 # 1. 主键doc_idFieldSchema(namedoc_id,dtypeDataType.VARCHAR,is_primaryTrue,max_length64)# 2. 文本内容全文检索目标textFieldSchema(nametext,dtypeDataType.VARCHAR,max_length8192)# 3. 稠密向量语义检索dense_vectorFieldSchema(namedense_vector,dtypeDataType.FLOAT_VECTOR,dim1024# BGE-M3 输出1024维)# 4. 稀疏向量BM25 关键词检索sparse_vectorFieldSchema(namesparse_vector,dtypeDataType.SPARSE_FLOAT_VECTOR)# 5. 标量元数据过滤 排序categoryFieldSchema(namecategory,dtypeDataType.VARCHAR,max_length64)publish_dateFieldSchema(namepublish_date,dtypeDataType.INT64)# Unix时间戳sourceFieldSchema(namesource,dtypeDataType.VARCHAR,max_length128)# Schema 组装 schemaCollectionSchema(fields[doc_id,text,dense_vector,sparse_vector,category,publish_date,source],description混合检索知识库 - 稠密稀疏标量)2.2 关键设计决策为什么稀疏向量不用全文检索函数而用 SPARSE_FLOAT_VECTOR 类型Milvus 2.5 提供了两种 BM25 能力方式版本优势劣势全文检索函数Function2.5自动分词BM25零代码不可控无法自定义稀疏向量SPARSE_FLOAT_VECTOR2.4可配合 BGE-M3 稀疏输出灵活可控需要手动生成稀疏向量生产环境推荐方案如果用 BGE-M3 作为 Embedding 模型 → 用 SPARSE_FLOAT_VECTOR 类型充分利用 BGE-M3 同时输出稠密和稀疏向量的能力如果不想引入额外模型 → 用全文检索函数让 Milvus 自动处理分词和 BM25本文采用 BGE-M3 方案一个模型搞定稠密和稀疏两种向量。三、数据写入管道3.1 BGE-M3 双模态向量化BGE-M3 是 BAAI 发布的多功能 Embedding 模型一次前向传播同时产出稠密向量和稀疏向量fromFlagEmbeddingimportFlagModel modelFlagModel(BAAI/bge-m3,use_fp16True)defembed_text(text:str)-dict: BGE-M3 双模态向量化 返回: { dense: [0.023, -0.451, ...], # 1024维浮点数组 sparse: {3: 0.82, 107: 0.45, ...} # 稀疏字典 {token_id: weight} } embeddingsmodel.encode([text],batch_size1,return_denseTrue,return_sparseTrue,return_colbert_vecsFalse# ColBERT暂不用)return{dense:embeddings[dense_vecs][0].tolist(),sparse:dict(zip(embeddings[sparse_vecs][0].indices.tolist(),embeddings[sparse_vecs][0].values.tolist()))}3.2 批量写入importtimefrompymilvusimportCollectiondefbatch_insert(collection:Collection,documents:list,batch_size:256): 批量写入文档到混合检索Collection documents: [{doc_id: str, text: str, category: str, source: str}, ...] foriinrange(0,len(documents),batch_size):batchdocuments[i:ibatch_size]# 批量向量化texts[d[text]fordinbatch]embeddingsmodel.encode(texts,batch_sizebatch_size,return_denseTrue,return_sparseTrue,return_colbert_vecsFalse)# 构造数据data[]forj,docinenumerate(batch):data.append({doc_id:doc[doc_id],text:doc[text],dense_vector:embeddings[dense_vecs][j].tolist(),sparse_vector:dict(zip(embeddings[sparse_vecs][j].indices.tolist(),embeddings[sparse_vecs][j].values.tolist())),category:doc.get(category,),publish_date:int(time.time()),source:doc.get(source,)})collection.insert(data)print(f已写入{min(ibatch_size,len(documents))}/{len(documents)})collection.flush()四、索引构建混合检索的索引策略需要兼顾精度和性能# 稠密向量索引 dense_index_params{index_type:IVF_FLAT,# 百万级以下优先metric_type:COSINE,# 余弦相似度params:{nlist:1024}# 聚类中心数}# 千万级以上改用 IVF_PQ 或 HNSW# dense_index_params {index_type: HNSW, metric_type: COSINE, params: {M: 16, efConstruction: 256}}# 稀疏向量索引Milvus 2.6 新增sparse_index_params{index_type:SPARSE_WAND,# 或 SPARSE_INVERTED_INDEXmetric_type:BM25# Milvus 2.6 原生BM25索引}# 创建索引 collection.create_index(field_namedense_vector,index_paramsdense_index_params)collection.create_index(field_namesparse_vector,index_paramssparse_index_params)# 标量字段索引加速过滤查询collection.create_index(field_namecategory,index_params{index_type:Trie})collection.create_index(field_namepublish_date,index_params{index_type:STL_SORT})索引选型参考数据规模稠密索引稀疏索引说明 100万IVF_FLAT (nlist1024)SPARSE_WAND精度优先100万~1000万IVF_PQ (m64, nbits8)SPARSE_BM25性能与精度平衡 1000万HNSW (M16) RaBitQSPARSE_BM25大规模场景Milvus 2.6五、混合查询语法这是整个方案的核心——如何在一次查询中同时利用稠密向量和稀疏向量。5.1 基础混合查询collection.load()# 查询文本向量化query_text昇腾910B的FP16算力是多少query_embmodel.encode([query_text],return_denseTrue,return_sparseTrue,return_colbert_vecsFalse)# 稠密向量检索请求 dense_reqAnnSearchRequest(data[query_emb[dense_vecs][0].tolist()],anns_fielddense_vector,param{metric_type:COSINE,params:{nprobe:64}},limit20,# Top-K×2留足融合空间expr# 可加标量过滤条件)# 稀疏向量检索请求 sparse_reqAnnSearchRequest(data[dict(zip(query_emb[sparse_vecs][0].indices.tolist(),query_emb[sparse_vecs][0].values.tolist()))],anns_fieldsparse_vector,param{metric_type:BM25},limit20)# 多路融合 # WeightedRanker按权重融合两路结果的分数resultscollection.hybrid_search(reqs[dense_req,sparse_req],rankerWeightedRanker(0.5,0.5),# 稠密权重0.5 : 稀疏权重0.5limit10,# 最终返回Top-10output_fields[doc_id,text,category,source])# 遍历结果forhitinresults[0]:print(f[{hit.score:.4f}]{hit.entity.get(text)[:100]})5.2 加标量过滤的混合查询生产环境中标量过滤是控制检索范围的关键手段# 只搜索特定分类 最近30天的文档importtime thirty_days_agoint(time.time())-30*86400dense_reqAnnSearchRequest(data[query_emb[dense_vecs][0].tolist()],anns_fielddense_vector,param{metric_type:COSINE,params:{nprobe:64}},limit20,exprfcategory 硬件规格 publish_date {thirty_days_ago}# 标量预过滤)sparse_reqAnnSearchRequest(data[dict(zip(query_emb[sparse_vecs][0].indices.tolist(),query_emb[sparse_vecs][0].values.tolist()))],anns_fieldsparse_vector,param{metric_type:BM25},limit20,exprfcategory 硬件规格 publish_date {thirty_days_ago})resultscollection.hybrid_search(reqs[dense_req,sparse_req],rankerWeightedRanker(0.5,0.5),limit10,output_fields[doc_id,text,category,source,publish_date])5.3 RRF 融合不依赖分数绝对值的鲁棒方案当两路检索的分数尺度差异大时比如稠密余弦 0~1稀疏 BM25 0~30WeightedRanker 需要反复调参。更稳健的选择是 RRFReciprocal Rank FusionfrompymilvusimportRRFRanker# RRF只看排名不看分数天然适应不同尺度的分数resultscollection.hybrid_search(reqs[dense_req,sparse_req],rankerRRFRanker(),# 默认 k60limit10,output_fields[doc_id,text,category,source])何时用 WeightedRanker vs RRFRankerWeightedRanker需要精确控制语义/关键词的权重比例比如医疗场景关键词优先RRFRanker不想调参、两路分数尺度差异大、快速验证方案六、Milvus 2.6 新特性在生产中的应用6.1 冷热分层存储千万级文档的知识库存储成本是不可回避的问题。Milvus 2.6 的冷热分层存储让热数据留在 SSD冷数据自动下沉到对象存储S3/OSS/MinIO写入数据 ──→ Collection ──→ 热数据SSD │ 数据老化30天 │ ▼ 冷数据对象存储 S3/OSS │ 查询命中冷数据 │ ▼ 延迟上升50ms → 200ms 但存储成本降低 60%配置方式Milvus 2.6# milvus.yamldataCoord:segment:# 自动将超过30天未访问的segment下沉到对象存储tieredStorage:enabled:truehotTier:ssdcoldTier:s3policy:maxAge:720h# 30天minSegmentSize:512# 小segment不迁移6.2 RaBitQ 1bit 量化当稠密向量从千万级走向亿级内存会成为第一个瓶颈。Milvus 2.6 引入的 RaBitQ 将稠密向量量化到 1bit效果惊人优化方案内存占用1亿向量×1024维QPS召回率未量化FP32~400 GB基准100%HNSW SQ8~100 GB2×98%RaBitQ 1bit~12.5 GB4×95%# 启用RaBitQ量化索引dense_index_params{index_type:RaBitQ,# 1bit量化metric_type:COSINE,params:{nbits:1,# 量化位数reserve_memory:0.28# SQ8精排内存占比1bit主索引SQ8精排}}6.3 短语匹配与分词增强Milvus 2.6 的全文检索新增了两个实用功能# 短语匹配词序敏感# 查询 昇腾910B芯片 只匹配完整短语不匹配 昇腾芯片910Bdense_reqAnnSearchRequest(data[query_emb[dense_vecs][0].tolist()],anns_fielddense_vector,param{metric_type:COSINE,params:{nprobe:64,drop_ratio_search:0.2# 低分结果预丢弃提升速度}},limit20)七、性能基准测试以下是 Milvus 2.5/2.6 在混合检索场景下的基准数据基于 100 万条中文文档BGE-M3 1024 维检索模式QPS (并发32)P95延迟召回率10内存占用纯稠密IVF_FLAT85035ms82%4.2 GB纯稀疏BM2522008ms71%1.8 GB混合Weighted 0.5:0.568052ms94%6.0 GB混合 RaBitQ120048ms91%2.1 GB混合 冷热分层68055ms(热) / 180ms(冷)94%2.4 GB(热SSD)关键结论混合检索的召回率比任何单一模态提升 10~20 个百分点延迟增加约 15ms对大多数业务场景可接受RaBitQ 混合检索 大规模场景的最优性价比组合八、常见问题排查问题根因解决方案稀疏向量写入报错 “sparse vector format error”BGE-M3 稀疏输出格式不匹配确保转为{index: weight}字典格式混合查询结果为空稀疏/稠密索引未同时创建检查两个向量字段都已创建索引BM25 召回率过低分词器不适合中文Milvus 2.6 配置 Jieba 分词器内存不足 OOM稠密向量全部加载到内存启用 RaBitQ 量化或冷热分层WeightedRanker 结果偏向稠密权重比例不合理BM25 分数量级通常高于余弦建议稀疏权重从 0.3 起调九、总结Milvus 2.5/2.6 将混合检索从两个系统拼凑升级为原生一体化关键价值点一个 Collection 承载全量检索稠密向量、稀疏向量、标量过滤、全文检索共存BGE-M3 一站式向量化一次前向传播产出稠密稀疏两种向量Milvus 2.6 降本利器RaBitQ 量化降低 90% 内存冷热分层降低 60% 存储混合查询 API 简洁hybrid_searchWeightedRanker/RRFRanker四行代码搞定下一篇文章将深入 RRF 融合算法和多路召回策略以及交叉编码器重排序的工程实现。