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 加载到内存中的数据
相关推荐
二川bro2 分钟前
AutoML自动化机器学习:Python实战指南
python·机器学习·自动化
代码or搬砖5 分钟前
SpringMVC的执行流程
java·spring boot·后端
杨超越luckly17 分钟前
基于 Overpass API 的城市电网基础设施与 POI 提取与可视化
python·数据可视化·openstreetmap·电力数据·overpass api
极光代码工作室1 小时前
基于SpringBoot的流浪狗管理系统的设计与实现
java·spring boot·后端
Rust语言中文社区1 小时前
【Rust日报】Dioxus 用起来有趣吗?
开发语言·后端·rust
小灰灰搞电子1 小时前
Rust Slint实现颜色选择器源码分享
开发语言·后端·rust
q***23571 小时前
python的sql解析库-sqlparse
数据库·python·sql
boolean的主人1 小时前
mac电脑安装nginx+php
后端
boolean的主人1 小时前
mac电脑安装运行多个php版本
后端
18你磊哥2 小时前
Django WEB 简单项目创建与结构讲解
前端·python·django·sqlite