本文章分享如何在 Python 项目中使用 Milvus 向量数据库进行向量存储和检索。将以实际代码示例为基础,逐步讲解 Milvus 的基本使用,包括集合创建、数据导入、索引构建和查询。示例基于网络文章数据(original_page
表),使用 llama.cpp server 作为嵌入模型推理服务。
Milvus 简介
Milvus 是一个开源的向量数据库,专为大规模向量相似性搜索而设计。它支持多种索引类型(如 HNSW
、IVF
),并与各种嵌入模型集成。在 AI 应用中,Milvus 常用于存储和检索文本、图像等的高维向量表示。
Milvus 的优势包括:
- 高性能的近似最近邻(ANN)搜索
- 支持动态数据插入和删除
- 与 PostgreSQL 等数据库的无缝集成
- 分布式部署能力
在我们的项目中,我们使用 Milvus 来存储旅游文章的向量嵌入,实现基于内容的相似搜索。
安装和启动 Milvus
下载 Qwen3 嵌入模型
下载 Qwen3-Embedding-0.6B-Q8_0.gguf 嵌入模型
安装 huggingface 下载器
shell
pip install huggingface_hub hf-transfer
设置环境变量
shell
export HF_ENDPOINT=https://hf-mirror.com
export HF_HUB_ENABLE_HF_TRANSFER=1
下载模型
shell
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 的关键配置:
yaml
services:
db:
build:
context: ./scripts/software/postgres
dockerfile: Dockerfile
restart: unless-stopped
env_file:
- ./scripts/.env
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- aiguide
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:
- aiguide
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:
- aiguide
networks:
aiguide:
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 调用代码:
python
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:
python
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。这会批量导入数据,并刷新到磁盘,关键代码如下:
python
from sqlmodel import select
from aiguide.domain.page.page_model import OriginalPage
from aiguide.infra.db import with_session
from aiguide.infra.llm.ollama.embed import compute_embeddings
from aiguide.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
列表。先通过 zip
将 pages
和 embeddings
合并一个 tuple[OriginalPage, list[float]]
序列,再对其进行遍历并生成 articles
集合需要的 Milvus 数据格式。
创建 Index
注意:
- 当前 Milvus 在创建 Collection 时并不会创建索引,需要在导入数据后手动创建索引。当索引创建完成后,后续插入数据时会自动更新索引。
- 数据需要加载到内存中才能进行查询
创建索引的代码如下:
python
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': 1024},
))
# 为数值字段创建索引
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
索引,用于快速前缀搜索和检索的树形数据结构索引。
索引创建后,加载集合以启用查询。
python
client.load_collection(collection_name='articles')
进行数据查询
查询相似文章:计算查询文本的嵌入向量,然后进行搜索。示例代码:
python
# 检查集合是否存在
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
示例如下:
python
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 集成的关键要点和注意事项:
核心实践原则
-
性能优先架构
- 批量插入数据时控制批次大小(建议 500-1000 条/批)
- 使用
client.flush()
后执行client.compact()
优化存储碎片 - 查询时结合
filter
条件缩小搜索空间
-
索引生命周期管理
- 创建索引前确保数据量 > 1 万条(小数据量时 IVF 索引效果差)
- 定期重建索引(
reindex
)应对数据分布变化 - 不同场景使用不同索引组合(如 HNSW + TRIE 联合索引)
易错点警示(含解决方案)
⚠️ 索引与查询配置失配
python
# 错误:创建 COSINE 索引但使用 L2 距离查询
client.search(metric_type='L2')
✅ 解决方案:保持索引 metric_type 与查询参数一致
⚠️ 未加载集合直接查询 症状:返回 collection not loaded
错误 ✅ 修复流程:
python
if client.get_load_state('articles') != 'LOADED':
client.load_collection('articles')
⚠️ 向量维度不匹配 典型错误:1024 vs 768
维度冲突(多模型混用导致) ✅ 预防措施:
python
assert len(embedding) == schema['content_vector']['dim']
最后
Milvus 要求数据必需加载到内存才能查询和搜索,这对服务器(内存)资源要求较高。在实际使用中,我们可以从数据的角度对内存使用情况进行进一步优化。比如:
- 对于
area
和scenic
字段存储为对应的 ID,比如:int32
,int64
类型,减少内存使用 - 可以去掉
title
,content
这样的字符串字段,改为存储原始文本、图片等数据的引用 ID 或 URL 链接,这样也可以减少 Milvus 加载到内存中的数据