跳至内容

Milvus 向量检索

Milvus相似度检索入门和部署。

  • 向量检索
  • Milvus
  • Embedding
  • HNSW

Milvus 负责存储、索引和检索向量,原文、类别等字段可与向量保存在同一条记录中。 Embedding 模型把文本转换成向量,Milvus 根据向量相似度查找内容,并按业务字段限定检索范围。

官网:https://milvus.io/

1. 向量与集合

向量是一组数字,维度就是这组数字的数量。 语义检索根据向量间的接近程度寻找相关内容。

文档入库: 文档批量编码,向量与原文、业务字段一起写入集合。

代码块 Text
1
文档 → Embedding 模型 → 文档向量 → 与原文、类别组成记录 → Milvus 集合

查询检索: 用户输入单独编码,Milvus 在已有文档向量中搜索,返回匹配记录。

代码块 Text
1
查询文本 → Embedding 模型 → 查询向量 → Milvus 搜索 → 文档、相似度分数

文档和查询需要使用兼容的模型与编码方式。更换模型后,即使维度相同,旧文档向量也未必能与新查询向量比较,需要重新编码文档。

Milvus 用 Collection(集合) 组织记录,Schema(字段定义) 规定字段名称、类型等属性。

文档检索示例包含以下字段,名称由应用程序定义:

字段类型用途
idINT64文档主键
vectorFLOAT_VECTOR1024 维文档向量
textVARCHAR原始文本
categoryVARCHAR文档类别

vector 用于相似度检索;text、category 保存原文和类别,用于返回内容或执行条件过滤。这类不存储向量的字段称为标量字段。

文档向量由 Python 程序调用模型生成并传入 vector,写入 text 字段本身不会触发编码。

2. 文档编码

示例使用 Python 3.13 和 CPU。以下命令在 macOS、Linux 中创建环境,python3 应指向 Python 3.13:

代码块 Shell
1
2
3
python3 -m venv .venv
source .venv/bin/activate
python -m pip install "pymilvus==3.0.2" "milvus-lite==3.2.2" "sentence-transformers==5.1.2" "faiss-cpu==1.12.0"

faiss-cpu 是 Milvus Lite 3.2.2 的底层依赖,安装命令将其固定为 1.12.0。该安装包要求 Apple Silicon 使用 macOS 14 及以上,Intel Mac 使用 macOS 13 及以上。安装包平台

模型采用 BAAI/bge-m3,通过 Sentence Transformers 将文档和查询编码为 1024 维稠密向量。查询无需添加检索指令,首次加载会下载模型文件。

第 2~6 节代码在同一 Python 会话中依次执行,示例包含六条文档:

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np
from sentence_transformers import SentenceTransformer

docs = [
    {"id": 101, "text": "Faiss 提供向量索引和相似度搜索,原始文档由应用程序管理。", "category": "数据库"},
    {"id": 102, "text": "Milvus 支持在向量搜索中按类别等字段过滤文档。", "category": "数据库"},
    {"id": 103, "text": "MySQL 使用关系表保存数据,通过 SQL 查询和更新记录。", "category": "数据库"},
    {"id": 104, "text": "梯度下降根据损失函数的梯度调整模型参数。", "category": "机器学习"},
    {"id": 105, "text": "文本分类模型为文档预测类别,用于组织搜索结果。", "category": "机器学习"},
    {"id": 106, "text": "Docker Compose 使用配置文件管理多个容器及其数据卷。", "category": "运维"},
]

model = SentenceTransformer("BAAI/bge-m3", device="cpu")
texts = [doc["text"] for doc in docs]
vectors = model.encode(texts, normalize_embeddings=True).astype(np.float32)

ids = np.array([doc["id"] for doc in docs], dtype=np.int64)
categories = [doc["category"] for doc in docs]
np.savez("vectors.npz", vectors=vectors, ids=ids, texts=texts, categories=categories)
print(vectors.shape)
# 输出:(6, 1024)

vectors 的每一行对应一篇文档。normalize_embeddings=True 将向量归一化为单位长度,数组使用 float32 保存。

np.savez 在当前工作目录生成 vectors.npz,保存文档向量、ID、原文和类别,读取后能重建完整记录。

