背景

       基于 LangChain 0.3 集成 Milvus 2.5 向量数据库构建的 NFRA(National Financial Regulatory Administration,国家金融监督管理总局)政策法规智能问答系统。(具体代码版本,可见

       上一次探究了检索前处理方法——查询扩展。介绍的是一种比较有前景的查询优化技术,假设性文档嵌入(Hypothetical Docunment Embeddings, HyDE),该技术源于一篇论文《Precise Zero-Shot Dense Retrieval without Relevance Labels》

       通过使用不同的模型和构建不同的 prompt 来对查询进行扩展来进行检索评估,最终是得到了一个不错的检索召回率,实现了目标(>= 85%)。实现 HyDE 的检索流程图如下:

       本次按计划进行,来到了索引优化。相对于检索前处理(让查询更清晰、多角度、更丰富)来说,索引优化是通过合理地组织、存储文本分块,从而让它们更高效、更准确地被检索到。

目标

       检索召回率 >= 85%

实现方法

       本次探究:使用 Milvus 官方推出的 milvus-model,它是 Python SDK 的一部分。它实现了 BGE-M3 模型的集成,用于在 Milvus 向量数据库中进行高效的密集向量(Dense)、稀疏向量(Sparse)和多向量(Multi-Vector)混合检索(Hybrid Search)。同时,它还实现了重排

       这个对应于 RAG系统整体优化思路图(见下图)的“信息嵌入-嵌入模型”,索引优化部分并未在图中体现,而之所以也包含索引优化,是因为原来索引只有密集向量,此次要实现的混合索引,加上了稀疏向量索引。

       书中还介绍了 LangChain 提供的集成检索器 (EnsembleRetriever),它通过组合不同类型检索器的结果来实现混合检索。它同样也实现了检索后重排。

       而之所以没选择这种实现方法,不是因为无法实现,而是因为要单独实现稀疏检索器比较不合目前项目的实现架构。

       当前项目的向量存储是使用了 milvus,要实现密集向量和稀疏向量的混合检索,就要根据已嵌入到 milvus 中的文本内容,进行单独的稀疏嵌入,实现稀疏嵌入检索器,从而与 milvus检索器一同传给 EnsembleRetriever。

执行过程

BGE-M3

       BGE-M3(Bidirectional Guided Encoder - Multi-Lingual, Multi-Function, Multi-Granularity) 是由智源研究院(BAAI) 推出的优秀的开源文本嵌入模型。

为何叫 M3 呢?是因为它主要有3个特点:

  1. 多语言性(Multi-Linguality):BGE-M3 支持超过 100种语言,具备强大的多语言、跨语言检索能力。
  2. 多功能性(Multi-Functionality):BGE-M3 模型集成密集检索、稀疏检索和多向量检索3种功能,能够灵活应对不同的检索需求。
  3. 多粒度性(Multi-Granularity):可对句子、段落、文档等不同粒度文本进行有效编码,满足不同长度文本的处理需求。

稀疏向量

       稀疏向量是一种特殊的高维向量,其中大部分元素为零,只有少数维度的值不为零。

       稀疏向量是信息检索自然语言处理中捕捉表层术语匹配的重要方法。虽然稠密向量在语义理解方面表现出色,但稀疏向量往往能提供更可预测的匹配结果,尤其是在搜索特殊术语或文本标识符时。

实现过程

1. 加载文档、分块、嵌入、存储

前面有说,要实现混合检索,需重新初始化向量集合,因为嵌入模型要换成:BAAI/bge-m3

代码实现如下:

def load_data_milvus_hybrid():
    # 加载目录文件
    logger.info("Loading data...")
    documents = load_pdf2document(config.FILE_PATH)

    # 分块
    logger.info("Chucking data...")
    docs = FileSplitter(chunk_size=config.FILE_CHUNK_SIZE, chunk_overlap=config.FILE_CHUNK_OVERLAP).split_documents(
        documents)

    # 嵌入
    logger.info("Embeddings...")
    bgem3_ef = BGEM3EmbeddingFunction(use_fp16=config.BGEM3_USE_FP16, device=config.BGEM3_DEVICE)

    texts = [text.page_content for text in docs]
    texts_embeddings = bgem3_ef(texts)
    logger.info(f"向量生成完成,密集向量维度:{bgem3_ef.dim['dense']}")

    # 存储
    logger.info("Vectors and Store Milvus...")

    dense_vector_field = config.DENSE_VECTOR_FIELD
    sparse_vector_field = config.SPARSE_VECTOR_FIELD

    # 定义字段
    fields = [
        FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
        FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=65535),
        FieldSchema(name="metadata", dtype=DataType.JSON),
        FieldSchema(name=dense_vector_field, dtype=DataType.FLOAT_VECTOR, dim=bgem3_ef.dim["dense"]),
        FieldSchema(name=sparse_vector_field, dtype=DataType.SPARSE_FLOAT_VECTOR)
    ]

    # 创建集合模式
    schema = CollectionSchema(fields=fields, description="RAG_NFRA Collection hybrid search support")

    # 创建集合
    milvus_util = MilvusUtils()
    collection = milvus_util.create_collection(collection_name=config.COLLECTION_NAME,
                                               consistency_level=config.CONSISTENCY_LEVEL,
                                               schema=schema)

    # 构建索引
    # 密集向量索引
    dense_index_params = {
        "index_type": config.INDEX_TYPE,
        "metric_type": config.METRIC_TYPE,
        "params": {"nlist": config.NLIST}
    }
    collection.create_index(dense_vector_field, dense_index_params)
    # 稀疏向量索引
    sparse_index_params = {
        "index_type": config.SPARSE_INDEX_TYPE,
        "metric_type": config.SPARSE_METRIC_TYPE
    }
    collection.create_index(sparse_vector_field, sparse_index_params)

    # 插入数据
    data = [
        texts,  # text 字段
        [text.metadata for text in docs],  # metadata 字段
        texts_embeddings["dense"],  # dense_vector 字段
        texts_embeddings["sparse"]  # sparse_vector 字段,用于稀疏检索
    ]
    collection.insert(data)

    # 加载集合到内存
    collection.load()

    logger.info(
        f"成功将 {len(texts_embeddings["dense"])} 条向量存储到 Milvus 集合 {config.COLLECTION_NAME} 并支持混合检索。")

