本文章分享如何在 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 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 loggingfrom openai import OpenAIfrom openai.types import CreateEmbeddingResponseclient = OpenAI( base_url='http://localhost:8888/v1' , api_key='no-key-required' , ) 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 , 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 selectfrom ai.domain.page.page_model import OriginalPagefrom ai.infra.db import with_sessionfrom ai.infra.llm.llama.embed import compute_embeddingsfrom ai.infra.vdb.milvus import MILVUS_CLIENTtexts = [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) ] 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 时并不会创建索引,需要在导入数据后手动创建索引。当索引创建完成后,后续插入数据时会自动更新索引。
数据需要加载到内存中才能进行查询
创建索引的代码如下:
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' , )) 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, 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 集成的关键要点和注意事项:
核心实践原则
性能优先架构
批量插入数据时控制批次大小(建议 500-1000 条/批)
使用 client.flush()
后执行 client.compact()
优化存储碎片
查询时结合 filter
条件缩小搜索空间
索引生命周期管理
创建索引前确保数据量 > 1 万条(小数据量时 IVF 索引效果差)
定期重建索引(reindex
)应对数据分布变化
不同场景使用不同索引组合(如 HNSW + TRIE 联合索引)
易错点警示(含解决方案) ⚠️ 索引与查询配置失配
1 2 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 要求数据必需加载到内存才能查询和搜索,这对服务器(内存)资源要求较高。在实际使用中,我们可以从数据的角度对内存使用情况进行进一步优化。比如:
对于 area
和 scenic
字段存储为对应的 ID,比如:int32
, int64
类型,减少内存使用
可以去掉 title
, content
这样的字符串字段,改为存储原始文本、图片等数据的引用 ID 或 URL 链接,这样也可以减少 Milvus 加载到内存中的数据