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 下载器

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 列表。先通过 zippagesembeddings 合并一个 tuple[OriginalPage, list[float]] 序列,再对其进行遍历并生成 articles 集合需要的 Milvus 数据格式。

创建 Index

注意:

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

创建索引的代码如下:

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 集成的关键要点和注意事项:

核心实践原则

  1. 性能优先架构

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

    • 创建索引前确保数据量 > 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 要求数据必需加载到内存才能查询和搜索,这对服务器(内存)资源要求较高。在实际使用中,我们可以从数据的角度对内存使用情况进行进一步优化。比如:

  1. 对于 areascenic 字段存储为对应的 ID,比如:int32, int64 类型,减少内存使用
  2. 可以去掉 title, content 这样的字符串字段,改为存储原始文本、图片等数据的引用 ID 或 URL 链接,这样也可以减少 Milvus 加载到内存中的数据
相关推荐
追逐时光者2 小时前
推荐 7 款开源、免费、美观的 .NET Blazor UI 组件库
后端·.net
叫我:松哥2 小时前
基于python django深度学习的中文文本检测+识别,可以前端上传图片和后台管理图片
图像处理·人工智能·后端·python·深度学习·数据挖掘·django
程序员岳焱2 小时前
从 0 到 1:Spring Boot 与 Spring AI 打造智能客服系统(基于DeepSeek)
人工智能·后端·deepseek
mldong2 小时前
GoFrame中间件注册竟然还能这样玩?团队开发效率提升200%!
后端·架构·go
paid槮3 小时前
Python进阶第三方库之Numpy
开发语言·python·numpy
艾醒3 小时前
使用服务器训练模型详解
后端
测试19983 小时前
Jmeter如何做接口测试?
自动化测试·软件测试·python·测试工具·jmeter·测试用例·接口测试
别来无恙1493 小时前
Spring Boot自动装配原理深度解析:从核心注解到实现机制
java·spring boot·后端
Gession-杰3 小时前
OpenCV快速入门之CV宝典
人工智能·python·opencv·计算机视觉
愿你天黑有灯下雨有伞4 小时前
Spring Boot+Redis Zset:三步构建高可靠延迟队列系统
spring boot·redis·后端