数据量较大时,文档编码和数据库写入可分别分批处理。每批编码多少条取决于显存或内存,每批写入多少条取决于请求大小和数据库的写入能力,两者不必相同。

3. 集合与索引

3.1 定义字段

Milvus Lite 提供本地向量数据库,Python 客户端通过数据路径连接它。

下面定义集合名称和字段:

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pymilvus import DataType, MilvusClient

client = MilvusClient(uri="./milvus_bge_m3.db")
collection = "article_demo"

schema = MilvusClient.create_schema(auto_id=False, enable_dynamic_field=False)
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True)
schema.add_field(field_name="vector", datatype=DataType.FLOAT_VECTOR, dim=vectors.shape[1])
schema.add_field(field_name="text", datatype=DataType.VARCHAR, max_length=1024)
schema.add_field(field_name="category", datatype=DataType.VARCHAR, max_length=64)

数据目录由 uri 指定为 ./milvus_bge_m3.db,集合名由 collection 指定为 article_demo。

dim 必须与模型输出维度一致。auto_id=False 表示主键由程序提供;max_length 按 UTF-8 字节计算,不是汉字个数。

3.2 字段与度量

字段名、距离度量和索引类型分别对应不同的配置项:

概念配置项含义与示例
向量字段建索引时的 field_name、搜索时的 anns_field指向 Schema 中存储向量的字段,如 vector
距离度量metric_type规定向量之间的比较方式,如 L2、IP、COSINE
索引类型index_type规定向量的组织和检索方式,如 FLAT、IVF_FLAT、HNSW

具体使用哪种度量,要看 metric_type,不能只看索引名称。 IVF_FLAT 支持 L2、IP、COSINE,建索引时通过 metric_type 选定其中一种:

  • L2:欧氏距离。 比较向量在空间中的距离,数值越小越接近。Milvus 返回的是欧氏距离的平方,不执行开平方。
  • IP:内积。 对应维度相乘后求和,受向量长度和方向影响,数值越大越相似。
  • COSINE:余弦相似度。 比较向量方向,取值范围为 -1 到 1,数值越大越相似。

选择度量时应结合模型的使用说明。

文档与查询都归一化为单位长度后,IP 与 COSINE 的结果相等。相似度分数不等于答案正确的概率。

3.3 索引类型

Top-K 指按所选度量排序最靠前的 K 条记录。索引的检索方式分为两类:

  • 精确检索:找出真实的 Top-K。
  • 近似检索:减少搜索范围或压缩向量,节省计算和内存,但可能漏掉部分近邻。

常见的浮点向量索引如下:

索引检索方式主要取舍
FLAT逐个比较原始向量,精确检索不需要聚类或建图,查询计算量随数据量增长
IVF_FLAT向量聚成若干组,查询只搜索选中的组保存原始向量,可能漏掉未访问组中的近邻
IVF_SQ8在 IVF 分组基础上,把每个维度量化为 8 位降低向量内存占用,额外引入量化误差
IVF_PQ在 IVF 分组基础上,把向量分段并用码本编码需要训练码本,压缩率和精度受参数影响
HNSW建立多层近邻图,沿图寻找候选常用于低延迟检索,图结构增加内存和构建成本
DISKANN将主要图结构放在磁盘,结合内存中的压缩表示搜索适合数据量超过内存的场景,依赖 SSD 和磁盘 I/O
AUTOINDEX由当前部署按默认策略选择索引减少初始配置,实际实现随版本和配置变化

索引还要匹配字段类型和运行环境:

  • 稀疏向量:SPARSE_INVERTED_INDEX。
  • 二进制向量:BIN_FLAT、BIN_IVF_FLAT 等。
  • GPU 部署:另有 GPU_CAGRA、GPU_IVF_FLAT 等索引。
  • 标量字段:category 等字段能建立 INVERTED 索引,加速条件过滤。

Lite 3.2.2 支持 FLAT、IVF_FLAT、IVF_SQ8、HNSW 等索引,不支持 PQ、二进制向量、DISKANN 和 GPU 索引。上表包含服务端能力,不能在 Lite 中任意替换类型。

3.4 索引参数

建索引参数和搜索参数作用不同。 前者决定索引结构,后者控制每次查询的搜索范围:

索引建索引时设置搜索时设置
IVF 系列nlist:聚类分组数nprobe:一次查询访问多少组
HNSWM:节点连接规模;efConstruction:建图时的候选规模ef:查询时的候选规模

IVF_PQ 另有分段数 m 和编码位数 nbits,其中 m 必须整除向量维度。

以 IVF_FLAT 为例,nlist=100 表示将向量分成 100 组:

  • nprobe=10:每次搜索其中 10 组,减少比较量,但可能漏掉其他组里的近邻。
  • nprobe=100:搜索全部分组。由于保存了原始向量,此时可得到精确结果,也失去了跳过分组带来的收益。

增大 nprobe 或 ef 通常能提高召回率,也会增加查询耗时。近邻检索的召回率衡量找回了多少真实近邻,文档是否回答了问题仍需单独评价。

选择索引时,需要同时比较召回率、查询延迟、内存和构建时间。六条文档适合验证流程,无法体现大规模检索的性能差异。

3.5 建立索引与写入

在 article_demo 的 vector 字段上建立 FLAT 索引,使用 COSINE 度量。六条文档直接逐个比较即可:

代码块 Python
1
2
3
4
5
6
7
8
9
index_params = client.prepare_index_params()
index_params.add_index(field_name="vector", index_type="FLAT", metric_type="COSINE")
client.create_collection(collection_name=collection, schema=schema, index_params=index_params,
                         consistency_level="Strong")

records = [{**doc, "vector": vector.tolist()} for doc, vector in zip(docs, vectors)]
result = client.insert(collection_name=collection, data=records)
print(result["insert_count"])
# 输出:6

传入 index_params 创建集合时,会建立索引并加载集合。Strong 使后续查询能读取已完成的写入。

创建前,确认 article_demo 尚不存在。重复实验时更换集合名,并同步替换后续读取示例中的 article_demo。只读已有数据不需要再次创建或插入。

4. 搜索与过滤

4.1 查询编码

查询到来时,只需编码查询文本,已入库的文档向量直接复用:

代码块 Python
1
2
3
4
query = "检索结果只保留指定类别的文档"
query_vector = model.encode([query], normalize_embeddings=True).astype(np.float32)
print(query_vector.shape)
# 输出:(1, 1024)

query_vector 有一行,对应一次查询;维度和归一化方式与文档向量一致。

4.2 相似度搜索

search 接收查询向量,anns_field 指定检索字段。搜索沿用 vector 字段索引的 COSINE 度量,limit=3 表示返回相似度最高的三条记录:

代码块 Python
1
2
3
4
5
hits = client.search(collection_name=collection, data=query_vector.tolist(), anns_field="vector",
                     limit=3, output_fields=["text", "category"])

for hit in hits[0]:
    print(hit["id"], round(hit["distance"], 4), hit["entity"]["text"])

hits[0] 对应第一个查询。每条结果的 id 是主键,entity 包含 output_fields 指定的字段。

distance 是所选度量的分数。 使用 COSINE 时,distance 越大越相似。

不同索引的搜索参数也不同:

  • IVF:通过 search_params={"params": {"nprobe": 10}} 设置访问的分组数。
  • HNSW:使用 ef 控制候选规模,普通 Top-K 搜索中它应至少等于 limit。
  • FLAT:不需要设置 nprobe 或 ef。

4.3 字段过滤

只搜索“数据库”类别时,将条件传入 filter:

代码块 Python
1
2
3
4
5
hits = client.search(collection_name=collection, data=query_vector.tolist(), anns_field="vector",
                     filter='category == "数据库"', limit=2, output_fields=["text", "category"])

for hit in hits[0]:
    print(hit["id"], hit["entity"]["category"], hit["entity"]["text"])

Milvus 在符合条件的记录中检索。 六条文档中,“数据库”类别的候选记录是 101、102、103,返回其中最相似的两条;104、105、106 不属于“数据库”,不会出现在结果中。

对全量 Top-K 再筛类别,可能得不到限定类别的 Top-K:

假设全量 Top-2 是“机器学习”文档 A 和“数据库”文档 B:

  • 全量检索后筛类别:删掉 A,只剩 B。
  • 限定“数据库”检索:返回 B 和该类别中排名第二的 C。

