RAG 系列 7:部署 Milvus 并实现向量检索


FAISS 和 Chroma 都可以直接嵌入 Python 程序。Milvus Standalone 不同,它是一个独立运行的向量数据库服务,应用通过网络端口访问它。

这一篇会在 M1 Max 上真实启动 Milvus 2.6.9 Standalone,使用 PyMilvus 2.6.9 创建 Collection,写入本地 Qwen3 Embedding 向量,再完成主键读取、标量过滤和 COSINE Top-K 搜索。

本文只使用 Milvus 原生 MilvusClient,不使用 LangChain、VectorStore、Retriever 或 RAG。

1. Milvus 的三种部署形态

Milvus 2.6 提供三种部署方式:

形态 特点 适合场景
Lite 嵌入 Python 进程,使用本地文件 学习、快速实验、小规模数据
Standalone 单机数据库服务 本地开发、单机服务、完整功能验证
Distributed 多节点分布式集群 更大数据规模和生产负载

本篇选择 Standalone。原因不是 4 条演示数据需要三个容器,而是要完整理解数据库服务、持久化依赖、健康检查和重启恢复。若只想在 Python 进程中做小规模实验,Lite 的使用成本更低。

2. Standalone 由哪些组件组成

Milvus Standalone 部署结构

Milvus v2.6.9 的 Standalone Compose 会启动三个容器:

容器 作用
milvus-standalone 对外提供 Collection、写入、查询、索引和 WebUI
milvus-etcd 保存 Collection Schema、Segment 状态等元数据
milvus-minio 保存日志快照、标量与向量数据文件、索引文件等对象数据

本篇 Compose 还设置了 MQ_TYPE=woodpecker。Woodpecker 是 Milvus 2.6 使用的 WAL 实现,负责记录有序写入并支持故障恢复;它不是第四个独立容器,而是运行在 Milvus Standalone 内部,并复用 MinIO 对象存储。

主要端口为:

19530 -> PyMilvus 连接端口
9091  -> 健康检查和 Milvus WebUI
9000  -> MinIO API,仅供本地组件与排查使用
9001  -> MinIO Console,仅供本地排查使用

Compose 文件把这四个端口都绑定到 127.0.0.1。这是本地教学环境,不应把默认未启用鉴权的 Milvus 或使用演示账号的 MinIO 暴露到局域网和公网。

Milvus 的数据层次可以先理解为:

Database
└── Collection
    └── Partition
        └── Entity

Collection 类似表,Entity 类似一行记录。本篇使用默认 Database 和默认 Partition。每个容器的数据目录都映射到项目内的 volumes/,停止容器不会删除这些目录;只有主动删除目录或执行带清理数据的命令才会丢失演示数据。

3. 为什么创建独立的 Colima profile

本机默认 Colima 只有 6 GiB 内存,而且已经运行其他项目的多个容器。Milvus Standalone 官方要求至少准备 8 GiB 内存,因此不应该停止、重配或挤占默认实例。

这里新建名为 milvus 的独立 profile:

colima start milvus \
  --cpus 4 \
  --memory 12 \
  --disk 60 \
  --runtime docker \
  --activate=false

本机启动后的真实配置为:

PROFILE    STATUS     ARCH       CPUS    MEMORY    DISK      RUNTIME
default    Running    aarch64    4       6GiB      100GiB    docker
milvus     Running    aarch64    4       12GiB     60GiB     docker

–activate=false 不修改当前默认 Docker context。之后所有 Milvus 命令都显式指定:

docker --context colima-milvus ...

这样不会把 Milvus 容器误启动到默认 Colima,也不会停止默认实例中的现有服务。

4. 启动 Milvus Standalone

项目保存了 Milvus v2.6.9 官方 Standalone Compose 文件:

rag/p07_milvus_native/docker-compose.yml

启动服务:

cd source/_posts/llm_learning

docker --context colima-milvus compose \
  -f rag/p07_milvus_native/docker-compose.yml \
  up -d

查看容器:

docker --context colima-milvus compose \
  -f rag/p07_milvus_native/docker-compose.yml \
  ps

三个容器完全启动后的真实状态为:

NAME                STATUS                  PORTS
milvus-etcd         Up (healthy)            2379-2380/tcp
milvus-minio        Up (healthy)            127.0.0.1:9000-9001->9000-9001/tcp
milvus-standalone   Up (healthy)            127.0.0.1:9091->9091/tcp, 127.0.0.1:19530->19530/tcp