相对于原来只使用密集检索的向量集合初始化过程,主要的变化点:

  • 通过使用 BGEM3EmbeddingFunction 来间接使用嵌入模型:BAAI/bge-m3,use_fp16=False, device='cpu', 这两个是配套使用的参数,不使用半精度加速,使用 CPU计算
  • 字段定义新增:用于存储稀疏向量的字段,字段类型为:SPARSE_FLOAT_VECTOR
  • 创建集合时,声明事务一致性级别为:Strong(强一致性);
  • 给新增的稀疏向量字段,建立索引,索引类型为:SPARSE_INVERTED_INDEX

2. 混合检索

def milvus_hybrid_retrieve(collection: Collection, bgem3_ef: BGEM3EmbeddingFunction, query: str, k: int = config.TOP_K,
                           rerank_method: str = config.RERANK_METHOD_RRF):
    """在Milvus中执行稠密和稀疏向量混合检索,并对结果进行重排。
    :param collection: Milvus集合对象,用于执行搜索操作。
    :param bgem3_ef: 用于生成查询文本的稠密和稀疏向量表示。
    :param query: 用户输入的查询文本。
    :param k: 返回的最相似结果数量,默认值由配置项 config TOP_K 指定。
    :param rerank_method: 指定结果重排方法,支持加权合并或RRF(Reciprocal Rank Fusion),
                                         默认值由配置项 config RERANK_METHOD_RRF 指定。
    :return: list: 检索并重排后的结果 Hits 对象列表
    """
    dense_params = {
        "metric_type": config.METRIC_TYPE,
        "params": {"nprobe": config.NPROBE}
    }
    sparse_params = {
        "metric_type": config.SPARSE_METRIC_TYPE,
        "params": {}
    }

    query_embeddings = bgem3_ef([query])

    dense_req = AnnSearchRequest(
        data=[
            query_embeddings["dense"][0]
        ],
        anns_field=config.DENSE_VECTOR_FIELD,
        param=dense_params,
        limit=2 * k
    )
    sparse_req = AnnSearchRequest(
        data=[
            query_embeddings["sparse"]._getrow(0)
        ],
        anns_field=config.SPARSE_VECTOR_FIELD,
        param=sparse_params,
        limit=4 * k
    )

    # 根据选择的重排方法创建不同的重排器
    if rerank_method == config.RERANK_METHOD_WEIGHTED:
        sparse_weights = config.SPARSE_WEIGHTED
        dense_weights = config.SPARSE_WEIGHTED
        rerank = WeightedRanker(dense_weights, sparse_weights)
    else:  # rrf
        rrf_k = config.RRF_K
        rerank = RRFRanker(rrf_k)

    # 执行混合搜索并使用指定的重排器对结果进行重排序
    results = collection.hybrid_search(
        reqs=[dense_req, sparse_req],
        rerank=rerank,
        limit=k,
        output_fields=["id", "text", "metadata"],
    )[0]

    return results