C 没进入全量 Top-2,却应出现在类别 Top-2 中。

5. 查询、更新与删除

5.1 按字段查询

query 按条件取记录,不计算向量相似度。指定 id in [101, 102],即可读取这两条记录的原文和类别:

代码块 Python
1
2
3
4
5
6
rows = client.query(collection_name=collection, filter="id in [101, 102]", output_fields=["text", "category"])
for row in sorted(rows, key=lambda row: row["id"]):
    print(row["id"], row["category"], row["text"])
# 输出:
# 101 数据库 Faiss 提供向量索引和相似度搜索,原始文档由应用程序管理。
# 102 数据库 Milvus 支持在向量搜索中按类别等字段过滤文档。

5.2 更新记录

upsert 更新已有主键,对不存在的主键执行插入。下面提交主键 101 的完整记录,只修改类别:

代码块 Python
1
2
3
4
updated_record = {**records[0], "category": "检索工具"}
result = client.upsert(collection_name=collection, data=[updated_record])
print(result["upsert_count"])
# 输出:1

类别变化不影响这条记录的文本向量。修改正文时,需要重新编码正文,并将新向量与新正文一起更新。 否则返回的文字已经变化,检索仍使用旧内容的表示。

5.3 删除记录

代码块 Python
1
2
3
client.delete(collection_name=collection, ids=[106])
print(client.query(collection_name=collection, filter="id == 106", output_fields=["text"]))
# 输出:data: [], extra_info: {}

data: [] 表示已经查不到 ID 为 106 的记录。删除只影响数据库,不会修改内存中的 docs、records 或之前保存的 vectors.npz。

6. 本地数据保存

Lite 将集合、向量和标量字段保存在 milvus_bge_m3.db 目录中。close() 关闭连接,数据仍然保留;重新连接同一路径即可读取:

代码块 Python
1
2
3
4
5
6
7
8
client.close()
client = MilvusClient(uri="./milvus_bge_m3.db")
client.load_collection(collection_name="article_demo")
rows = client.query(collection_name="article_demo", filter="id == 101", output_fields=["text", "category"])
for row in rows:
    print(row["id"], row["category"], row["text"])
client.close()
# 输出:101 检索工具 Faiss 提供向量索引和相似度搜索,原始文档由应用程序管理。

load_collection 加载已有集合,不会重新创建集合或插入数据。

重新连接后,ID 101 的类别仍是更新后的“检索工具”。vectors.npz 中该记录的类别仍是编码时的“数据库”。

7. Docker 部署

7.1 部署形式

形式运行方式常见用途
LitePython 程序连接本地存储Notebook、功能验证、本地小型应用
Standalone独立的 Milvus 服务及其依赖单机部署、多个客户端访问
Distributed多节点分布式服务分担数据和查询负载,扩展计算资源

本节部署 Milvus Standalone 3.0.2。Linux 需要 Docker Engine 和 Compose V2,macOS 使用 Docker Desktop。

按官方环境要求配置资源:

  • Docker Desktop 虚拟机:至少分配 2 个 CPU 核心、8 GB 内存。
  • Milvus Standalone:推荐 4 核、16 GB 内存。

实际资源需求还取决于向量数量、维度、索引和查询负载。

7.2 启动服务

下载 Compose 配置并启动:

代码块 Shell
1
2
3
4
5
6
7
8
9
mkdir milvus-demo
cd milvus-demo

curl -L --fail \
  https://github.com/milvus-io/milvus/releases/download/v3.0.2/milvus-standalone-docker-compose.yaml \
  -o docker-compose.yaml

docker compose up -d
docker compose ps -a

配置中有三个常驻服务:

服务职责宿主机端口数据目录
etcd保存元数据不映射volumes/etcd
minio保存对象数据9000、9001volumes/minio
standalone提供 Milvus 服务19530、9091volumes/milvus

milvus-volume-init 是初始化数据目录的一次性任务,正常完成时显示 Exited (0)。

三个常驻服务显示 healthy,表示各自的状态检查已通过;刚启动时可能显示 health: starting。

查看启动日志和服务状态:

代码块 Shell
1
2
docker compose logs --tail=100 standalone
curl --fail http://localhost:9091/healthz