也可以直接检查 9091:

curl --noproxy '*' http://127.0.0.1:9091/healthz

返回:

OK

浏览器打开下面的地址可以查看内置管理页:

http://127.0.0.1:9091/webui/

Milvus 2.6.9 Standalone WebUI 数据组件页面

截图展示的是 Milvus 内置 WebUI。它适合观察 Collection、Segment、Channel、任务和配置等运行信息,但本文的写入与查询仍以 PyMilvus 返回结果为准,WebUI 不代替代码验证。

5. 准备 PyMilvus 环境

本篇继续使用 Chroma 文章创建的 .venv_vector_db:

source .venv_vector_db/bin/activate
python -m pip install \
  -r rag/p07_milvus_native/requirements.txt
python -m pip check

依赖中固定了:

pymilvus==2.6.9
sentence-transformers==5.1.2
transformers==4.57.6

本机 pip check 返回:

No broken requirements found.

6. 检查 Milvus 服务

代码文件为 01_check_milvus_server.py:

# 这个文件连接 Milvus Standalone,检查服务版本、数据库和 Collection。

from pymilvus import MilvusClient, __version__ as pymilvus_version


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

print(f"PyMilvus 版本:{pymilvus_version}")
print(f"Milvus 服务版本:{client.get_server_version()}")
print(f"数据库:{client.list_databases()}")
print(f"default 数据库中的 Collection:{client.list_collections()}")

在全新的 volumes/ 数据目录中第一次运行:

python rag/p07_milvus_native/01_check_milvus_server.py

真实输出为:

PyMilvus 版本:2.6.9
Milvus 服务版本:2.6.9
数据库:['default']
default 数据库中的 Collection:[]

此时服务已连接成功,但还没有创建 Collection。

7. 创建 Collection 并写入向量

写入和搜索是两条方向相反的数据链路:文档先生成向量并写入 Collection;查询文本再生成同模型、同维度的查询向量,由 Milvus 计算 COSINE 相似度并返回 Top-K。

Milvus 写入与搜索数据流

代码文件为 02_create_collection_and_insert.py:

# 这个文件创建 Milvus Collection,并写入 Qwen3 生成的向量、文本和分类字段。

from pathlib import Path

import torch
from pymilvus import MilvusClient
from sentence_transformers import SentenceTransformer


PROJECT_ROOT = Path(__file__).resolve().parents[2]
MODEL_PATH = PROJECT_ROOT / "embedding_model"
COLLECTION_NAME = "life_notes"

documents = [
    {"id": 1, "text": "西湖边有步道和树荫,很适合周末散步。", "category": "travel"},
    {"id": 2, "text": "苹果富含膳食纤维,是常见的健康水果。", "category": "food"},
    {"id": 3, "text": "汽车需要定期更换机油并检查轮胎。", "category": "car"},
    {"id": 4, "text": "杭州植物园环境安静,适合慢慢游览。", "category": "travel"},
]

device = "mps" if torch.backends.mps.is_available() else "cpu"
model = SentenceTransformer(str(MODEL_PATH), device=device)
vectors = model.encode(
    [item["text"] for item in documents],
    normalize_embeddings=True,
).tolist()

client = MilvusClient(uri="http://127.0.0.1:19530")
if client.has_collection(COLLECTION_NAME):
    client.drop_collection(COLLECTION_NAME)

# 快速创建方式会生成主键 id、1024 维 vector 字段,并允许写入动态字段。
client.create_collection(
    collection_name=COLLECTION_NAME,
    dimension=1024,
    metric_type="COSINE",
    consistency_level="Strong",
)

rows = []
for item, vector in zip(documents, vectors):
    rows.append(
        {
            "id": item["id"],
            "vector": vector,
            "text": item["text"],
            "category": item["category"],
        }
    )

result = client.upsert(collection_name=COLLECTION_NAME, data=rows)
client.flush(collection_name=COLLECTION_NAME)
stats = client.get_collection_stats(collection_name=COLLECTION_NAME)

print(f"运行设备:{device}")
print(f"Collection:{COLLECTION_NAME}")
print(f"写入数量:{result['upsert_count']}")
print(f"Collection 统计:{stats}")

7.1 快速创建 Collection

create_collection() 接收两个关键参数:

dimension=1024
metric_type="COSINE"

维度必须与 Qwen3-Embedding-0.6B 的输出一致。快速创建方式默认创建 id 主键、vector 向量字段、向量索引并加载 Collection;未在 Schema 中声明的 text、category 会被保存到预留的 $meta 动态字段中。