检索评估(召回率)

 RAG 相关处理说明:

切分策略:分块大小: 500; 分块重叠大小: 100; 使用正则表达式,[r"第\S*条 "]
嵌入模型:模型名称: BAAI/bge-m3 (使用归一化; 维度:1024)
向量存储:向量索引类型:IVF_FLAT (倒排文件索引+精确搜索)

稀疏向量存储: 索引类型:SPARSE_INVERTED_INDEX
密集向量度量标准类型:IP(内积); 聚类数目: 100;查询时聚类数目: 10

稀疏向量度量标准类型:IP(内积);存储数据库: Milvus
向量检索:混合检索(具体混合检索策略如下各章节), 混合检索最终返回向量数目: 3

检索评估数据集:evaluation/data/retrieveInputData_V1_2.xlsx

重排策略:RRF(k=60)

(混合检索最终返回向量数目: 3)

评估组别

密集检索返回数

稀疏检索返回数

召回率

1

3

3

95%

2

3

6

93%

3

3

9

94%

4

5

5

94%

5

5

10

95%

6

6

6

94%

7

6

9

96%

8

6

10

96%

9

6

12

95%

10

9

9

95%

从上表可得:

  1. 最佳表现:评估组别7(6+9)和8(6+10)均达到最高召回率:96%
  2. 参数规律:稀疏检索扩展至 9-10 时效果明显;密集检索在 5-6 区间表现稳定
  3. 稳定性:所有配置召回率差异<3%(93%-96%),系统鲁棒性强

重排策略:权重

(混合检索最终返回向量数目: 3)

评估组别

密集检索返回数

稀疏检索返回数

权重配置(密集:稀疏)

召回率

1

3

3

0.5 : 0.5

95%

2

3

6

0.5 : 0.5

95%

3

3

9

0.5 : 0.5

95%

4

6

6

0.5 : 0.5

94%

5

6

9

0.5 : 0.5

95%

6

6

9

0.7 : 0.3

95%

7

6

9

0.9 : 0.6

95%

8

6

9

0.7 : 0.5

95%

9

6

9

1.0 : 0.5

95%

10

6

9

1.0 : 0.7

95%

11

6

9

1.0 : 1.0

95%

12

6

12

1.0 : 0.7

95%

13

6

12

1.0 : 1.0

95%

14

9

12

1.0 : 1.0

94%

15

9

12

0.7 : 1.0

94%

从上表可得:

  1. 稳定性表现:所有配置召回率差异仅 1个 百分点(94%-95%),系统表现高度稳定
  2. 参数不敏感现象:权重配置变化(0.3-1.0)未影响召回结果;稀疏检索扩展至 12 仍保持 95%的召回率。

检索评估小结

  1. 根据上述不同的重排策略评估结果可知,检索召回率在 93%-96%,是超过目标 85%的;
  2. 在相同的 RAG 处理,及密集与稀疏检索返回数相同的情况下,重排选择:RRF(k=60),获得到了最高召回率:96%

总结

  1. 从实践中体会到了 Milvus 结合 BGE-M3 实现混合检索的高召回率,可见 BGE-M3 作为一款嵌入模型的强大;
  2. Milvus 中集合(collection)提供的 hybrid_search 灵活可配置,不仅支持 RRF重排,还支持权重重排,通过调整密集、稀疏权重,适应不同任务;
  3. 基于 RAG 实现的智能问答系统,适合选择混合检索,它不仅考虑了语义相关性,还考虑到关键词匹配,而且检索召回率高,能更好地应对用户各种各样的问题。


文中基于的项目代码地址:https://gitee.com/qiuyf180712/rag_nfra

本文关联项目的文章:RAG项目实战:LangChain 0.3集成 Milvus 2.5向量数据库,构建大模型智能应用-CSDN博客


 项目同样上传到 github 了,地址:https://github.com/qiugongisme/rag_nfra

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