19530 是客户端端口;9091 提供状态检查和 WebUI,浏览器地址为 http://localhost:9091/webui/。

7.3 Python 连接

Python 程序运行在 Docker 宿主机上时,连接 localhost:19530:

代码块 Python
1
2
3
4
5
from pymilvus import MilvusClient

client = MilvusClient(uri="http://localhost:19530")
print(client.list_collections())
# 新实例输出:[]

第 2 节的 pymilvus==3.0.2 与服务版本对应。其他容器中的 localhost 指向容器自身,需要改成该容器能够访问的 Milvus 服务地址。

Lite 和 Docker 服务是两个独立的数据存储,改变连接地址不会迁移数据。 向新服务写入六条文档时:

  1. 使用已连接 Docker 中 Milvus 服务的 client,准备好 docs 和 vectors。
  2. 执行第 3.1 节代码,跳过连接 Lite 的 MilvusClient(uri="./milvus_bge_m3.db")。
  3. 执行第 3.5 节的建索引和写入代码。

同一 Python 会话中可复用已有的 docs 和 vectors。换了会话时,在第 1 步从 vectors.npz 恢复它们,无需重新加载模型编码文档:

代码块 Python
1
2
3
4
5
6
import numpy as np

data = np.load("vectors.npz")
vectors = data["vectors"]
docs = [{"id": int(doc_id), "text": str(text), "category": str(category)}
        for doc_id, text, category in zip(data["ids"], data["texts"], data["categories"])]

在 vectors.npz 所在目录执行这段代码,或把路径改为该文件的实际位置。

恢复的数据包含原始六条文档,不含第 5 节的类别更新和删除。

如果要在新会话中继续第 4 节的相似度搜索,还需要执行第 2 节中 SentenceTransformer 的导入和 model 的初始化语句。查询编码使用这个模型,已恢复的文档向量不需要重新生成。

7.4 停止与重启

在 docker-compose.yaml 所在目录执行:

代码块 Shell
1
docker compose down

该命令停止并移除容器,数据保留在 volumes 目录中。重新启动会继续挂载这些数据:

代码块 Shell
1
docker compose up -d

读取已有集合时不需要再次创建或插入:

代码块 Python
1
2
3
4
client = MilvusClient(uri="http://localhost:19530")
client.load_collection(collection_name="article_demo")
print(client.query(collection_name="article_demo", filter="id == 101", output_fields=["text", "category"]))
client.close()

8. 客户端复用

实际调用中,集合名、向量字段、度量和返回字段通常保持不变,可以在客户端初始化时集中配置。每次检索只传入查询向量、过滤条件和返回条数。

Milvus 集合客户端完整脚本

  • 集合初始化:按调用方提供的 Schema 和索引参数创建集合,已有集合显式加载。
  • 分批写入与更新:insert()、upsert() 自动拆分批次,返回累计写入数。
  • 检索与记录管理:search() 接受一条向量,支持标量过滤和分数筛选;query() 按条件查询,delete() 按主键删除。

以下调用使用配套的 milvus_client.py 模块。服务中已有第 7.3 节写入的 article_demo 集合,query_vector 沿用第 4.1 节生成的查询向量:

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from milvus_client import MilvusCollectionClient

store = MilvusCollectionClient(
    uri="http://localhost:19530", collection_name="article_demo", vector_field="vector",
    metric_type="COSINE", output_fields=["text", "category"], timeout=30,
)
try:
    store.load()
    hits = store.search(query_vector[0].tolist(), limit=3, filter='category == "数据库"')
    for hit in hits:
        print(hit["id"], hit["distance"], hit["entity"]["text"])
    print(store.query("id == 101"))
finally:
    store.close()

MilvusCollectionClient 是配套示例定义的类,内部调用官方 MilvusClient。它只接收已经生成的向量,不绑定某个模型;search() 已去掉单条查询结果的最外层,调用方直接遍历命中列表。

需要按距离或相似度筛选结果时,向 search() 传入 threshold。COSINE、IP 保留不低于阈值的结果,L2 保留不高于阈值的结果。筛选发生在 Top-K 返回后,因此最终条数可能少于 limit;阈值需要结合实际数据确定。

评论

使用 GitHub 登录后参与讨论,评论将公开保存在 GitHub。

阅读到这里时加载评论。