ai

Milvus 向量数据库实践入门

本文章分享如何在 Python 项目中使用 Milvus 向量数据库进行向量存储和检索。将以实际代码示例为基础,逐步讲解 Milvus 的基本使用,包括集合创建、数据导入、索引构建和查询。示例基于网络文章数据(original_page 表),使用 llama.cpp server 作为嵌入模型推理服务。

Milvus 简介

Milvus 是一个开源的向量数据库,专为大规模向量相似性搜索而设计。它支持多种索引类型(如 HNSWIVF),并与各种嵌入模型集成。在 AI 应用中,Milvus 常用于存储和检索文本、图像等的高维向量表示。

Milvus 的优势包括:

  • 高性能的近似最近邻(ANN)搜索
  • 支持动态数据插入和删除
  • 与 PostgreSQL 等数据库的无缝集成
  • 分布式部署能力

在我们的项目中,我们使用 Milvus 来存储旅游文章的向量嵌入,实现基于内容的相似搜索。

安装和启动 Milvus

下载 Qwen3 嵌入模型

下载 Qwen3-Embedding-0.6B-Q8_0.gguf 嵌入模型

安装 huggingface hub 下载器

1
pip install huggingface_hub hf-transfer

设置环境变量

1
2
export HF_ENDPOINT=https://hf-mirror.com
export HF_HUB_ENABLE_HF_TRANSFER=1

下载模型

1
huggingface-cli download Qwen/Qwen3-Embedding-0.6B-GGUF --include Qwen3-Embedding-0.6B-Q8_0.gguf

使用 docker compose 安装

Milvus 可以通过 Docker Compose 快速启动。以下是 docker-compose.yml 的关键配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
services:
db:
build:
context: ./scripts/software/postgres
dockerfile: Dockerfile
restart: unless-stopped
env_file:
- ./scripts/.env
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- ai
ports:
- "35432:35432"
milvus:
image: milvusdb/milvus:v2.5.14
command: ["milvus", "run", "standalone"]
security_opt:
- seccomp:unconfined
environment:
MINIO_REGION: us-east-1
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
volumes:
- milvus_data:/var/lib/milvus
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
interval: 30s
start_period: 90s
timeout: 20s
retries: 3
networks:
- ai
ports:
- "19530:19530"
- "9091:9091"
depends_on:
- "etcd"
- "minio"
llama:
image: ghcr.io/ggml-org/llama.cpp:server
volumes:
- $HOME/.cache/huggingface/hub:/cache
command: >
-m /cache/models--Qwen--Qwen3-Embedding-0.6B-GGUF/snapshots/370f27d7550e0def9b39c1f16d3fbaa13aa67728/Qwen3-Embedding-0.6B-Q8_0.gguf
-b 16384
--ubatch-size 16384
--ctx-size 32768
--embeddings
--pooling mean
--host 0.0.0.0
--port 8888
ports:
- "8888:8888"
networks:
- ai
networks:
ai:
volumes:
pgdata:
milvus_data:

运行 docker-compose up -d 启动服务。Milvus 默认监听 19530 端口。

嵌入模型:llama.cpp Server

docker-compose 文件中的 llama service 是基于 llama.cpp 的嵌入模型推理服务,用于将文本转换为向量。以下是关键配置说明:

volumes: 这个配置将宿主机的 Hugging Face 缓存目录挂载到容器内的 /cache 目录。这样做的好处是:

  • 避免重复下载模型文件
  • 模型文件在宿主机和容器之间共享
  • 容器重启后模型文件仍然可用

command: 各参数说明:

  • -m: 指定模型文件路径,使用挂载的 Qwen3 嵌入模型
  • -b 16384: 设置批处理大小为 16384
  • --ubatch-size 16384: 设置微批处理大小
  • --ctx-size 32768: 设置上下文窗口大小
  • --embeddings: 启用嵌入模式(而非文本生成)
  • --pooling mean: 使用平均池化策略
  • --host 0.0.0.0: 监听所有网络接口
  • --port 8888: 服务端口

这些配置确保 llama.cpp server 以嵌入模式运行,并设置了高效处理文本向量化请求的参数。

Python 调用代码:

1
2
3
4
5
6
7
8
9
10
11
import logging
from openai import OpenAI
from openai.types import CreateEmbeddingResponse