动态字段适合快速验证。生产项目通常会显式定义经常查询和过滤的字段类型、长度与索引,避免把重要业务结构长期隐藏在 $meta 中。

7.2 upsert、flush 和 stats

upsert -> 主键不存在时插入,存在时使用新记录覆盖原记录
flush  -> 将 growing segment 封存并刷入对象存储
stats  -> 查看 Collection 统计信息

Milvus 会自动触发 flush,正常写入也会先进入 WAL,因此不需要在每次 upsert() 后都手动调用。这里数据量很小,调用一次 flush() 是为了在教学演示中立即得到稳定的行数和 sealed segment;高频手动 flush 会制造大量小 Segment,不适合照搬到生产代码。

运行代码:

python rag/p07_milvus_native/02_create_collection_and_insert.py

真实输出为:

运行设备:mps
Collection:life_notes
写入数量:4
Collection 统计:{'row_count': 4}

8. 主键查询、标量过滤和向量搜索

代码文件为 03_search_query_and_filter.py:

# 这个文件演示 Milvus 的主键查询、标量过滤和向量 Top-K 搜索。

from pathlib import Path

import torch
from pymilvus import MilvusClient
from sentence_transformers import SentenceTransformer


PROJECT_ROOT = Path(__file__).resolve().parents[2]
MODEL_PATH = PROJECT_ROOT / "embedding_model"
COLLECTION_NAME = "life_notes"

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

print("按主键读取 id=1:")
print(client.get(collection_name=COLLECTION_NAME, ids=[1], output_fields=["text", "category"]))

print("\n标量过滤 category == travel:")
filtered_rows = client.query(
    collection_name=COLLECTION_NAME,
    filter='category == "travel"',
    output_fields=["id", "text", "category"],
)
for row in filtered_rows:
    print(row)

device = "mps" if torch.backends.mps.is_available() else "cpu"
model = SentenceTransformer(str(MODEL_PATH), device=device)
query = "周末想找一个适合散步的地方"
query_vector = model.encode(
    [query],
    prompt_name="query",
    normalize_embeddings=True,
).tolist()

results = client.search(
    collection_name=COLLECTION_NAME,
    data=query_vector,
    anns_field="vector",
    limit=3,
    output_fields=["text", "category"],
    # 搜索使用的度量必须与 Collection 的向量索引保持一致。
    search_params={"metric_type": "COSINE"},
)

print(f"\n向量查询:{query}")
for rank, hit in enumerate(results[0], start=1):
    entity = hit["entity"]
    print(
        f"{rank}. score={hit['distance']:.4f} id={hit['id']} "
        f"category={entity['category']} text={entity['text']}"
    )

这里分别使用了三个接口:

接口 用途
get() 按主键读取记录
query() 按标量表达式过滤记录
search() 按向量相似度返回 Top-K

运行结果为:

按主键读取 id=1:
data: ["{'id': 1, 'text': '西湖边有步道和树荫,很适合周末散步。', 'category': 'travel'}"], extra_info: {}

标量过滤 category == travel:
{'id': 1, 'text': '西湖边有步道和树荫,很适合周末散步。', 'category': 'travel'}
{'id': 4, 'text': '杭州植物园环境安静,适合慢慢游览。', 'category': 'travel'}

向量查询:周末想找一个适合散步的地方
1. score=0.6498 id=1 category=travel text=西湖边有步道和树荫,很适合周末散步。
2. score=0.4660 id=4 category=travel text=杭州植物园环境安静,适合慢慢游览。
3. score=0.2183 id=2 category=food text=苹果富含膳食纤维,是常见的健康水果。

PyMilvus 的命中结果仍使用 distance 作为字段名,但在 COSINE 模式下它表示余弦相似度,分数越大越接近。本文显式传入 search_params={“metric_type”: “COSINE”},避免阅读代码时误以为搜索采用了其他度量。

RAG 系列 5 的 FAISS 使用归一化向量和 IndexFlatIP。归一化向量的内积等于余弦相似度,因此这 4 条数据的排序与分数一致。这是当前度量、归一化方式和小规模精确检索共同产生的结果,并不意味着 FAISS 与 Milvus 在所有索引和参数下都必然返回完全相同的结果。

9. 验证重启后的持久化

重启三个容器:

docker --context colima-milvus compose \
  -f rag/p07_milvus_native/docker-compose.yml \
  restart

