RAG实战 - 向量数据库(Milvus)

RAG实战-向量数据库(Milvus)

    • [一、Milvus 基础概念](#一、Milvus 基础概念)
      • [1.1 什么是 Milvus](#1.1 什么是 Milvus)
      • [1.2 数据模型与层级结构](#1.2 数据模型与层级结构)
      • [1.3 向量相似度度量与 ANN](#1.3 向量相似度度量与 ANN)
      • [1.4 索引](#1.4 索引)
      • [1.5 正排索引与倒排索引](#1.5 正排索引与倒排索引)
      • [1.6 查询 API 区分](#1.6 查询 API 区分)
      • [1.7 Milvus 数据更新机制](#1.7 Milvus 数据更新机制)
      • [1.8 RAG 父子分块方案(业务常用)](#1.8 RAG 父子分块方案(业务常用))
    • [二、Milvus 基础代码实战](#二、Milvus 基础代码实战)
      • [2.1 安装与环境](#2.1 安装与环境)
      • [2.2 连接与创建数据库](#2.2 连接与创建数据库)
      • [2.3 创建 Collection 与 Schema](#2.3 创建 Collection 与 Schema)
      • [2.4 创建索引](#2.4 创建索引)
      • [2.5 插入数据](#2.5 插入数据)
      • [2.6 查询 Query(标量查询)](#2.6 查询 Query(标量查询))
      • [2.7 向量相似度检索 search(单路)](#2.7 向量相似度检索 search(单路))
      • [2.8 混合检索 hybrid_search](#2.8 混合检索 hybrid_search)
      • [2.9 更新与删除](#2.9 更新与删除)
      • [2.10 分区加载与释放](#2.10 分区加载与释放)
      • [2.11 多查询向量搜索](#2.11 多查询向量搜索)
      • [2.12 向量归一化](#2.12 向量归一化)
    • [三、完整 RAG 父子分块案例(milvus_3_case_2_WeightRank_RRFRank.py)](#三、完整 RAG 父子分块案例(milvus_3_case_2_WeightRank_RRFRank.py))
      • [3.1 流程概述](#3.1 流程概述)
      • [3.2 关键代码与函数](#3.2 关键代码与函数)
      • [3.3 父子分块 Milvus + MySQL 简易表结构](#3.3 父子分块 Milvus + MySQL 简易表结构)
    • 四、不同代码方法对比总结

一、Milvus 基础概念

1.1 什么是 Milvus

Milvus 是一款开源的向量数据库(2019 年提出),唯一目标是存储、索引和管理由深度神经网络和其他机器学习模型生成的大规模嵌入向量。它能处理万亿级别的向量索引。

与关系型数据库主要处理结构化数据不同,Milvus 从底层设计用于处理从非结构化数据转换而来的嵌入向量。非结构化数据包括电子邮件、论文、物联网传感器数据、照片、蛋白质结构等。使用嵌入技术将它们转换为向量,Milvus 存储和索引这些向量,通过计算相似距离来分析两个向量之间的相关性。如果两个嵌入向量非常相似,则意味着原始数据源也很相似。

为什么选择 Milvus:

  • 在处理大规模数据集的向量搜索时具有高性能(依靠索引)。
  • 开发者社区活跃,提供多语言支持和工具链。
  • 云扩展性和高可靠性,即使出现故障也不会受到影响。
  • 可进行混合搜索。

1.2 数据模型与层级结构

Milvus 的数据组织结构非常有层次性,类似于传统数据库的"库-表-行-列"。

复制代码
Milvus实例 → Database数据库 → Collection集合(类似MySQL表) → Partition分区(同集合内逻辑分组,类似MySQL表分区) → Entity行记录 → Field字段
  • Database 数据库 :Milvus 自带默认库 default,不手动创建库则全部操作在 default 库。一个 Milvus 集群最多支持 64 个数据库。作用:业务隔离、权限管控;不同数据库集合完全隔离。
  • Collection 集合:相当于 MySQL 的表,一套固定 Schema,字段类型固定。1 个 collection 最多支持 4 个向量 Field。
  • Partition 分区 :集合内部逻辑分组,同一个集合所有分区共用一套 Schema。每个集合自带默认分区 _default,插入不指定分区,数据写入 _default。用途:按时间/业务类型分组;检索指定分区,只加载对应分区索引,减少检索开销。
  • Entity 实体:类似于 SQL 中的行,一个实体代表一个被向量化的对象,由主键字段、一个或多个向量字段以及其他标量字段组成。
  • Field 字段 :类似于 SQL 中的列。
    • 主键字段:唯一标识一个实体。
    • 向量字段:存储向量数据,如 FLOAT_VECTOR(稠密向量)或 SPARSE_FLOAT_VECTOR(稀疏向量)。
    • 标量字段:存储元数据,如 INT32、VARCHAR、BOOL、FLOAT 等,可以对这些字段进行过滤查询。

分区与索引的比喻:

  • 分区:图书馆有不同的楼层,比如一楼存放"2000年以前的图书",二楼存放"2000年及以后的图书"。查询时可以指定只去二楼寻找,忽略一楼,粗粒度过滤,减少搜索范围。
  • 索引:在"二楼"这个楼层内,图书被整理并放置在不同的书架上,例如"天文学"、"物理学"、"化学"等。这个内部的整理和编目系统就是索引。寻找"太空探索"的书时,索引会直接指向"天文学"书架。

注意区分:

  • Partition(逻辑分区,业务控制)≠ Shard(物理分片,底层自动做扩容,上层无感知)。

1.3 向量相似度度量与 ANN

向量相似度的核心是计算两个向量在多维空间中的"距离"或"夹角"。这是在海量向量中找到与查询目标最相似项的基础。

假设有两个 3 维向量:

  • 向量 A = 1, 2, 3
  • 向量 B = 4, 2, 1
欧氏距离 (Euclidean Distance, L2)

衡量空间中两点之间的"直线距离"。值越小,越相似。

公式:

D(A, B) = \\sqrt{\\sum_{i=1}\^{n}(a_i - b_i)\^2}

计算示例:

① 计算差值平方:

  • (1 - 4)² = (-3)² = 9
  • (2 - 2)² = 0² = 0
  • (3 - 1)² = 2² = 4

② 求和:9 + 0 + 4 = 13

③ 取平方根:√13 ≈ 3.606

适用场景:适用于向量的数值大小本身具有实际意义的场景,如图像识别或物理世界坐标。

内积 (Inner Product, IP)

衡量两个向量的方向是否一致。值越大,越相似。使用 IP 前,通常需要对向量进行归一化 (Normalization),即让向量的模长变为 1。归一化后,IP 的值等价于余弦相似度。

公式:

IP(A, B) = \\sum_{i=1}\^{n}a_i \\cdot b_i

计算示例(演示未归一化,归一化略):

① 对向量 A 和 B 进行归一化:

  • 向量的模长 ||V|| = sqrt(v₁² + v₂² + ... + vₙ²)
  • A 的模长:||A|| = sqrt(1² + 2² + 3²) = sqrt(14) ≈ 3.742
  • A' = 1/3.742, 2/3.742, 3/3.742 ≈ 0.267, 0.534, 0.802
  • B 的模长:||B|| = sqrt(4² + 2² + 1²) = sqrt(21) ≈ 4.583
  • B' = 4/4.583, 2/4.583, 1/4.583 ≈ 0.873, 0.436, 0.218

② 计算归一化后向量 A' 和 B' 的内积:

IP(A', B') = (0.267 * 0.873) + (0.534 * 0.436) + (0.802 * 0.218) ≈ 0.233 + 0.233 + 0.175 = 0.641

这个结果 0.641 就是向量 A 和 B 的余弦相似度,在 -1, 1 区间内,比未归一化的内积 11 更具可解释性。

适用场景:文本语义相似度计算。文本嵌入模型(如 Word2Vec、BERT)通常使用向量的方向来表示语义,此时归一化后的内积(即余弦相似度)是最佳选择。

ANN (近似最近邻) 搜索

当数据库中有数百万、数十亿的向量时,将查询向量与每一个向量都计算一遍(暴力搜索或精确搜索)极其耗时。ANN 算法的核心思想是牺牲极小的精度来换取巨大的性能提升。它不保证找到绝对最相似的邻居,但能以极快的速度(通常是毫秒级)找到一个或多个"足够相似"的邻居。

1.4 索引

索引是一种特殊的数据结构,旨在极大提升数据检索的速度。代价是需要额外的存储空间和在写入数据时稍有增加的维护成本来更新索引。

FLAT:100% 准确的精确搜索(暴力搜索)

  • 定义:精确搜索,不进行任何压缩。100% 准确,但性能最低。
  • 特点:拿着查询向量,与数据库中每一个向量进行一对一的距离计算,然后返回距离最近的 K 个结果。
  • 优点:100% 准确。
  • 缺点:性能极差,随着数据量增长,计算量呈线性增长。
  • 适用场景:数据集很小(例如几万以内)或对准确率有绝对要求的场景。常用作评估其他索引性能和准确率的"黄金标准"。

IVF_FLAT 索引:基于聚类的倒排文件

  • 定义:IVF (Inverted File) 系列索引是 ANN 搜索的典型代表。核心思想是"聚类",预先将所有向量进行分组,搜索时只访问最可能包含结果的几个组,避免全局扫描。
  • 特点:借用倒排索引思想,是一个映射:聚类中心 ID → 该聚类包含的所有向量 ID。
  • 构建阶段(由 nlist 参数控制):使用 k-means 等聚类算法,将所有向量数据聚成 nlist 个簇。每个簇有一个中心点(质心)。建立"倒排文件",记录每个质心(簇)包含了哪些向量。
  • 搜索阶段(由 nprobe 参数控制):
    1. 粗筛:仅将查询向量 Q 与 nlist 个质心进行比较,找到与 Q 最近的 nprobe 个质心。
    2. 精筛:通过"倒排文件",瞬间获取这 nprobe 个簇包含的所有向量,然后仅在这些向量内部进行精确搜索(暴力搜索)。
    3. 从这 nprobe 个簇的搜索结果中,选出最终的 Top-K。
  • 优点:性能和准确率之间一个非常好的平衡选择,最常用、最基础的 ANN 索引之一。

HNSW (Hierarchical Navigable Small World)

  • 思想:基于图的索引。将向量看作图中的节点,通过构建一个"高速公路+普通公路"式的多层网络图,实现极快的路径查找。
  • 特点:召回率(准确率)非常高,性能也很好,是目前最先进的索引之一。缺点是构建索引时速度较慢,且占用内存较大。

SPARSE_INVERTED_INDEX

  • 思想:专门为稀疏向量设计的倒排索引。核心关心哪些维度上有值。
  • 特点:仅适用于稀疏向量检索等。

ANN 索引(如 IVF_FLAT)的工作方式类似于书架系统。预先通过聚类算法将所有向量分成很多个"桶"或"簇"(就像书架)。当一个查询请求进来时,Milvus 会:

  1. 判断查询向量离哪几个"桶"(书架)最近。
  2. 只在这几个被选中的"桶"里进行精确的距离计算。
  3. 忽略掉其他所有"桶",从而极大地缩减计算范围,实现毫秒级搜索。

分区是宏观上的数据划分,索引是微观上的数据组织结构,两者共同协作以实现最高效的查询。

1.5 正排索引与倒排索引

  • 正排存储 :row_id → 整条实体数据。倒排索引只用于召回候选 row_id,不保存原始文本。
  • 正排索引:文档 ID → 文档包含的词。
  • 倒排索引:词 → 文档 id 列表。
  • 记忆口诀:正排:文档查词;倒排:词查文档。
  • 倒排索引存储结构:token_id -> [(row_id, weight), ...]
    • token_id:词编号;row_id:Milvus 内部行 ID(不是业务主键)。
    • 检索流程:拿到匹配 row_id → 去底层正排存储,根据 row_id 读取完整行所有字段。

1.6 查询 API 区分

  • collection.query():标量查询,类似 SQL select,基于 expr 过滤条件,不做向量相似度检索。
  • collection.search():单路向量 ANN 检索,支持稠密向量检索、稀疏向量检索,通过 anns_field 指定向量字段。
  • collection.hybrid_search():混合检索,多路向量检索,分别执行 search,按权重融合分数,同时支持稠密语义+稀疏关键词联合召回。

1.7 Milvus 数据更新机制

Milvus 不支持原地 update。更新操作采用:先 delete 删除旧数据,再 insert 插入新数据。

collection.delete(expr):按 expr 条件删除实体,底层是墓碑标记,不会立刻清理磁盘。

1.8 RAG 父子分块方案(业务常用)

  • 父块:较长文本,保存完整上下文,不生成向量,不存入 Milvus;存储在 MySQL,主键 parent_id。
  • 子块:父块切分出短切片,生成稠密向量存入 Milvus;Milvus 子块记录包含 parent_id 关联父文档。
  • 检索流程:用户问题向量 → Milvus 检索子块向量,命中子块拿到 parent_id → 去 MySQL 批量查询父块原文,交给 LLM。
  • 优势:向量模型支持短文本保证检索质量;给大模型传入长完整上下文,避免断章取义。
  • 存储选型:生产环境父文本外置 MySQL,避免 Milvus 内部父文本大量冗余存储。

二、Milvus 基础代码实战

2.1 安装与环境

bash 复制代码
pip install pymilvus==2.4.1

2.2 连接与创建数据库

方法一:使用 MilvusClient(推荐)

python 复制代码
from pymilvus import MilvusClient

client = MilvusClient(uri="http://82.156.249.211:19530")

# 创建名称为 milvus_demo 的数据库
databases = client.list_databases()
if "milvus_demo" not in databases:
    client.create_database(db_name="milvus_demo")
    print("创建数据库成功")
else:
    client.using_database(db_name="milvus_demo")
    print("数据库已存在")

关键函数:

  • MilvusClient(uri, db_name=None):初始化客户端。uri 为 Milvus 服务地址,db_name 可选,指定连接后默认使用的数据库。
  • client.list_databases():返回当前实例下所有数据库名称列表。
  • client.create_database(db_name):创建新数据库。
  • client.using_database(db_name):切换当前连接使用的数据库。
  • client.drop_database(db_name):删除数据库,删除前必须保证库内所有集合都被删除。

方法二:使用旧版 connections + db 模块

python 复制代码
from pymilvus import connections, db

connections.connect(alias="default", host="localhost", port="19530")
DB_NAME = "vector_demo_db"
if DB_NAME not in db.list_databases():
    db.create_database(DB_NAME)
db.using_database(DB_NAME)

区别:

  • MilvusClient 是 PyMilvus 2.4 推荐的高层 API,更简洁,直接面向客户端对象。
  • connections + db 是旧版 API,需要先建立连接,再操作数据库对象。功能类似,但新版 API 更统一。

2.3 创建 Collection 与 Schema

示例 1:基础集合(无分区)

python 复制代码
from pymilvus import MilvusClient, DataType
import random

client = MilvusClient(uri="http://82.156.249.211:19530")
COLLECTION_NAME = "tech_books"
VECTOR_FIELD_NAME = "book_intro_vector"

if client.has_collection(collection_name=COLLECTION_NAME):
    client.drop_collection(collection_name=COLLECTION_NAME)

schema = MilvusClient.create_schema(
    auto_id=False,
    enable_dynamic_field=True,
)
schema.add_field(field_name="book_id", datatype=DataType.INT64, is_primary=True)
schema.add_field(field_name="book_title", datatype=DataType.VARCHAR, max_length=255)
schema.add_field(field_name=VECTOR_FIELD_NAME, datatype=DataType.FLOAT_VECTOR, dim=768)

client.create_collection(
    collection_name=COLLECTION_NAME,
    schema=schema,
    consistency_level="Strong"
)

关键函数与参数:

  • MilvusClient.create_schema(auto_id=False, enable_dynamic_field=True):创建 Schema 对象。
    • auto_id:是否自动生成主键 ID。False 表示手动指定。
    • enable_dynamic_field:是否启用动态字段。启用后可以插入 Schema 中未定义的字段。
  • schema.add_field(field_name, datatype, is_primary=False, max_length=None, dim=None):
    • field_name:字段名称。
    • datatype:数据类型,如 DataType.INT64、DataType.VARCHAR、DataType.FLOAT_VECTOR、DataType.SPARSE_FLOAT_VECTOR。
    • is_primary:是否为主键。
    • max_length:VARCHAR 最大长度。
    • dim:仅 FLOAT_VECTOR 需要,指定向量维度。
  • client.create_collection(collection_name, schema, consistency_level="Strong"):
    • consistency_level:一致性级别。Strong 强一致性,确保所有数据写入操作在所有相关节点上完成并同步后才允许读取,读取可能稍有延迟。其他级别如 Bounded、Eventually 等。

示例 2:带分区的集合

python 复制代码
COLLECTION_NAME = "tech_books"
PARTITION_NAME = "ai_subfield"
VECTOR_FIELD_NAME = "book_intro_vector"

# ... 创建 schema 和 collection 同上 ...

if not client.has_partition(COLLECTION_NAME, PARTITION_NAME):
    client.create_partition(
        collection_name=COLLECTION_NAME,
        partition_name=PARTITION_NAME
    )

关键函数:

  • client.has_partition(collection_name, partition_name):检查分区是否存在。
  • client.create_partition(collection_name, partition_name):创建分区。

示例 3:使用旧版 Collection + Partition + FieldSchema

python 复制代码
from pymilvus import (
    connections, utility, FieldSchema, CollectionSchema,
    DataType, Collection, Partition, db
)

connections.connect(alias="default", host="localhost", port="19530")
DB_NAME = "vector_demo_db"
COLLECTION_NAME = "demo_dense_sparse_collection"
PARTITION_NAME = "p_2026"

db.using_database(DB_NAME)
if utility.has_collection(COLLECTION_NAME):
    utility.drop_collection(COLLECTION_NAME)

fields = [
    FieldSchema(name="doc_id", dtype=DataType.INT64, is_primary=True),
    FieldSchema(name="content", dtype=DataType.VARCHAR, max_length=1000),
    FieldSchema(name="dense_vec", dtype=DataType.FLOAT_VECTOR, dim=1024),
    FieldSchema(name="sparse_vec", dtype=DataType.SPARSE_FLOAT_VECTOR),
]
schema = CollectionSchema(fields=fields, description="...")
collection = Collection(name=COLLECTION_NAME, schema=schema)
part_2026 = Partition(collection, name=PARTITION_NAME, description="2026文档分区")

区别:

  • 新版 MilvusClient 使用 create_schema 和 add_field,更简洁。
  • 旧版使用 FieldSchema、CollectionSchema、Collection、Partition 类,更面向对象,但代码更冗长。
  • 两者功能等价,新版是趋势。

2.4 创建索引

示例:稠密向量索引(HNSW)和稀疏向量索引

python 复制代码
dense_index_params = {
    "index_type": "HNSW",
    "metric_type": "IP",
    "params": {"M": 16, "ef_construction": 100}
}
collection.create_index(field_name="dense_vec", index_params=dense_index_params)

sparse_index_params = {
    "index_type": "SPARSE_INVERTED_INDEX",
    "metric_type": "IP",
    "params": {"drop_ratio_build": 0.2}
}
collection.create_index(field_name="sparse_vec", index_params=sparse_index_params)

collection.load()

关键函数与参数:

  • collection.create_index(field_name, index_params):
    • field_name:需要建立索引的字段名。
    • index_params:索引配置字典。
      • index_type:索引类型。稠密向量支持 HNSW、IVF_FLAT、FLAT 等;稀疏向量固定 SPARSE_INVERTED_INDEX。
      • metric_type:度量方式。IP 内积,L2 欧氏距离。
      • params:索引特定参数。如 HNSW 的 M、ef_construction;IVF_FLAT 的 nlist;SPARSE_INVERTED_INDEX 的 drop_ratio_build。
  • collection.load():加载集合到内存,检索前必须调用。

示例:使用 MilvusClient 创建索引

python 复制代码
index_params = client.prepare_index_params()
index_params.add_index(
    field_name=VECTOR_FIELD_NAME,
    index_type="IVF_FLAT",
    metric_type="L2",
    params={"nlist": 128}
)
client.create_index(
    collection_name=COLLECTION_NAME,
    index_params=index_params
)

区别:

  • 旧版 collection.create_index 直接传入字典。
  • 新版 client.prepare_index_params() 先创建参数对象,再 add_index,最后 client.create_index。更灵活,支持一次添加多个索引。

2.5 插入数据

示例:插入到指定分区

python 复制代码
data_rows = [
    {
        "book_id": 1,
        "book_title": "Attention Is All You Need",
        VECTOR_FIELD_NAME: [random.random() for _ in range(768)]
    },
    {
        "book_id": 2,
        "book_title": "Deep Learning with Python",
        VECTOR_FIELD_NAME: [random.random() for _ in range(768)]
    }
]

result = client.insert(
    collection_name=COLLECTION_NAME,
    data=data_rows,
    partition_name=PARTITION_NAME
)
print(f"数据插入成功,主键 ID 为: {result['ids']}")

关键函数与参数:

  • client.insert(collection_name, data, partition_name=None):
    • data:列表,每个元素是字典,键为字段名,值为对应数据。也可以使用二维数组按字段顺序组织。
    • partition_name:指定写入哪个分区,默认写入 _default 分区。
    • 返回:InsertResult,包含插入成功的 id 信息。

旧版插入:

python 复制代码
insert_data = [
    [1, 2, 3],  # doc_id
    ["Milvus支持稠密向量语义检索", ...],  # content
    [gen_dummy_dense_vector(), ...],  # dense_vec
    [gen_dummy_sparse_vector(), ...]  # sparse_vec
]
insert_result = collection.insert(insert_data, partition_name=PARTITION_NAME)
collection.flush()

区别:

  • 新版支持字典列表,更直观。
  • 旧版使用二维数组,按字段顺序组织,容易出错。
  • collection.flush() 用于持久化落盘,新版 client.insert 后通常不需要手动 flush,但旧版需要。

2.6 查询 Query(标量查询)

python 复制代码
query_res = collection.query(
    expr="doc_id in [1,2]",
    output_fields=["doc_id", "content", "dense_vec", "sparse_vec"],
    partition_names=[PARTITION_NAME]
)

关键函数与参数:

  • collection.query(expr, output_fields=None, partition_names=None):
    • expr:布尔表达式,过滤条件,类似 where 语句。
    • output_fields:需要返回的字段列表,* 代表全部字段。
    • partition_names:限定查询分区。
    • 注意:query 是按标量条件查询,不是向量相似度检索。

使用 MilvusClient 的 query:

python 复制代码
query_results = client.query(
    collection_name=COLLECTION_NAME,
    filter="release_year > 2005",
    output_fields=["id", "title", "release_year"]
)

区别:

  • 旧版 collection.query(expr=...) 使用 expr 参数。
  • 新版 client.query(filter=...) 使用 filter 参数。
  • 功能相同。

2.7 向量相似度检索 search(单路)

单路稠密向量检索:

python 复制代码
query_dense_vec = [gen_dummy_dense_vector()]
search_dense_res = collection.search(
    data=query_dense_vec,
    anns_field="dense_vec",
    param={"metric_type": "IP", "params": {"ef": 100}},
    limit=3,
    output_fields=["doc_id", "content"],
    partition_names=[PARTITION_NAME]
)

单路稀疏向量检索:

python 复制代码
query_sparse_vec = [{10: 0.7, 22: 0.4}]
search_sparse_res = collection.search(
    data=query_sparse_vec,
    anns_field="sparse_vec",
    param={"metric_type": "IP"},
    limit=3,
    output_fields=["doc_id", "content"],
    partition_names=[PARTITION_NAME]
)

关键函数与参数:

  • collection.search(data, anns_field, param, limit, expr=None, output_fields=None, partition_names=None):
    • data:list向量,查询向量数组;稠密是 list 浮点数,稀疏是 dict。
    • anns_field:待检索的向量字段名称。
    • param:索引相关参数,metric_type 要和索引保持一致。params 中可设置搜索参数,如 HNSW 的 ef,IVF_FLAT 的 nprobe。
    • limit:返回 topN 结果数量。
    • expr:可选标量过滤条件。
    • output_fields:返回的非向量字段。
    • partition_names:限定检索分区。
    • 返回:SearchResult,包含 hits,每个 hit 有 id、score、entity 字段。

使用 MilvusClient 的 search:

python 复制代码
search_results = client.search(
    collection_name=COLLECTION_NAME,
    data=[query_vector],
    limit=2,
    partition_names=[PARTITION_NAME],
    output_fields=["book_title"]
)

区别:

  • 旧版 collection.search 需要指定 anns_field 和 param。
  • 新版 client.search 自动使用已创建的索引,只需指定 data、limit、filter 等。更简洁。
python 复制代码
hybrid_reqs = [
    HybridSearchRequest(
        data=query_dense_vec,
        anns_field="dense_vec",
        param={"metric_type": "IP", "params": {"ef": 100}},
        limit=3,
        weight=0.6
    ),
    HybridSearchRequest(
        data=query_sparse_vec,
        anns_field="sparse_vec",
        param={"metric_type": "IP"},
        limit=3,
        weight=0.4
    )
]
hybrid_res = collection.hybrid_search(
    reqs=hybrid_reqs,
    limit=3,
    output_fields=["doc_id", "content"],
    partition_names=[PARTITION_NAME]
)

关键函数与参数:

  • collection.hybrid_search(reqs, limit, output_fields=None, partition_names=None):
    • reqs:listHybridSearchRequest,多路检索请求。
    • HybridSearchRequest(data, anns_field, param, limit, weight):
      • weight:当前这一路分数权重,两路权重之和不强制等于 1。
    • limit:混合检索最终返回 topN。
    • output_fields:返回字段。
    • partition_names:限定检索分区。

使用 MilvusClient 的 hybrid_search:

python 复制代码
from pymilvus import AnnSearchRequest, WeightedRanker, RRFRanker

dense_request = AnnSearchRequest(
    data=[dense_query_vector],
    anns_field="dense_vector",
    param={"metric_type": "IP", "params": {"nprobe": 2}},
    limit=10
)
sparse_request = AnnSearchRequest(
    data=[sparse_query_vector],
    anns_field="sparse_vector",
    param={"metric_type": "IP", "params": {}},
    limit=10
)

reranker = WeightedRanker(0.7, 0.3)  # 或 RRFRanker()
milvus_results = client.hybrid_search(
    collection_name=COLLECTION_NAME,
    reqs=[dense_request, sparse_request],
    ranker=reranker,
    limit=5,
    output_fields=["text", "doc_hash", "file_path", "timestamp", "parent_content", "parent_id"]
)[0]

区别:

  • 旧版 collection.hybrid_search 使用 HybridSearchRequest,权重直接写在请求中。
  • 新版 client.hybrid_search 使用 AnnSearchRequest,权重通过 WeightedRanker 或 RRFRanker 指定。
  • WeightedRanker:加权求和,权重之和不必为 1,分数范围因度量方式而异。
  • RRFRanker:Reciprocal Rank Fusion,基于排名的融合,不需要指定权重,对分数范围不敏感,更鲁棒。

WeightedRanker 与 RRFRanker 的区别:

  • WeightedRanker(w1, w2, ...):对每一路检索的分数进行加权求和。需要手动调权重,适合对分数分布有把握的场景。
  • RRFRanker(): Reciprocal Rank Fusion,根据每一路结果中的排名计算融合分数,公式为 1/(k + rank)。不需要调权重,对异常分数不敏感,适合多路检索结果分数不可比的情况。

2.9 更新与删除

更新: Milvus 没有 update 语法,采用删除旧数据 + 插入新数据。

python 复制代码
collection.delete(expr="doc_id == 1", partition_name=PARTITION_NAME)
collection.flush()
# 插入更新后的新数据
update_data = [
    [1],
    ["【更新后】Milvus稠密向量用于语义匹配"],
    [gen_dummy_dense_vector()],
    [gen_dummy_sparse_vector()]
]
collection.insert(update_data, partition_name=PARTITION_NAME)
collection.flush()

删除:

python 复制代码
collection.delete(expr="doc_id == 3", partition_name=PARTITION_NAME)
collection.flush()

关键函数:

  • collection.delete(expr, partition_name=None):按 expr 条件删除实体。底层是墓碑标记,不会立刻清理磁盘。
  • collection.flush():持久化落盘。

2.10 分区加载与释放

python 复制代码
client.load_partitions(
    collection_name=COLLECTION_NAME,
    partition_names=[PARTITION_NAME]
)
# 搜索...
client.release_partitions(
    collection_name=COLLECTION_NAME,
    partition_names=[PARTITION_NAME]
)

关键函数:

  • client.load_partitions(collection_name, partition_names):加载指定分区到内存。
  • client.release_partitions(collection_name, partition_names):释放指定分区的内存。
  • 检索前必须加载对应分区或整个集合。

2.11 多查询向量搜索

python 复制代码
query_vectors = [
    normalize_vector([random.random() for _ in range(768)]),
    normalize_vector([random.random() for _ in range(768)])
]
search_results = client.search(
    collection_name=COLLECTION_NAME,
    data=query_vectors,
    limit=2,
    partition_names=[PARTITION_NAME],
    output_fields=["book_title"]
)

说明:

  • data 可以传入多个查询向量,返回结果是一个列表,每个元素对应一个查询向量的搜索结果。
  • 适合批量查询场景。

2.12 向量归一化

python 复制代码
import numpy as np

def normalize_vector(vector):
    norm = np.linalg.norm(vector)
    if norm == 0:
        return vector
    return [x / norm for x in vector]

作用:

  • 使用 IP 度量时,归一化后内积等价于余弦相似度。
  • 避免向量模长影响相似度计算。

三、完整 RAG 父子分块案例(milvus_3_case_2_WeightRank_RRFRank.py)

3.1 流程概述

  1. 读取 PDF 简历。
  2. 层级分块:先切分为父块,再将每个父块切分为子块,添加 parent_id 和 parent_content。
  3. 使用 BGE-M3 生成子块的 dense 和 sparse 向量。
  4. 创建 Milvus 集合,包含稠密向量、稀疏向量、文本、父块信息等字段。
  5. 插入数据。
  6. 混合搜索:稠密 + 稀疏,使用 WeightedRanker 或 RRFRanker 融合。
  7. 输出结果,优先返回父文档内容。

3.2 关键代码与函数

读取 PDF:

python 复制代码
from pypdf import PdfReader

def read_pdf(file_path: str) -> str:
    reader = PdfReader(file_path)
    text = ""
    for page in reader.pages:
        text += page.extract_text() or ""
    return text

层级分块:

python 复制代码
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.docstore.document import Document
import hashlib
from datetime import datetime

def chunk_text_hierarchical(text, parent_chunk_size=200, child_chunk_size=100, chunk_overlap=50):
    parent_splitter = RecursiveCharacterTextSplitter(chunk_size=parent_chunk_size, chunk_overlap=chunk_overlap)
    child_splitter = RecursiveCharacterTextSplitter(chunk_size=child_chunk_size, chunk_overlap=chunk_overlap)

    doc_hash = hashlib.md5(text.encode('utf-8')).hexdigest()
    doc = Document(page_content=text, metadata={"file_path": PDF_PATH, "hash": doc_hash, "timestamp": datetime.now().isoformat()})

    parent_docs = parent_splitter.split_documents([doc])
    child_chunks = []
    for j, parent_doc in enumerate(parent_docs):
        parent_id = f"{doc_hash}_parent_{j}"
        parent_doc.metadata["parent_id"] = parent_id
        parent_doc.metadata["parent_content"] = parent_doc.page_content
        parent_doc.metadata.update(doc.metadata)

        sub_chunks = child_splitter.split_documents([parent_doc])
        for k, sub_chunk in enumerate(sub_chunks):
            sub_chunk.metadata["parent_id"] = parent_id
            sub_chunk.metadata["parent_content"] = parent_doc.page_content
            sub_chunk.metadata["id"] = f"{parent_id}_child_{k}"
            sub_chunk.metadata.update(parent_doc.metadata)
            child_chunks.append(sub_chunk)
    return child_chunks

BGE-M3 向量化:

python 复制代码
from milvus_model.hybrid import BGEM3EmbeddingFunction

embedding_function = BGEM3EmbeddingFunction(
    model_name=EMBEDDING_MODEL_PATH,
    device='cpu',
    use_fp16=False
)
dense_dim = embedding_function.dim["dense"]
embeddings = embedding_function.encode_documents(texts)

CSR 稀疏向量转换:

python 复制代码
def csr_to_milvus_sparse(csr_matrix, index: int) -> dict:
    indices = csr_matrix.indices[csr_matrix.indptr[index]:csr_matrix.indptr[index + 1]]
    data = csr_matrix.data[csr_matrix.indptr[index]:csr_matrix.indptr[index + 1]]
    return {int(k): float(v) for k, v in zip(indices, data)}

创建集合 Schema:

python 复制代码
schema = client.create_schema(auto_id=False, enable_dynamic_field=True)
schema.add_field(field_name="id", datatype=DataType.VARCHAR, is_primary=True, max_length=100)
schema.add_field(field_name="chunk_id", datatype=DataType.VARCHAR, max_length=100)
schema.add_field(field_name="text", datatype=DataType.VARCHAR, max_length=65535)
schema.add_field(field_name="dense_vector", datatype=DataType.FLOAT_VECTOR, dim=dense_dim)
schema.add_field(field_name="sparse_vector", datatype=DataType.SPARSE_FLOAT_VECTOR)
schema.add_field(field_name="parent_id", datatype=DataType.VARCHAR, max_length=100)
schema.add_field(field_name="parent_content", datatype=DataType.VARCHAR, max_length=65535)
schema.add_field(field_name="file_path", datatype=DataType.VARCHAR, max_length=255)
schema.add_field(field_name="doc_hash", datatype=DataType.VARCHAR, max_length=32)
schema.add_field(field_name="timestamp", datatype=DataType.VARCHAR, max_length=50)

索引参数:

python 复制代码
index_params = client.prepare_index_params()
index_params.add_index(
    field_name="dense_vector", index_name="dense_index", index_type="IVF_FLAT",
    metric_type="IP", params={"nlist": 2}
)
index_params.add_index(
    field_name="sparse_vector", index_name="sparse_index", index_type="SPARSE_INVERTED_INDEX",
    metric_type="IP"
)
client.create_collection(collection_name=COLLECTION_NAME, schema=schema, index_params=index_params)

混合搜索:

python 复制代码
dense_request = AnnSearchRequest(
    data=[dense_query_vector], anns_field="dense_vector",
    param={"metric_type": "IP", "params": {"nprobe": 2}}, limit=10
)
sparse_request = AnnSearchRequest(
    data=[sparse_query_vector], anns_field="sparse_vector",
    param={"metric_type": "IP", "params": {}}, limit=10
)
reranker = WeightedRanker(0.7, 0.3)  # 或 RRFRanker()
milvus_results = client.hybrid_search(
    collection_name=COLLECTION_NAME,
    reqs=[dense_request, sparse_request],
    ranker=reranker,
    limit=5,
    output_fields=["text", "doc_hash", "file_path", "timestamp", "parent_content", "parent_id"]
)[0]

输出结果:

python 复制代码
for hit in milvus_results:
    entity = hit["entity"]
    print(f"文档哈希: {entity['doc_hash']}")
    print(f"文件路径: {entity['file_path']}")
    print(f"时间戳: {entity['timestamp']}")
    print(f"父块 ID: {entity['parent_id']}")
    print(f"父文档内容: {entity['parent_content'][:250]}...")
    print(f"子块内容: {entity['text'][:100]}...")
    print(f"相似度得分: {hit['distance']:.4f}")

3.3 父子分块 Milvus + MySQL 简易表结构

Milvus 集合(存储子块向量):

python 复制代码
fields = [
    FieldSchema(name="child_id", dtype=DataType.INT64, is_primary=True),
    FieldSchema(name="parent_id", dtype=DataType.INT64),
    FieldSchema(name="child_text", dtype=DataType.VARCHAR, max_length=1000),
    FieldSchema(name="dense_vec", dtype=DataType.FLOAT_VECTOR, dim=1024),
]

MySQL 父块表:

parent_id parent_content
1001 完整长段落父块文本...

检索伪代码:

python 复制代码
# 1. 问题向量检索子块
res = collection.search(..., output_fields=["parent_id"])
# 2. 提取命中 parent_id 并去重
parent_ids = {hit.entity.get("parent_id") for hit in hits}
# 3. MySQL 批量查询父块原文
parent_texts = mysql_client.query("select parent_content from parent_table where parent_id in (%s)", parent_ids)

四、不同代码方法对比总结

对比项 旧版 API (connections + Collection) 新版 API (MilvusClient)
连接 connections.connect(alias, host, port) MilvusClient(uri, db_name)
数据库 db.create_database, db.using_database client.create_database, client.using_database
Schema FieldSchema, CollectionSchema client.create_schema(), schema.add_field()
创建集合 Collection(name, schema) client.create_collection(collection_name, schema)
分区 Partition(collection, name) client.create_partition(collection_name, partition_name)
索引 collection.create_index(field_name, index_params) client.prepare_index_params(), client.create_index()
插入 collection.insert(data, partition_name) client.insert(collection_name, data, partition_name)
查询 collection.query(expr, output_fields) client.query(collection_name, filter, output_fields)
搜索 collection.search(data, anns_field, param, limit) client.search(collection_name, data, limit, filter)
混合搜索 collection.hybrid_search(reqs, limit) client.hybrid_search(collection_name, reqs, ranker, limit)
加载/释放 collection.load(), collection.release() client.load_collection(), client.release_collection()
分区加载 client.load_partitions(), client.release_partitions() 同左
删除 collection.delete(expr) client.delete(collection_name, filter)

索引类型对比:

索引类型 适用向量 特点 关键参数
FLAT 稠密 精确搜索,100% 准确,性能低 无
IVF_FLAT 稠密 基于聚类,平衡性能与准确率 nlist(聚类数),nprobe(搜索簇数)
HNSW 稠密 基于图,高召回,高内存 M,ef_construction,ef
SPARSE_INVERTED_INDEX 稀疏 倒排索引,关键词匹配 drop_ratio_build

度量方式对比:

度量 公式 特点 适用场景
L2 欧氏距离 值越小越相似 图像、物理坐标
IP 内积 值越大越相似,归一化后等价余弦相似度 文本语义相似度

混合检索融合器对比:

融合器 原理 优点 缺点
WeightedRanker 加权求和 可手动调权重,灵活 需要调参,对分数范围敏感
RRFRanker 基于排名融合 无需调参,鲁棒 丢失分数绝对值信息
相关推荐
资深技术分享员1 小时前
Geejing WebBuilder 日志、在线用户、系统监控,出问题时的三个第一现场
java·运维·数据库
我是小白呀2 小时前
n8n 实战:把 AcmeFlow 的 READY 事件接到 CRM 通知
数据库·人工智能·workflow
蓝速科技2 小时前
仓储盘点移动终端选型与蓝速科技 K10 实战方案
运维·数据库·人工智能·科技·材质
TLA技术2 小时前
LogMiner vs 裸日志解析(六):想提速,只能“堆实例”,结果把源库拖垮
数据库·oracle·flink·dba·迁移学习
学好statistics和DS2 小时前
数据库数据世界的逻辑基石:Armstrong公理系统全解析
数据库·数据库开发
IT古董2 小时前
《FDE前沿部署工程师实战教程》23 - Enterprise AI Event Bus:事件驱动、消息队列与AI工作流自动触发
大数据·数据库·人工智能
程序猿乐锅2 小时前
【黑马点评 | 第七篇】Redis 分布式锁的两种实现
java·数据库·redis·分布式·spring·缓存
这个DBA有点耶2 小时前
连接池与MySQL交互实战:连接风暴、连接泄漏与连接状态异常排查
数据库·mysql·架构
TDengine (老段)2 小时前
TDengine TSDB 实战排障三(集群高可用)
数据库·物联网·时序数据库·tdengine·涛思数据·问题排查