client = OpenAI(
base_url='http://localhost:8888/v1', # 指向本地服务
api_key='no-key-required', # llama.cpp 不需要 key,但必须传值
)
def compute_embeddings(texts: list[str] | str) -> list[list[float]]:
response = client.embeddings.create(model='Qwen3-Embedding-0.6B-Q8_0', input=texts)
return [data.embedding for data in response.data]

调用 compute_embeddings 函数,这会将文本转换为 1024 维的嵌入向量。

Articles Collection

在 Milvus 中,集合(Collection)类似于数据库表。我们创建一个名为 articles 的集合,用于存储文章数据。集合 schema 基于 OriginalPage 模型(aiguide/domain/page/page_model.py):

  • id: str (主键)
  • title: str
  • content: str (Markdown 文本)
  • content_vector: vector(1024) (嵌入向量)
  • area: str (地区)
  • scenic: str (景区)
  • publish_time: int (时间戳)

创建集合的代码 Python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
schema = CollectionSchema([], check_fields=False)
schema.add_field(field_name='id', datatype=DataType.VARCHAR, max_length=256, is_primary=True, description='文章ID')
schema.add_field(field_name='title', datatype=DataType.VARCHAR, max_length=128, description='文章标题')
schema.add_field(field_name='content', datatype=DataType.VARCHAR, max_length=65535, description='文章内容')
schema.add_field(
field_name='publish_time', datatype=DataType.INT32, index_type='STL_SORT', description='发布时间,epoch seconds'
)
schema.add_field(
field_name='content_vector',
datatype=DataType.FLOAT_VECTOR,
dim=1024, # 使用 Qwen3-embedding 0.6B 模型
index_type='IVF_FLAT',
metric_type='COSINE',
description='文章内容向量',
)
schema.add_field(
field_name='area',
datatype=DataType.VARCHAR,
max_length=128,
index_type='TRIE',
description='地区/城市名称',
)
schema.add_field(
field_name='scenic',
datatype=DataType.VARCHAR,
max_length=128,
index_type='TRIE',
description='景区名称',
)

client.create_collection(collection_name=coll_name, schema=schema)

数据导入

从 PostgreSQL 的 original_page 表导入数据:查询文章,计算嵌入向量,然后插入 Milvus。这会批量导入数据,并刷新到磁盘,关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from sqlmodel import select
from ai.domain.page.page_model import OriginalPage
from ai.infra.db import with_session
from ai.infra.llm.llama.embed import compute_embeddings
from ai.infra.vdb.milvus import MILVUS_CLIENT

# pages 是 OriginalPage 列表,从 PostgreSQL 数据库加载
texts = [page.markdown.strip() for page in pages]
# 计算嵌入向量
embeddings = compute_embeddings(texts)

data = [
{
'id': page.id,
'title': page.title,
'content': page.markdown,
'publish_time': round(page.publish_time.timestamp()),
'content_vector': embedding,
'area': page.area,
'scenic': page.scenic,
}
for page, embedding in zip(pages, embeddings)
]

# 插入数据到 Milvus 的 articles 集合
client.insert(collection_name='articles', data=data)
client.flush(collection_name='articles')

上面代码使用 Python 的 for 推导式语法生成 data 列表。先通过 zippagesembeddings 合并一个 tuple[OriginalPage, list[float]] 序列,再对其进行遍历并生成 articles 集合需要的 Milvus 数据格式。

创建 Index

注意:

  1. 当前 Milvus 在创建 Collection 时并不会创建索引,需要在导入数据后手动创建索引。当索引创建完成后,后续插入数据时会自动更新索引。
  2. 数据需要加载到内存中才能进行查询

创建索引的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
index_params = MilvusClient.prepare_index_params()

# 创建向量字段索引
index_params.append(IndexParam(
field_name='content_vector',
index_name='content_vector_idx',
index_type='IVF_FLAT', # 向量索引类型,保存全是向量,提供精准搜索
metric_type='COSINE', # 使用余弦相似度
params={'nlist': 500},
))

# 为数值字段创建索引
index_params.append(IndexParam(
field_name='publish_time',
index_name='publish_time_idx',
index_type='STL_SORT',
))

# 为字符串字段创建索引(使用 TRIE 索引)
index_params.append(IndexParam(
field_name='area',
index_name='area_idx',
index_type='TRIE',
))
index_params.append(IndexParam(
field_name='scenic',
index_name='scenic_idx',
index_type='TRIE',
))