等待三个容器恢复 healthy 后重新运行检查脚本,真实结果为:

PyMilvus 版本:2.6.9
Milvus 服务版本:2.6.9
数据库:['default']
default 数据库中的 Collection:['life_notes']

再次运行搜索脚本仍返回相同的四条数据和 Top-3 排序,说明 etcd、MinIO 与 Milvus 的三个挂载目录都被保留,容器重启没有清空 Collection。

10. 查看容器资源占用

三个容器进入 healthy 后,可以记录一次资源占用:

docker --context colima-milvus stats --no-stream \
  milvus-standalone milvus-etcd milvus-minio

本文只写入 4 条数据,实测结果如下:

NAME                CPU %     MEM USAGE / LIMIT     MEM %
milvus-standalone   8.93%     304.9MiB / 11.66GiB   2.55%
milvus-etcd         1.83%     53.84MiB / 11.66GiB   0.45%
milvus-minio        0.06%     162.3MiB / 11.66GiB   1.36%

三个容器在这次采样中合计使用约 521 MiB 内存。CPU 百分比是命令执行时的瞬时值,内存也会随着数据量、索引、并发和后台任务变化,因此这组数据只用于确认本机测试环境能够正常运行,不能代替正式部署的容量规划。

11. 停止和重新启动

测试完成后只停止独立 profile:

colima stop milvus

这不会影响默认 Colima。需要再次启动独立实例时执行:

colima start milvus --activate=false

docker --context colima-milvus compose \
  -f rag/p07_milvus_native/docker-compose.yml \
  up -d

12. 常见问题

12.1 连接 19530 失败

依次检查:

colima list
docker --context colima-milvus ps
curl --noproxy '*' http://127.0.0.1:9091/healthz

只有 milvus-standalone 进入 healthy 后再运行 Python。

12.2 启动后内存不足

Milvus Standalone 不适合放进只有 6 GiB 的默认虚拟机。本篇独立 profile 分配 12 GiB。不要为了启动 Milvus 停止其他项目的容器,也不要直接调整正在承载现有服务的默认 profile。

12.3 端口被占用

检查 19530、9091、9000 和 9001:

lsof -nP -iTCP:19530 -sTCP:LISTEN
lsof -nP -iTCP:9091 -sTCP:LISTEN

12.4 Collection 维度错误

本文 Collection 是 1024 维,写入其他维度会失败。修改 Embedding 模型后,需要创建匹配的新 Collection,不能只替换模型路径。

12.5 Docker 命令操作了错误的实例

始终检查命令中是否包含:

--context colima-milvus

本篇所有 Milvus 命令都指向 colima-milvus,没有对默认 Colima 执行启动、停止或重启命令。测试期间默认实例中的三个 HBL worker 被其自身的外部机制重建,因此不能把“容器 ID 全程不变”写成 Milvus 隔离验证结果;可以确认的是 Milvus 操作没有使用默认 Docker context。

12.6 将本地端口暴露到局域网

官方 Compose 示例通常使用 19530:19530 这类端口映射,Docker 可能监听所有网卡。本篇改为 127.0.0.1:19530:19530,其余三个端口同样只绑定回环地址。若需要提供远程访问,应先配置 Milvus 鉴权、网络访问控制和 TLS,而不是直接删除 127.0.0.1。

13. 小结

检查项 结果
Milvus Server 2.6.9
PyMilvus 2.6.9
部署模式 Standalone
隔离环境 Colima milvus profile,4 CPU / 12 GiB / 60 GiB
容器 Milvus、etcd、MinIO,全部 healthy
SDK 端口 19530
端口绑定 19530、9091、9000、9001 均只监听 127.0.0.1
WebUI 与健康检查 9091
Collection life_notes
向量维度与度量 1024 / COSINE
写入数量 4
主键查询 成功
标量过滤 category == “travel” 返回 2 条
Top-K 搜索 成功,归一化向量下的排序与 RAG 系列 5 的 FAISS 案例一致
容器重启持久化 成功
本次测试内存 三个容器合计约 521 MiB
默认 Colima Milvus 流程未对其执行变更命令

到这里,FAISS、Chroma 和 Milvus 已经分别通过原生 API 完成真实向量检索。三者的核心都是“保存向量并返回相近记录”,但部署方式、元数据管理和服务边界明显不同。后续再把这些基础能力接入 LangChain 时,就能分清问题来自 Embedding、数据库还是上层框架。


文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录