client.create_index(collection_name='articles', index_params=index_params)

VARCHAR 类型使用了 TRIE 索引,用于快速前缀搜索和检索的树形数据结构索引。

索引创建后,加载集合以启用查询。

1
client.load_collection(collection_name='articles')

进行数据查询

查询相似文章:计算查询文本的嵌入向量,然后进行搜索。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 检查集合是否存在
if not client.has_collection(collection_name=ARTICLE_COLLECTION_NAME):
print(f'集合 {ARTICLE_COLLECTION_NAME} 不存在,请先运行 examples/milvus-load-pages.py 来创建和加载数据')
exit(1)

# 检查集合是否加载
load_state = client.get_load_state(collection_name=ARTICLE_COLLECTION_NAME)
if load_state != 'LoadStateLoaded':
print(f'集合 {ARTICLE_COLLECTION_NAME} 未加载,先加载集合')
client.load_collection(collection_name=ARTICLE_COLLECTION_NAME)

embeddings = compute_embeddings(['北京市恭王府博物馆东侧院'])
result = client.search(
collection_name=ARTICLE_COLLECTION_NAME,
data=embeddings, # Milvus search 方法期望 data 是一个 vector 或 list(vector)
limit=10,
output_fields=['title', 'publish_time', 'area', 'scenic'],
)

输出的 result 示例如下:

1
2
3
4
5
6
7
data: [
[
{'id': '2:7528270196203176483', 'distance': 0.6920966506004333, 'entity': {'publish_time': 1752811980, 'area': '北京市', 'scenic': '颐和园', 'title': '北京颐和园'}},
{'id': '2:7525292299044061711', 'distance': 0.6636029481887817, 'entity': {'publish_time': 1752118860, 'area': '北京市', 'scenic': '故宫博物院', 'title': '故宫博物院百年院庆——有关故宫的十个冷知识'}},
{'id': '2:7527506587083178505', 'distance': 0.6381424069404602, 'entity': {'publish_time': 1752636540, 'area': '北京市', 'scenic': '故宫博物院', 'title': '2025年7月16日,北京故宫博物院的真实现场排队场景,大家看看吧'}},
{'id': '2:7524882070259237376', 'distance': 0.6093088388442993, 'entity': {'publish_time': 1752023100, 'area': '北京市', 'scenic': '故宫博物院', 'title': '故宫博物院上线青少网站英文版、繁体版'}}]
]

在查询时一定要指定 output_fields,否则会返回的 entity 将是一个空对象。

可以添加过滤器,如 filter='area == "北京市"' 以限制只在指定的区域进行向量搜索。

总结与最佳实践

根据我在项目的落地经验,以下是 Milvus 集成的关键要点和注意事项:

核心实践原则

  1. 性能优先架构

    • 批量插入数据时控制批次大小(建议 500-1000 条/批)
    • 使用 client.flush() 后执行 client.compact() 优化存储碎片
    • 查询时结合 filter 条件缩小搜索空间
  2. 索引生命周期管理

    • 创建索引前确保数据量 > 1 万条(小数据量时 IVF 索引效果差)
    • 定期重建索引(reindex)应对数据分布变化
    • 不同场景使用不同索引组合(如 HNSW + TRIE 联合索引)

易错点警示(含解决方案)

⚠️ 索引与查询配置失配

1
2
# 错误:创建 COSINE 索引但使用 L2 距离查询
client.search(metric_type='L2')

✅ 解决方案:保持索引 metric_type 与查询参数一致

⚠️ 未加载集合直接查询
症状:返回 collection not loaded 错误
✅ 修复流程:

1
2
if client.get_load_state('articles') != 'LOADED':
client.load_collection('articles')

⚠️ 向量维度不匹配
典型错误:1024 vs 768 维度冲突(多模型混用导致)
✅ 预防措施:

1
assert len(embedding) == schema['content_vector']['dim']

只使用必要的字段

Milvus 要求数据必需加载到内存才能查询和搜索,这对服务器(内存)资源要求较高。在实际使用中,我们可以从数据的角度对内存使用情况进行进一步优化。比如:

  1. 对于 areascenic 字段存储为对应的 ID,比如:int32, int64 类型,减少内存使用
  2. 可以去掉 title, content 这样的字符串字段,改为存储原始文本、图片等数据的引用 ID 或 URL 链接,这样也可以减少 Milvus 加载到内存中的数据
分享到