向量数据库 Milvus 完全指南
面向 AI 初学者的系统性教程 | 从大模型应用开发岗位角度出发
基于课程项目 rag_examples/01_milvus_basics + milvus_config.py
目录
- 一、向量数据库基础
- [二、Milvus 连接与配置](#二、Milvus 连接与配置)
- [三、Collection 设计](#三、Collection 设计)
- 四、数据插入
- 五、索引机制
- 六、检索与查询
- [七、度量类型(Metric Type)](#七、度量类型(Metric Type))
- 八、企业实战案例
一、向量数据库基础
1.1 什么是向量数据库
**向量数据库(Vector Database)**是一种专门设计用于存储、管理和检索向量数据的数据库系统。要理解向量数据库,我们需要先理解两个关键概念:向量(Vector)和近似最近邻搜索(ANN, Approximate Nearest Neighbor)。
什么是向量
向量是数学中的概念,简单理解就是一组有序的数字。在 AI 领域,我们通过 Embedding 模型将文本、图像、音频等非结构化数据转换成向量。例如:
"我喜欢猫" → [0.12, -0.34, 0.56, 0.78, ..., 0.91] (1024 维)
"今天天气真好" → [0.89, 0.45, -0.12, 0.33, ..., -0.22] (1024 维)
核心思想:语义相似的内容在向量空间中距离相近,语义不同的内容距离较远。这就像图书馆里同一类别的书放在同一个书架上。
什么是 ANN(近似最近邻搜索)
ANN 是指在海量向量中快速找到与查询向量最相似的 Top-K 个向量的算法。它的核心特点是在精度和速度之间做权衡:
- 精确搜索:比较全部 N 个向量,找到最精确的 Top-K(时间复杂度 O(N),当 N 很大时极慢)
- 近似搜索:通过特殊的数据结构,以微小的精度损失换取数十倍的速度提升
为什么需要近似搜索?
假设我们有 1000 万个商品向量,每个向量 1024 维。精确搜索需要计算 1000 万次余弦相似度,每次需要遍历 1024 个浮点数 ------ 这意味着用户需要等待数百毫秒甚至数秒才能得到结果。而 ANN 可以将这个时间压缩到 10 毫秒以内,虽然在召回率上损失 1%-5%,但对用户体验来说是天壤之别。
1.2 为什么需要向量数据库
传统的关系型数据库(如 MySQL、PostgreSQL)擅长处理结构化数据 (精确匹配、范围查询),但在面对语义搜索时无能为力。
传统数据库的局限
场景:用户搜索"如何用 AI 写文章"
- 传统数据库:只能做关键词匹配,搜索 title 或 content 中包含"AI"且包含"写文章"的文档。但如果文档写的是"用人工智能辅助内容创作",虽然语义相同,关键词不匹配就搜不到。
- 向量数据库:将问题和所有文档都转成向量,计算语义相似度,能够找到"人工智能辅助内容创作"这样的语义匹配结果。
更具体的对比:
| 特性 | MySQL | Milvus |
|---|---|---|
| 精确搜索 | 支持 | 支持 |
| 模糊搜索 | LIKE(低效) | 不支持 |
| 语义搜索 | 不支持 | 核心能力 |
| 全文检索 | 需插件 | 需结合 ES |
| 向量存储 | 不支持 | 原生支持 |
| ANN 搜索 | 不支持 | 核心能力 |
| 亿级数据毫秒响应 | 不可能 | 可以 |
向量数据库在大模型 RAG 中的地位
当前大模型应用开发中,**RAG(Retrieval-Augmented Generation,检索增强生成)**是最主流的架构。它的工作流程是:
用户提问
→ 向量数据库检索相关知识
→ 将检索结果作为上下文注入 LLM
→ LLM 基于知识生成准确回答
在这个架构中,向量数据库是知识中枢,决定了 LLM 能获取到什么知识,直接影响回答质量。没有向量数据库,大模型就只能依赖自身的训练数据,存在知识过时、幻觉等问题。
1.3 主流向量数据库对比
Milvus
- 定位:开源、云原生、高性能向量数据库
- 优点:社区活跃(30K+ GitHub Stars)、功能完善、支持亿级向量、丰富的索引类型
- 缺点:部署相对复杂(需 Docker/集群),学习曲线较陡
- 适合:中大型项目、企业级应用、对性能和功能要求高的场景
Pinecone
- 定位:全托管的向量数据库 SaaS 服务
- 优点:零运维、上手快、免费额度(1 个 Pod,2GB 存储)
- 缺点:闭源、价格昂贵(生产环境每月数百美元起)、数据无法迁移
- 适合:初创团队快速验证、不想自己维护基础设施的场景
Weaviate
- 定位:开源向量数据库,强调易用性
- 优点:有 GraphQL 接口、内置模块化(可集成 OpenAI/Cohere 等)
- 缺点:性能不如 Milvus、社区相对小
- 适合:中小型项目、需要快速集成的场景
Qdrant
- 定位:Rust 编写的高性能向量数据库
- 优点:性能好、部署简单(单个二进制文件)、支持过滤
- 缺点:功能不如 Milvus 丰富、社区较小
- 适合:对性能要求高的中小型项目
选型建议
| 数据规模 | 预算 | 建议 |
|---|---|---|
| <10 万条 | 无 | 可用 FAISS(Meta 的向量检索库,非数据库) |
| 10万-1000万条 | 有运维能力 | Milvus |
| 10万-1000万条 | 想省事 | Pinecone / Qdrant Cloud |
| >1000万条 | 企业级 | Milvus 集群版 |
本课程选择 Milvus 的原因:
- 开源免费,学生可自行搭建
- 功能完善,覆盖从入门到企业级所有场景
- 社区活跃,文档丰富
- 国产项目(中国开源的优秀代表),中文资料多
1.4 Milvus 核心概念
在正式开始使用 Milvus 之前,我们需要理解几个核心概念。这些概念和关系型数据库有很好的对应关系,方便初学者理解:
| Milvus 概念 | MySQL 类比 | 说明 |
|---|---|---|
| Collection | 表(Table) | 数据容器,包含多个 Field |
| Field | 列(Column) | 数据字段,有类型和约束 |
| Entity | 行(Row) | 一条数据记录 |
| Primary Key | 主键 | 唯一标识每行数据 |
| Schema | 表结构 | 定义所有 Field 的名称和类型 |
| Index | 索引 | 加速检索的数据结构 |
| Partition | 分区表 | 将数据水平分割 |
| Database | 数据库 | 管理多个 Collection |
| Metric Type | - | 向量相似度的计算方法 |
| Vector Field | - | 存储向量的特殊字段 |
Collection(集合)
Collection 是 Milvus 中最核心的数据容器,对应关系型数据库中的"表"。一个 Collection 包含:
- 标量字段(Scalar Field):存储结构化信息,如文本内容、标题、分类、价格、浏览量等
- 向量字段(Vector Field):存储 Embedding 向量,用于语义相似度计算
每个 Collection 有一个主键 字段和向量字段,其中向量字段的维度在所有数据中必须一致。
Field(字段)
Field 定义了 Collection 中的数据结构。Milvus 支持的字段类型包括:
| 数据类型 | 说明 | 示例 |
|---|---|---|
| INT64 | 64 位整数 | 主键 ID、浏览量 |
| VARCHAR | 可变长度字符串 | 标题、内容、分类 |
| FLOAT | 浮点数 | 价格、评分 |
| BOOL | 布尔值 | 是否发布 |
| FLOAT_VECTOR | 浮点型向量 | Embedding 输出 |
| BINARY_VECTOR | 二进制向量 | 某些场景压缩用 |
Index(索引)
索引是加速向量检索的关键数据结构。没有索引时,搜索需要将查询向量与库中所有向量逐一比较(暴力搜索),当数据量达到百万级或亿级时,耗时不可接受。索引通过特定的数据结构(如聚类、图、树等),将搜索范围大幅缩小。
核心理解:向量数据库的"数据库"部分(存储、管理、标量查询)和其他数据库相似,但它的"杀手锏"是高效的向量索引和 ANN 搜索能力。
二、Milvus 连接与配置
2.1 pymilvus 客户端连接
Milvus 提供了官方的 Python SDK ------ pymilvus,通过 MilvusClient 类建立连接。安装方式:
bash
pip install pymilvus
最基本的连接方式极为简单:
python
from pymilvus import MilvusClient
# 连接本地 Milvus 服务
client = MilvusClient(uri="http://localhost:19530")
# 检查连接是否正常
version = client.get_server_version()
print(f"Milvus 版本:{version}")
MilvusClient 对象是线程安全的,通常在应用中使用单例模式,避免频繁创建和销毁连接。
2.2 URI 配置方式
Milvus 的 uri 参数支持多种连接方式:
方式一:本地 Docker 服务(推荐)
python
client = MilvusClient(uri="http://localhost:19530")
这是本课程推荐的默认方式。需要在本地通过 Docker 启动 Milvus 服务:
bash
docker run -d --name milvus \
-p 19530:19530 \
-p 9091:9091 \
milvusdb/milvus:latest
方式二:远程服务器
python
client = MilvusClient(uri="http://47.xxx.xxx.xxx:19530")
适用于团队共享一台 GPU 服务器的场景,或者连接部署在云上的 Milvus 服务。
方式三:带认证连接
python
client = MilvusClient(
uri="http://localhost:19530",
user="root",
password="Milvus" # 生产环境请修改默认密码!
)
适用于生产环境,需要权限控制的场景。
方式四:云服务连接(如 Zilliz Cloud)
python
client = MilvusClient(
uri="https://your-cluster.zillizcloud.com",
token="your-api-token" # 使用 token 而不是 user/password
)
URI 格式汇总
本地文件: milvus_demo.db (Milvus Lite,⚠️ 不支持 Windows)
HTTP 连接: http://localhost:19530 (Docker / 远程)
HTTPS 云服务: https://your-cluster.zillizcloud.com
完整带认证格式: http://user:password@host:port/dbname
2.3 数据库管理
Milvus 2.3+ 支持多数据库(Multi-Database),类似于 MySQL 中的数据库概念。
python
from pymilvus import MilvusClient
client = MilvusClient(uri="http://localhost:19530")
# 列出所有数据库
databases = client.list_databases()
print(f"当前所有数据库:{databases}")
# 创建新数据库
client.create_database("my_project_db")
# 切换到指定数据库
demo_client = MilvusClient(uri="http://localhost:19530", db_name="my_project_db")
# 或者使用 use_database() 切换
client.use_database("my_project_db")
# 删除数据库
client.drop_database("my_project_db")
重要提醒:
- 默认情况下,连接到
default数据库 - 多数据库功能仅在 Docker/远程部署版本中支持,Milvus Lite 不支持
- 不同数据库之间的 Collection 完全隔离,适用于多租户场景
2.4 配置的 .env 化(不硬编码)
大型项目开发中,硬编码 IP 地址和密码是常见的安全隐患。本项目采用环境变量管理配置:
python
# milvus_config.py
import os
from dotenv import load_dotenv
load_dotenv()
# Milvus 连接 URI(从环境变量读取,默认本地 Docker)
MILVUS_URI = os.getenv("MILVUS_URI", "http://localhost:19530")
# Milvus 数据库名(从环境变量读取)
MILVUS_DB_NAME = os.getenv("MILVUS_DB_NAME", "default")
# 其他常量
DEFAULT_DIMENSION = 1024 # Embedding 维度
DEFAULT_COLLECTION_NAME = "rag_demo" # 默认集合名
DEFAULT_METRIC_TYPE = "COSINE" # 默认度量类型
对应的 .env 文件:
env
# milvus 连接配置
MILVUS_URI=http://localhost:19530
MILVUS_DB_NAME=default
在其他文件中导入使用:
python
from rag_examples.milvus_config import MILVUS_URI, DEFAULT_DIMENSION
client = MilvusClient(uri=MILVUS_URI)
这样做的好处:
- 安全性:IP、密码不进入代码仓库
- 环境隔离:开发、测试、生产环境不同配置
- 灵活性:切换 Milvus 实例只需改 .env,无需改代码
- 团队协作:每个人的 .env 可以不同
2.5 Windows 注意事项
关键限制:Milvus Lite 不支持 Windows。
Milvus Lite 是一种嵌入式版本,可以直接通过 uri="milvus_demo.db" 启动一个本地文件型 Milvus 实例(类似 SQLite),但它使用了 Windows 不兼容的底层机制(mmap 相关)。
Windows 用户的解决方案:
-
使用 Docker Desktop(推荐)
- 安装 Docker Desktop for Windows (WSL2 backend)
- 拉取并启动 Milvus 镜像
- 通过
http://localhost:19530连接
-
使用远程 Milvus 服务
- 连接老师或团队搭建的共享 Milvus 实例
- 或使用 Zilliz Cloud 的免费试用
-
使用 WSL2(Windows Subsystem for Linux)
- 在 WSL2 中安装 Docker 和 Milvus
- 从 Windows 通过 localhost 访问
Docker 启动 Milvus 的典型命令:
bash
# 拉取 Milvus 镜像
docker pull milvusdb/milvus:latest
# 启动 Milvus(单机版)
docker run -d --name milvus \
-p 19530:19530 \
-p 9091:9091 \
-v /data/milvus:/var/lib/milvus \
milvusdb/milvus:latest
# 检查是否启动成功
docker logs milvus --tail 20
# 连接测试
python -c "from pymilvus import MilvusClient; print(MilvusClient('http://localhost:19530').get_server_version())"
2.6 健康检查
在实际项目中,启动时通常需要检查 Milvus 是否可用:
python
def check_connection():
"""检查 Milvus 连接是否正常"""
try:
client = get_milvus_client()
version = client.get_server_version()
print(f"[OK] Milvus 连接正常,版本:{version}")
return True
except Exception as e:
print(f"[ERROR] Milvus 连接失败:{e}")
return False
更全面的健康检查还可以验证:
- 集合是否存在
- 索引是否有效
- 最近的操作是否影响性能
三、Collection 设计
3.1 创建 Collection(类比建表)
在 Milvus 中,Collection 的创建有两种方式:简单创建 和自定义 Schema 创建。
方式一:简单创建
适用于快速原型开发,只需指定向量维度:
python
from pymilvus import MilvusClient
client = MilvusClient(uri=MILVUS_URI)
client.create_collection(
collection_name="simple_docs",
dimension=1024, # 向量维度,必须与 Embedding 模型一致
auto_id=True, # 自动分配主键 ID
metric_type="COSINE", # 相似度计算方式
)
这种方式会自动创建三个必要字段:
id(INT64,主键)vector(FLOAT_VECTOR,1024 维)- 预留的动态字段用于其他标量数据
方式二:自定义 Schema 创建(推荐)
适用于正式项目,明确定义所有字段:
python
from pymilvus import MilvusClient, FieldSchema, CollectionSchema, DataType
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="content", dtype=DataType.VARCHAR, max_length=65535),
FieldSchema(name="title", dtype=DataType.VARCHAR, max_length=256),
FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=64),
FieldSchema(name="views", dtype=DataType.INT64),
FieldSchema(name="price", dtype=DataType.FLOAT),
FieldSchema(name="is_published", dtype=DataType.BOOL),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=1024),
]
schema = CollectionSchema(fields=fields)
client.create_collection(
collection_name="custom_docs",
schema=schema,
metric_type="COSINE",
)
为什么推荐自定义 Schema?
- 明确的数据结构约束,避免数据混乱
- 支持更多标量字段类型和查询条件
- 提高标量过滤效率
- 代码自文档化,新成员容易理解
3.2 Schema 关键字段详解
主键字段(Primary Key)
每个 Collection 必须有一个主键字段,用于唯一标识每条数据:
python
# 自动生成 ID(推荐,无需外部管理)
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True)
# 手动指定 ID(需要外部系统 ID 同步时)
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=False)
# 字符串主键
FieldSchema(name="doc_id", dtype=DataType.VARCHAR, max_length=64, is_primary=True, auto_id=False)
向量字段
向量字段是 Milvus 的核心,存储 Embedding 向量:
python
# 单一向量(最常见)
FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=1024)
# 多向量字段(多模态场景)
FieldSchema(name="text_embedding", dtype=DataType.FLOAT_VECTOR, dim=1024)
FieldSchema(name="image_embedding", dtype=DataType.FLOAT_VECTOR, dim=512)
向量维度一致性:同一个 Collection 中,所有数据的向量维度必须相同。1024 维的向量不能插入 768 维的 Collection。
标量字段
标量字段用于存储附属信息,支持多种数据类型:
python
# 字符串
FieldSchema(name="title", dtype=DataType.VARCHAR, max_length=256)
FieldSchema(name="content", dtype=DataType.VARCHAR, max_length=65535)
FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=64)
# 整数
FieldSchema(name="views", dtype=DataType.INT64)
FieldSchema(name="likes", dtype=DataType.INT32)
# 浮点数
FieldSchema(name="price", dtype=DataType.FLOAT)
FieldSchema(name="rating", dtype=DataType.DOUBLE)
# 布尔值
FieldSchema(name="is_published", dtype=DataType.BOOL)
3.3 动态字段(Dynamic Field)- Milvus 2.3+
动态字段是 Milvus 2.3+ 引入的强大特性,允许插入数据时自动添加未在 Schema 中定义的字段。
什么场景需要动态字段?
- 数据源不统一:不同来源的数据有不同的元数据字段
- 快速迭代:产品初期字段结构不稳定,先灵活后规范
- 稀疏元数据:某些数据有特殊字段,另一些没有
如何使用动态字段
python
# 创建启用动态字段的 Collection
schema = CollectionSchema(
fields=[
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="content", dtype=DataType.VARCHAR, max_length=65535),
FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=1024),
],
enable_dynamic_field=True, # 启用动态字段
)
client.create_collection(collection_name="dynamic_docs", schema=schema)
# 插入数据时,可以带任何额外字段
data = [
{
"content": "人工智能简介",
"vector": [0.1] * 1024,
"author": "张三", # 动态字段,未在 Schema 中定义
"tags": ["AI", "科技"], # 动态字段
"publish_date": "2024-01-15", # 动态字段
},
{
"content": "机器学习基础",
"vector": [0.2] * 1024,
"author": "李四",
"course_id": 101, # 不同类型的动态字段
"difficulty": "中级",
},
]
动态字段的优缺点
| 优点 | 缺点 |
|---|---|
| 灵活性强,无需变更 Schema | 无法对动态字段建索引 |
| 快速适应业务变化 | 查询动态字段效率较低 |
| 适合数据源多样场景 | 数据模式不清晰 |
生产建议:项目初期可以用动态字段快速迭代,稳定后迁移到固定 Schema。
3.4 企业实践:电商商品向量库设计
假设我们要为一个电商平台设计商品向量库,支持以图搜图 和语义搜索。
需求分析
- 商品信息:ID、标题、描述、价格、分类、品牌、标签
- 搜索方式:用户输入"红色连衣裙"→ 语义搜索相似商品
- 筛选条件:按价格范围、分类、品牌过滤
- 数据规模:1000 万商品
Schema 设计
python
from pymilvus import FieldSchema, CollectionSchema, DataType
fields = [
# 主键:使用商品自有 ID(不从外部同步则用 auto_id)
FieldSchema(name="product_id", dtype=DataType.INT64, is_primary=True, auto_id=False),
# 文本信息
FieldSchema(name="title", dtype=DataType.VARCHAR, max_length=256),
FieldSchema(name="description", dtype=DataType.VARCHAR, max_length=65535),
# 分类与属性
FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=64),
FieldSchema(name="brand", dtype=DataType.VARCHAR, max_length=128),
FieldSchema(name="tags", dtype=DataType.VARCHAR, max_length=1024),
# 价格与状态
FieldSchema(name="price", dtype=DataType.FLOAT),
FieldSchema(name="stock", dtype=DataType.INT64),
FieldSchema(name="is_on_sale", dtype=DataType.BOOL),
# 向量字段:商品的文本向量(用于语义搜索)+ 图片向量(用于以图搜图)
FieldSchema(name="text_vector", dtype=DataType.FLOAT_VECTOR, dim=1024),
FieldSchema(name="image_vector", dtype=DataType.FLOAT_VECTOR, dim=1024),
]
schema = CollectionSchema(
fields=fields,
description="电商平台商品向量库,支持图文混合检索",
)
检索场景设计
场景一:用户搜索"适合夏天的女装"
python
# 1. 将用户查询转成向量
query_vector = embedding_model.encode("适合夏天的女装")
# 2. 向量检索 + 标量过滤(只返回在售商品)
results = client.search(
collection_name="products",
data=[query_vector],
limit=20,
filter="is_on_sale == True",
output_fields=["title", "price", "category"],
)
场景二:用户拍照搜同款(以图搜图)
python
# 1. 将用户上传的图片转成向量
image_vector = image_embedding_model.encode(uploaded_image)
# 2. 在 image_vector 字段上搜索
results = client.search(
collection_name="products",
data=[image_vector],
anns_field="image_vector", # 指定用图片向量字段搜索
limit=10,
output_fields=["title", "price", "image_url"],
)
场景三:猜你喜欢(混合检索)
python
# 结合用户的浏览历史和语义理解
query_vector = embedding_model.encode("用户最近浏览的商品特征")
user_category = "服装" # 用户常看的品类
results = client.search(
collection_name="products",
data=[query_vector],
limit=10,
filter="category == '服装' and price >= 100 and price <= 500",
output_fields=["title", "price", "category"],
)
Collection 管理关键操作
python
# 检查 Collection 是否存在
exists = client.has_collection("products")
# 查看 Collection 详情
info = client.describe_collection("products")
print(f"名称:{info['collection_name']}")
for field in info.get('fields', []):
print(f" - {field['name']}: {field['type']}")
# 获取行数
stats = client.get_collection_stats("products")
print(f"行数:{stats.get('row_count', 0)}")
# 列出所有 Collection
collections = client.list_collections()
# 删除 Collection
client.drop_collection("products")
四、数据插入
4.1 数据插入流程
向 Milvus 插入数据的基本流程有三步:
准备原始数据 → 生成 Embedding 向量 → 组装数据并插入
第一步:准备原始数据
python
documents = [
{
"content": "人工智能是模拟人类智能的计算机科学领域。",
"title": "AI 简介",
"category": "AI",
"views": 1000,
},
{
"content": "机器学习通过训练数据让计算机自动学习规律。",
"title": "ML 基础",
"category": "AI",
"views": 800,
},
]
第二步:生成 Embedding 向量
有两种方式:模拟向量 (教学用)和真实 Embedding(生产用)。
模拟向量:
python
import random
def generate_mock_embeddings(texts, dim=1024):
"""模拟 Embedding 生成(仅教学用)"""
random.seed(42)
if isinstance(texts, str):
texts = [texts]
vectors = []
for text in texts:
vector = [random.random() for _ in range(dim)]
vectors.append(vector)
return vectors
真实 Embedding(阿里云 text-embedding-v4):
python
from openai import OpenAI
def generate_real_embedding(texts):
"""使用阿里云 DashScope API 生成真实 Embedding"""
client = OpenAI(
api_key=os.getenv("ALIYUN_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
response = client.embeddings.create(
model="text-embedding-v4",
input=texts,
encoding_format="float",
)
return [item.embedding for item in response.data]
第三步:组装并插入
python
# 生成向量
texts = [d["content"] for d in documents]
vectors = generate_mock_embeddings(texts)
# 组装数据
data_to_insert = []
for i, doc in enumerate(documents):
data_to_insert.append({
"content": doc["content"],
"title": doc["title"],
"category": doc["category"],
"views": doc["views"],
"vector": vectors[i], # 向量字段名必须与 Schema 定义一致
})
# 插入数据
result = client.insert(
collection_name="my_collection",
data=data_to_insert,
)
print(f"插入数量:{result.get('insert_count', 0)}")
4.2 单条插入 vs 批量插入
Milvus 支持两种插入方式,性能差异巨大:
单条插入
python
for doc in documents:
data = {
"content": doc["content"],
"vector": generate_vector(doc["content"]),
}
client.insert(collection_name="my_collection", data=data)
问题:每条数据一次网络往返,100 万条数据就需要 100 万次网络调用。每次调用都要经过:序列化 → TCP 握手 → 数据传输 → 服务端处理 → 响应返回。网络开销远大于实际数据处理时间。
批量插入(推荐)
python
data_to_insert = []
for doc in documents:
data_to_insert.append({
"content": doc["content"],
"vector": generate_vector(doc["content"]),
})
# 一次批量插入
result = client.insert(collection_name="my_collection", data=data_to_insert)
性能差异测试(实际项目中的经验数据):
| 方式 | 1000 条数据 | 10 万条数据 | 100 万条数据 |
|---|---|---|---|
| 单条插入 | ~3 秒 | ~300 秒 | ~50 分钟 |
| 批量插入 | ~0.2 秒 | ~5 秒 | ~45 秒 |
| 提升倍数 | 15 倍 | 60 倍 | 66 倍 |
批量大小建议:
- 100-1000 条/批是最佳实践
- 太少(<10 条):网络开销占比大
- 太多(>5000 条):内存占用高,失败重试成本大
4.3 手动指定 ID vs 自动 ID
自动 ID(auto_id=True,推荐)
python
client.create_collection(
collection_name="docs",
dimension=1024,
auto_id=True, # Milvus 自动分配 ID
)
data = [{"content": "AI 简介", "vector": [0.1]*1024}]
result = client.insert(collection_name="docs", data=data)
print(f"自动分配的 ID:{result['ids']}")
适用场景:
- 数据与外部系统无关联
- 不需要自定义 ID 规则
- 简单场景,无 ID 管理需求
手动 ID(auto_id=False)
python
client.create_collection(
collection_name="docs",
dimension=1024,
auto_id=False, # 手动指定 ID
)
data = [
{"id": 1001, "content": "AI 简介", "vector": [0.1]*1024},
{"id": 1002, "content": "ML 基础", "vector": [0.2]*1024},
]
result = client.insert(collection_name="docs", data=data)
适用场景:
- 与关系型数据库同步数据(用同一套 ID)
- 需要保留原始数据的唯一标识
- 需要在 ID 上做业务关联
4.4 插入最佳实践
实践 1:插入前不建索引,插入后建索引
python
# ❌ 错误做法
client.create_collection(...)
client.create_index(...) # 先建索引
client.insert(collection_name, data) # 再插数据
# ✅ 正确做法
client.create_collection(...)
client.insert(collection_name, data) # 先插数据
client.create_index(...) # 再建索引
原因:每次插入数据后,索引都需要更新。如果先建索引再插入,每次插入都会触发索引更新,大幅降低插入性能。
实践 2:向量维度必须匹配
python
# ❌ 报错:1024 维 Collection 插入了 768 维向量
client.create_collection(collection_name="docs", dimension=1024)
data = [{"vector": [0.1] * 768}] # 维度不匹配!
client.insert(collection_name="docs", data=data) # 报错!
# ✅ 正确:维度一致
client.create_collection(collection_name="docs", dimension=1024)
data = [{"vector": [0.1] * 1024}] # 维度匹配
client.insert(collection_name="docs", data=data) # 成功
实践 3:错误处理和重试
python
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def insert_with_retry(client, collection_name, data):
"""带重试的插入"""
try:
return client.insert(collection_name=collection_name, data=data)
except Exception as e:
print(f"插入失败:{e}")
raise
实践 4:大量数据分批次插入
python
def batch_insert(client, collection_name, all_data, batch_size=500):
"""分批插入大量数据"""
total = len(all_data)
for i in range(0, total, batch_size):
batch = all_data[i:i + batch_size]
try:
result = client.insert(collection_name=collection_name, data=batch)
print(f"已插入 {min(i + batch_size, total)}/{total} 条")
except Exception as e:
print(f"批次 {i//batch_size + 1} 插入失败:{e}")
# 记录失败批次,后续可以重试
failed_batches.append(batch)
实践 5:插入后使用 flush 确保持久化
python
client.insert(collection_name="docs", data=data)
client.flush(collection_name="docs") # 确保持久化到磁盘
注意:在生产环境中,频繁 flush 会影响性能。Milvus 会定期自动持久化,通常不需要手动 flush。
五、索引机制
5.1 为什么需要索引
索引 是一种特殊的数据结构,用于加速数据检索。可以把索引理解为书的目录:
- 没有目录(无索引):要找到"向量数据库"相关内容,需要从头到尾逐页翻阅(全表扫描)
- 有目录(有索引):直接查目录,找到"向量数据库"在第 200 页(快速定位)
在向量数据库中,索引的作用更加关键。因为向量检索的本质是比较相似度,没有索引就要和所有向量逐一比较:
无索引(FLAT):比较 N 次 → 100 万条 → ~500ms
有索引(IVF_FLAT):比较 √N 次 → 100 万条 → ~50ms
有索引(HNSW):图导航几步 → 100 万条 → ~10ms
索引的核心权衡 :精度 vs 速度
没有完美的索引------所有 ANN 索引都在召回率和检索速度之间做权衡。召回率 100% 意味着速度最慢;速度最快意味着可能漏掉一些结果。
5.2 FLAT:暴力全量搜索
原理:FLAT 索引实际上不建索引,搜索时将查询向量与库中所有向量逐一计算相似度,返回 Top-K 个最相似的结果。
python
index_params = client.prepare_index_params()
index_params.add_index(
field_name="vector",
index_type="FLAT",
metric_type="COSINE",
)
client.create_index(collection_name="my_collection", index_params=index_params)
| 特性 | 说明 |
|---|---|
| 精度 | 100%(精确搜索,无近似误差) |
| 速度 | 最慢(O(N),N 为总向量数) |
| 内存占用 | 低(无需额外数据结构) |
| 适用场景 | 数据量 < 1 万条 |
适合场景:
- 数据量小(<1 万条),精确搜索即可
- 需要精确结果作为其他索引的基准测试
- 数据规模不会增长
5.3 IVF_FLAT:聚类索引
原理 :IVF(Inverted File,倒排文件)通过聚类来加速检索:
-
训练阶段:使用 K-means 算法将所有向量聚为 nlist 个簇(每个簇有一个中心点)
-
索引构建:为每个簇建立倒排列表,记录属于该簇的向量
-
搜索阶段:先找到最近的 nprobe 个簇,只在目标簇内搜索
搜索过程示意:
所有向量(100 万条)
│
▼
┌─────────────────────────┐
│ 聚类为 nlist=1000 个簇 │
│ 每个簇约 1000 个向量 │
└─────────────────────────┘
│
▼
查询向量 → 找到最近的 10 个簇(nprobe=10)
→ 只需比较 10000 个向量(而非 100 万)
→ 速度提升 100 倍
python
index_params = client.prepare_index_params()
index_params.add_index(
field_name="vector",
index_type="IVF_FLAT",
metric_type="COSINE",
params={"nlist": 100}, # 聚类数
)
client.create_index(collection_name="my_collection", index_params=index_params)
# 搜索时需要指定 nprobe
results = client.search(
collection_name="my_collection",
data=[query_vector],
limit=5,
search_params={"params": {"nprobe": 10}}, # 搜索的聚类数
)
| 特性 | 说明 |
|---|---|
| 精度 | 95%-98%(损失 2%-5%) |
| 速度 | 快(O(√N),比 FLAT 快 10-50 倍) |
| 参数 | nlist(聚类数)、nprobe(搜索聚类数) |
| 适用场景 | 1 万-100 万条数据,通用场景 |
参数调优:
-
nlist(聚类数) :推荐的默认值是
√N(N 为向量总数)。例如 10 万条数据,nlist 设为 316 左右。 -
nprobe(搜索时探查的聚类数) :推荐的默认值是
√nlist。增大 nprobe 提高召回率但降低速度。nlist 选择示例:
10,000 条 → nlist = 100
100,000 条 → nlist = 316
1,000,000 条 → nlist = 1000nprobe 对性能的影响(以 nlist=1000 为例):
nprobe=1 → 速度快,召回率约 85%
nprobe=10 → 速度中等,召回率约 95%
nprobe=50 → 速度较慢,召回率约 98%
5.4 HNSW:图索引
原理 :HNSW(Hierarchical Navigable Small World,分层可导航小世界)通过构建多层图结构实现高效的向量导航。
HNSW 结构示意:
第 2 层(顶层)
○────○────○ ← 稀疏连接,长距离跳跃
╱ │ ╲
第 1 层(中间层)
○──○──○──○──○──○ ← 中等密度连接
│ │ │ │ │
第 0 层(底层)
○─○─○─○─○─○─○─○─○─○─○ ← 密集连接,包含所有节点
搜索过程:
- 从顶层开始,快速跳到目标区域(长距离跳跃)
- 逐层下降,逐步缩小范围
- 到底层后,在目标区域附近精确搜索
这种方式类似于在 Google 地图上找一家餐厅:先看全国地图,定位到城市 → 再到城区 → 再到街道 → 精确找到目标。
python
index_params = client.prepare_index_params()
index_params.add_index(
field_name="vector",
index_type="HNSW",
metric_type="COSINE",
params={
"M": 16, # 每个节点的最大连接数
"efConstruction": 200, # 索引构建时的搜索范围
},
)
client.create_collection(
collection_name="hnsw_demo",
dimension=1024,
auto_id=True,
metric_type="COSINE",
index_params=index_params, # 创建集合时直接指定索引
)
# 搜索时需要指定 ef
results = client.search(
collection_name="hnsw_demo",
data=[query_vector],
limit=5,
search_params={"params": {"ef": 64}}, # 搜索时的搜索范围
)
| 特性 | 说明 |
|---|---|
| 精度 | 98%-99%(所有索引中最高) |
| 速度 | 最快(到百万级仅需 10ms) |
| 内存占用 | 较高(需要存储图结构) |
| 参数 | M、efConstruction、ef |
| 适用场景 | 高精度、低延迟 |
参数调优:
python
# M(每个节点的连接数)
M=16 → 内存友好,精度适中
M=32 → 精度更高,内存占用翻倍
M=64 → 极高精度,大量内存
# efConstruction(索引构建时搜索范围)
efConstruction=200 → 默认值,平衡精度和构建速度
efConstruction=400 → 精度更高,构建时间增加
# ef(搜索时搜索范围,越大越准越慢)
ef=64 → 快速,召回率约 95%
ef=128 → 中等速度,召回率约 98%
ef=256 → 较慢,召回率约 99%
HNSW vs IVF_FLAT 对比:
| 方面 | IVF_FLAT | HNSW |
|---|---|---|
| 召回率 | 95%-98% | 98%-99% |
| 搜索速度 | 50ms(100 万条) | 10ms(100 万条) |
| 索引构建速度 | 快(30 秒/百万) | 慢(60 秒/百万) |
| 内存占用 | 低(接近原始数据) | 高(额外图结构) |
| 参数数量 | 2 个 | 3 个 |
| 适用场景 | 通用场景,性价比高 | 高要求场景,不计成本 |
5.5 AUTOINDEX:自动选择
Milvus 提供 AUTOINDEX 类型,让系统根据数据量和环境自动选择最优索引:
python
index_params = client.prepare_index_params()
index_params.add_index(
field_name="vector",
index_type="AUTOINDEX",
metric_type="COSINE",
)
client.create_index(collection_name="my_collection", index_params=index_params)
适合场景:
- 不确定该用哪种索引类型
- 数据量会动态变化
- 快速原型开发
5.6 企业实践:千万级数据量的索引选型策略
场景:电商平台 5000 万商品向量库
需求分析:
- 数据量:5000 万条(持续增长)
- 延迟要求:< 30ms(用户等待时间)
- 召回率要求:> 97%
- 硬件条件:128GB 内存、16 核 CPU
索引选型建议:
| 阶段 | 数据量 | 推荐索引 | 原因 |
|---|---|---|---|
| 初期 | <100 万 | IVFFLAT | 性价比高,足够满足需求 |
| 成长期 | 100 万-1000 万 | HNSW | 精度和速度均需保障 |
| 成熟期 | 1000 万-5000 万 | IVF_PQ + 分区 | 需要压缩存储,减少内存占用 |
终极方案:多级索引架构
第 1 级:粗筛(IVF_FLAT,nlist=50000,nprobe=50)
→ 50 个簇,每簇约 1000 万条 → 比较 50 万条 → 20ms
第 2 级:精排(HNSW,在上一级结果内)
→ 对初步筛选的 1000 条精确排序 → 2ms
总共:~22ms,召回率 > 98%
关键优化策略:
- 分区(Partition):按业务维度分区,搜索时指定分区,减少搜索范围
- 标量预过滤:先用标量条件过滤,再对结果做向量搜索
- 缓存热点数据:常用商品的向量缓存在内存中
- 渐进式索引:新插入数据先不建索引,批量建索引
索引性能对比汇总
| 索引类型 | 适用规模 | 检索延迟 | 召回率 | 内存占用 | 构建时间 |
|---|---|---|---|---|---|
| FLAT | <1 万 | 500ms(100 万) | 100% | 低 | 无需 |
| IVF_FLAT | 1 万-100 万 | 50ms(100 万) | 95-98% | 较低 | 较快 |
| IVF_PQ | >100 万 | 20ms(100 万) | 90-95% | 极低(压缩) | 中等 |
| IVF_SQ8 | 内存受限场景 | 40ms(100 万) | 90-95% | 低(8bit) | 较快 |
| HNSW | 高精度场景 | 10ms(100 万) | 98-99% | 较高 | 较慢 |
| AUTOINDEX | 不确定场景 | 自动优化 | 自动优化 | 自动优化 | 自动 |
六、检索与查询
6.1 向量搜索(语义搜索)
向量搜索是 Milvus 最核心的功能,通过计算查询向量与库中向量的相似度,返回最相似的 Top-K 条结果。
基础向量搜索
python
# 将查询文本转为向量
query_text = "机器学习是什么"
query_vector = get_embedding(query_text)
# 执行向量搜索
results = client.search(
collection_name="knowledge_base",
data=[query_vector], # 查询向量(可以是多个)
limit=5, # 返回 Top-5 结果
output_fields=["content", "category"], # 返回哪些字段
)
# 解析结果
for i, hit in enumerate(results[0]):
print(f"[{i+1}] 相似度:{hit['distance']:.4f}")
print(f" 类别:{hit['entity']['category']}")
print(f" 内容:{hit['entity']['content'][:50]}")
工作原理:
用户输入:"机器学习是什么"
│
▼
Embedding 模型:text-embedding-v4
│
▼
查询向量:[0.23, -0.45, 0.67, ..., 0.12] (1024 维)
│
▼
Milvus 向量检索:
├── 计算与文档 A 的余弦相似度:0.92 ← 最相似
├── 计算与文档 B 的余弦相似度:0.87
├── 计算与文档 C 的余弦相似度:0.65
└── ...
│
▼
返回 Top-5 结果 + 相似度分数
批量向量搜索
同时传入多个查询向量,一次调用完成多次搜索:
python
query_texts = [
"人工智能的核心技术有哪些?",
"如何设计一款好用的产品?",
"Python 编程的优势是什么?"
]
query_vectors = [get_embedding(text) for text in query_texts]
results = client.search(
collection_name="knowledge_base",
data=query_vectors, # 多个查询向量
limit=3,
output_fields=["content", "category"],
)
# 每个查询向量的结果
for i, result in enumerate(results):
print(f"查询 {i+1}: {query_texts[i]}")
for hit in result:
print(f" 相似度:{hit['distance']:.4f} | {hit['entity']['content'][:40]}")
6.2 标量查询(条件过滤)
标量查询在 Milvus 中通过 query 方法实现,类似于 SQL 中的 SELECT ... WHERE。
基础标量查询
python
# 单条件过滤
results = client.query(
collection_name="articles",
filter="category == 'AI'",
output_fields=["title", "category", "views"],
limit=10,
)
# 数值范围过滤
results = client.query(
collection_name="articles",
filter="views > 800",
output_fields=["title", "views"],
limit=10,
)
# 布尔过滤
results = client.query(
collection_name="articles",
filter="is_published == True",
output_fields=["title", "is_published"],
limit=10,
)
组合条件查询
Milvus 支持 and、or、in 等复合条件:
python
# and 组合
results = client.query(
collection_name="articles",
filter="category == 'AI' and views > 600",
limit=10,
)
# or 组合
results = client.query(
collection_name="articles",
filter="category == 'AI' or category == 'Product'",
limit=10,
)
# in 包含
results = client.query(
collection_name="articles",
filter="category in ['AI', 'Data']",
limit=10,
)
# 复杂组合
results = client.query(
collection_name="articles",
filter="is_published == True and (views > 500 or price == 0)",
limit=10,
)
支持的过滤语法
| 操作符 | 示例 | 说明 |
|---|---|---|
== |
category == 'AI' |
等于 |
!= |
category != 'AI' |
不等于 |
> |
price > 100 |
大于 |
< |
price < 100 |
小于 |
>= |
views >= 500 |
大于等于 |
<= |
views <= 500 |
小于等于 |
in |
category in ['AI', 'Data'] |
包含 |
and |
a > 1 and b < 5 |
与 |
or |
a > 1 or b < 5 |
或 |
not |
not (a > 1) |
非 |
like |
title like '%AI%' |
模糊匹配(性能较差) |
6.3 Search 参数详解
client.search() 是 Milvus 中最核心的方法,掌握其参数是高效使用 Milvus 的关键。
python
results = client.search(
collection_name="articles", # 必须:Collection 名称
data=[query_vector], # 必须:查询向量列表
anns_field="vector", # 可选:向量字段名(默认 "vector")
limit=10, # 可选:返回 Top-K 结果数(默认 10)
offset=5, # 可选:偏移量(用于分页)
output_fields=["title", "content"], # 可选:返回的字段列表(默认只返回 ID)
filter="category == 'AI'", # 可选:标量过滤条件
search_params={"params": {"nprobe": 10}}, # 可选:索引特定参数
timeout=30, # 可选:超时时间(秒)
)
各参数详解
collection_name(必须)
要搜索的 Collection 名称,必须先创建且存在。
data(必须)
查询向量列表,可以是一个或多个向量:
python
# 单向量查询
data=[query_vector]
# 多向量批量查询
data=[vector1, vector2, vector3]
# 每个向量的维度必须与 Collection 定义一致
anns_field(可选)
指定在哪个向量字段上搜索。对于有多个向量字段的 Collection(如文本向量 + 图片向量),此参数指定用哪个字段搜索:
python
results = client.search(
data=[image_vector],
anns_field="image_vector", # 在图片向量字段上搜索
...
)
limit(可选)
返回的 Top-K 结果数量。注意:这里的 limit 是最终结果数,不是候选数(索引内部会有不同的候选量)。
python
limit=5 # 返回 Top-5
limit=20 # 返回 Top-20
# 建议范围:5-20
# 太少 → 可能遗漏相关信息
# 太多 → 增加 LLM 上下文处理负担
offset(可选)
结果偏移量,用于分页:
python
# 第一页:1-10
results_page1 = client.search(limit=10)
# 第二页:11-20
results_page2 = client.search(limit=10, offset=10)
# 第三页:21-30
results_page3 = client.search(limit=10, offset=20)
output_fields(可选)
指定返回结果中需要包含哪些标量字段:
python
# 返回所有字段(注意:不包含向量字段)
output_fields=["*"]
# 只返回特定字段(推荐,减少网络传输)
output_fields=["title", "content", "category"]
# 不指定则只返回 ID
filter(可选)
在向量检索的结果上应用标量条件过滤:
python
# 语义搜索"AI 教程",但只返回免费的
results = client.search(
data=[query_vector],
filter="category == 'AI' and price == 0",
...
)
# 注意:filter 的执行时机由 Milvus 决定
# 可能是在向量搜索前过滤,也可能是在向量搜索后过滤
# 具体行为取决于集合配置和索引类型
search_params(可选)
索引类型的特定参数:
python
# IVF_FLAT 索引
search_params = {"params": {"nprobe": 10}}
# HNSW 索引
search_params = {"params": {"ef": 64}}
# 默认参数(通常够用)
search_params = {}
timeout(可选)
请求超时时间:
python
timeout=10 # 10 秒超时
timeout=None # 不超时(默认)
6.4 向量检索 + 标量过滤的混合查询
在实际的 RAG 系统中,向量搜索 + 标量过滤是最常见的查询模式:
python
def search_knowledge_base(query_text, category=None, min_views=0, top_k=5):
"""带标量过滤的向量检索"""
# 1. 生成查询向量
query_vector = get_embedding(query_text)
# 2. 构建过滤条件
filter_parts = []
if category:
filter_parts.append(f"category == '{category}'")
if min_views > 0:
filter_parts.append(f"views >= {min_views}")
filter_expr = " and ".join(filter_parts) if filter_parts else ""
# 3. 执行混合检索
results = client.search(
collection_name="knowledge_base",
data=[query_vector],
limit=top_k,
filter=filter_expr,
output_fields=["title", "content", "category"],
)
# 4. 解析结果
contexts = []
for hit in results[0]:
contexts.append({
"title": hit["entity"]["title"],
"content": hit["entity"]["content"],
"score": hit["distance"],
})
return contexts
# 使用示例
results = search_knowledge_base(
query_text="如何用 AI 写文章",
category="AI",
min_views=100,
top_k=5,
)
七、度量类型(Metric Type)
7.1 什么是度量类型
**度量类型(Metric Type)**定义了如何计算两个向量之间的"相似度"或"距离"。选择正确的度量类型对检索效果至关重要。
7.2 COSINE(余弦距离)
公式 :cos(θ) = (A·B) / (||A|| × ||B||)
A
│
│ θ
────────┴───────
B
cos(θ) = 1 → 方向完全相同(最相似)
cos(θ) = 0 → 方向垂直(不相关)
cos(θ) = -1 → 方向完全相反(最不相似)
特点:
- 范围:-1, 1,值越大越相似
- 不受向量长度影响,只关注方向(语义方向)
- 是最常用的度量类型,尤其适合语义检索
为什么 COSINE 最适合文本语义搜索?
python
# 示例:两个语义相同但长度不同的向量
A = [1, 2, 3] # "人工智能"的向量
B = [2, 4, 6] # 同一文本的不同表示
# COSINE:关注方向,所以完全相同
cosine(A, B) = 1.0 # 方向相同,完全相似
# L2:关注绝对距离,所以不同
l2(A, B) = √14 ≈ 3.74 # 距离不为 0
推荐设置:
python
DEFAULT_METRIC_TYPE = "COSINE" # milvus_config.py 中的默认值
7.3 L2(欧氏距离)
公式 :L2(A, B) = √(Σ(Ai - Bi)²)
B (2, 4)
│
│ 距离
│ /
│ /
A (1, 2)
欧氏距离 = √((2-1)² + (4-2)²) = √5 = 2.236
特点:
- 范围:[0, +∞),值越小越相似
- 关注绝对位置,考虑向量的大小和方向
- 适用于:图像搜索、向量未归一化的场景
典型应用:
python
# 图像搜索场景用 L2
client.create_collection(
collection_name="images",
dimension=512,
metric_type="L2",
)
# 结果中 distance 越小越相似
results = client.search(data=[image_vector], limit=5)
for hit in results[0]:
print(f"L2 距离:{hit['distance']:.4f}") # 越小越相似
7.4 IP(内积)
公式 :IP(A, B) = Σ(Ai × Bi)
IP = 向量 A 在向量 B 方向上的投影长度 × B 的长度
= |A| × |B| × cos(θ)
特点:
- 范围:(-∞, +∞),值越大越相似
- 同时考虑大小和方向
- 当向量归一化后(长度=1),IP 等价于 COSINE
python
# 当向量已归一化时,IP 和 COSINE 结果一致
import numpy as np
def normalize(v):
norm = np.linalg.norm(v)
return v / norm
A = normalize([1, 2, 3])
B = normalize([2, 4, 6])
# 归一化后 IP = COSINE
ip = np.dot(A, B) # 0.9999...
cosine = np.dot(A, B) # 0.9999...(归一化后与 IP 相同)
7.5 选型建议
| 场景 | 推荐度量类型 | 原因 |
|---|---|---|
| 文本语义搜索(RAG 系统) | COSINE | 关注语义方向,不受文本长度影响 |
| 图像相似度搜索 | L2 | 关注颜色/纹理的绝对特征 |
| 多模态检索(图文混合) | COSINE | 通用性好 |
| 推荐系统(用户向量 vs 商品向量) | IP | 同时关心方向和大小 |
| 人脸识别 | L2 | 需要精确的距离度量 |
| 异常检测 | L2 | 异常点通常距离较远 |
最佳实践:
python
# 对于文本 Embedding,99% 的情况用 COSINE
DEFAULT_METRIC_TYPE = "COSINE"
# 如果使用 OpenAI Embedding(text-embedding-3-small)
# 建议使用 COSINE(OpenAI 官方推荐)
# 如果使用 BGE 系列模型
# 建议使用 COSINE(BGE 官方推荐)
# 通用原则:不确定时,先用 COSINE 试试
误解澄清:
❌ 误解:L2 一定比 COSINE 差
✅ 正确:没有绝对好坏,取决于场景和 Embedding 模型
❌ 误解:所有 Embedding 模型都应该用 COSINE
✅ 正确:不同模型有不同推荐。用之前先看模型文档
❌ 误解:COSINE 和 IP 完全不同
✅ 正确:向量归一化后,两者完全等价
八、企业实战案例
案例一:电商平台的以图搜图系统
背景
某电商平台月活用户 5000 万,日均搜索量 3000 万次,商品 SKU 数量 2000 万。用户上传一张图片(或拍照),系统自动找到相似商品推荐给用户。
技术架构
用户上传图片
│
▼
图片预处理(缩放、裁剪、归一化)
│
▼
图像 Embedding 模型(ResNet50/ViT)
│ 输出 1024 维向量
▼
Milvus 向量检索(HNSW 索引)
│ 检索 Top-50 相似商品
▼
精排模型(融合价格、销量、用户偏好)
│
▼
返回 Top-10 推荐结果
核心实现
python
# 1. 创建商品向量集合
fields = [
FieldSchema(name="product_id", dtype=DataType.INT64, is_primary=True, auto_id=False),
FieldSchema(name="title", dtype=DataType.VARCHAR, max_length=256),
FieldSchema(name="price", dtype=DataType.FLOAT),
FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=64),
FieldSchema(name="sales_count", dtype=DataType.INT64),
FieldSchema(name="image_vector", dtype=DataType.FLOAT_VECTOR, dim=1024),
]
schema = CollectionSchema(fields=fields, description="商品以图搜图索引")
client.create_collection(
collection_name="products_image",
schema=schema,
metric_type="L2", # 图像搜索用 L2 距离
)
# 2. 创建 HNSW 索引(高精度、低延迟)
index_params = client.prepare_index_params()
index_params.add_index(
field_name="image_vector",
index_type="HNSW",
metric_type="L2",
params={"M": 32, "efConstruction": 200},
)
client.create_index(collection_name="products_image", index_params=index_params)
# 3. 搜索接口
def search_similar_products(uploaded_image, top_k=10, category=None):
# 提取图片向量
image_vector = extract_image_embedding(uploaded_image)
# 构造过滤条件
filter_expr = ""
if category:
filter_expr = f"category == '{category}'"
# 向量检索
results = client.search(
collection_name="products_image",
data=[image_vector],
anns_field="image_vector",
limit=top_k * 5, # 先召回多量,再精排
filter=filter_expr,
output_fields=["product_id", "title", "price", "sales_count"],
search_params={"params": {"ef": 128}},
)
# 精排(融合销量、价格等因素)
candidates = []
for hit in results[0]:
candidates.append({
"product_id": hit["entity"]["product_id"],
"title": hit["entity"]["title"],
"price": hit["entity"]["price"],
"similarity": hit["distance"],
"sales_count": hit["entity"]["sales_count"],
"final_score": hit["distance"] * 0.7 +
normalize_sales(hit["entity"]["sales_count"]) * 0.3,
})
# 按综合得分排序
candidates.sort(key=lambda x: x["final_score"], reverse=True)
return candidates[:top_k]
性能指标
| 指标 | 数值 |
|---|---|
| 商品总数 | 2000 万 |
| 索引类型 | HNSW(M=32, efConstruction=200) |
| 单次搜索延迟 | < 20ms |
| 召回率(Top-10) | > 95% |
| QPS(每秒查询量) | > 5000 |
| 索引构建时间 | ~2 小时 |
优化经验
- 分区策略:按一级类目分区(服装、数码、家居等),搜索时先确定类目,减少搜索范围 80%
- 缓存策略:热门商品图片向量在应用层缓存,避免重复提取
- 渐进式建索引:新上架商品先不建索引(插入即可搜),每天凌晨集中重建索引
- 降级策略:Milvus 不可用时,回退到标签搜索(按分类 + 品牌 + 价格范围)
案例二:知识库的语义检索系统
背景
某企业内部知识管理系统,包含 500 万篇文档(技术文档、设计方案、会议纪要、项目总结等),需要支持员工通过自然语言找到相关知识。
技术架构
用户提问:"如何搭建微服务架构?"
│
▼
文本 Embedding(text-embedding-v4)
│ 输出 1024 维向量
▼
Milvus 向量检索(IVF_FLAT 索引)
│ 返回 Top-10 相似文档
▼
Rerank 重排序(Cross-encoder)
│ 精排取 Top-3
▼
LLM 生成答案(Qwen-Plus)
│
▼
返回带引用的回答
文档处理的完整流程
python
# 1. 文档分片
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 每片 500 字
chunk_overlap=50, # 重叠 50 字(保持上下文连贯)
separators=["\n\n", "\n", "。", "!", "?", ","],
)
# 2. 向量化并插入
def index_documents(documents):
"""将一批文档索引到 Milvus"""
data_to_insert = []
for doc in documents:
# 分片
chunks = text_splitter.split_text(doc["content"])
for chunk in chunks:
# 生成向量
vector = get_embedding(chunk)
data_to_insert.append({
"doc_id": doc["id"],
"chunk_text": chunk,
"title": doc["title"],
"author": doc["author"],
"department": doc["department"],
"create_time": doc["create_time"],
"vector": vector,
})
# 批量插入
client.insert(collection_name="knowledge_base", data=data_to_insert)
return len(data_to_insert)
# 3. 检索 + 回答
def rag_query(question, top_k=5):
"""RAG 问答"""
# Step 1: 向量检索
question_vector = get_embedding(question)
results = client.search(
collection_name="knowledge_base",
data=[question_vector],
limit=top_k,
output_fields=["chunk_text", "title", "doc_id"],
filter="department == '技术部'", # 只检索技术部门文档
)
# Step 2: 拼接上下文
contexts = []
for hit in results[0]:
if hit["distance"] > 0.6: # 设置相似度阈值,过滤噪声
contexts.append(hit["entity"]["chunk_text"])
context_text = "\n\n".join(contexts)
# Step 3: LLM 生成回答
response = llm_client.chat(
model="qwen-plus",
messages=[
{"role": "system", "content": "请基于以下知识回答用户问题。如果你不知道答案,请明确说明。"},
{"role": "user", "content": f"知识:{context_text}\n\n问题:{question}"},
]
)
return response["choices"][0]["message"]["content"]
性能指标
| 指标 | 数值 |
|---|---|
| 文档数量 | 500 万 |
| 分片数 | ~2000 万(每篇约 4 片) |
| 索引类型 | IVF_FLAT(nlist=4096) |
| 单次检索延迟 | ~30ms |
| 检索召回率(Top-10) | ~97% |
| 每日新增 | ~5000 篇 |
优化经验
- 分片策略:500 字/片是知识库场景的经验值,太长则语义混杂,太短则上下文不足
- 相似度阈值:设置 0.5-0.7 的阈值,过滤掉低质量结果,减少 LLM 幻觉
- 多字段索引:同时存储标题和内容的 Embedding,搜索时加权融合
- 标签降噪:对检索结果做去重(同一篇文档的不同片段只取最高分)
案例三:Milvus 性能优化实战
1. 索引优化
python
# 不同数据量的索引配置策略
# 数据量 < 1 万:FLAT(精确搜索)
index_params = {"index_type": "FLAT", "metric_type": "COSINE"}
# 数据量 1 万-100 万:IVF_FLAT(平衡速度和精度)
# nlist = √N = √100000 ≈ 316, 取整为 128
index_params = {
"index_type": "IVF_FLAT",
"metric_type": "COSINE",
"params": {"nlist": 128}, # nlist 越大越精确但越慢
}
# 数据量 > 100 万:IVF_PQ(压缩存储)
# 或 HNSW(高精度,需更多内存)
index_params = {
"index_type": "HNSW",
"metric_type": "COSINE",
"params": {"M": 32, "efConstruction": 200},
}
2. 搜索参数调优
python
# IVF_FLAT 搜索参数
search_params = {"params": {"nprobe": 10}}
# nprobe 越大,召回率越高,速度越慢
# nprobe = √nlist 是推荐的起点
# HNSW 搜索参数
search_params = {"params": {"ef": 64}}
# ef 越大,召回率越高,速度越慢
# ef = limit × 10 是推荐的起点
3. 连接池优化
python
# 不要每次请求都创建新连接
# ❌ 错误做法
def search(query):
client = MilvusClient(uri=MILVUS_URI) # 每次创建连接
return client.search(...)
# ✅ 正确做法:使用全局单例
_client = None
def get_client():
global _client
if _client is None:
_client = MilvusClient(uri=MILVUS_URI)
return _client
def search(query):
return get_client().search(...)
4. 批量操作优化
python
# 大批量插入时:
# 1. 先关闭自动 flush
# 2. 插入完成后再手动 flush
client.insert(collection_name="large_collection", data=data_batch_1)
client.insert(collection_name="large_collection", data=data_batch_2)
# ...
# 全部插入完成后,手动 flush
client.flush(collection_name="large_collection")
# 最后再创建索引
client.create_index(collection_name="large_collection", index_params=index_params)
5. 标量过滤优化
python
# 性能提示:复杂标量过滤会降低向量检索速度
# ❌ 低效:复杂的字符串匹配
filter="title like '%人工智能%' or content like '%机器学习%'"
# ✅ 高效:使用独立的标签/分类字段
filter="category == 'AI'"
# ✅ 更高效:先用向量检索,再用标量过滤
6. 监控与调优
python
# 常用监控指标
def monitor_milvus_performance(collection_name):
"""监控 Milvus 性能"""
client = MilvusClient(uri=MILVUS_URI)
# 1. 查看集合统计
stats = client.get_collection_stats(collection_name)
print(f"行数:{stats.get('row_count', 0)}")
# 2. 查看索引状态
indexes = client.list_indexes(collection_name)
for index in indexes:
info = client.describe_index(collection_name, index)
print(f"索引 {index}: {info}")
# 3. 加载状态
# Milvus 2.4+ 有 HTTP 监控端口(9091)
# curl http://localhost:9091/metrics
return stats
关键性能数据总结
| 优化项 | 优化前 | 优化后 | 提升倍数 |
|---|---|---|---|
| 单条 vs 批量插入 | ~300 秒(10 万条) | ~5 秒(10 万条) | 60 倍 |
| 无索引 vs HNSW | ~500ms | ~10ms | 50 倍 |
| 无缓存 vs 连接池 | 每次~50ms | 首次~50ms | 显著 |
| 无分区 vs 分区 | 全量搜索 | 1/10 数据量 | 10 倍 |
附录
A. 本项目的完整工作流程
1. 连接 Milvus
python rag_examples/01_milvus_basics/01_connect_milvus.py
2. 创建 Collection
python rag_examples/01_milvus_basics/02_create_collection.py
3. 插入数据
python rag_examples/01_milvus_basics/03_insert_data.py
4. 创建索引
python rag_examples/01_milvus_basics/04_create_index.py
B. milvus_config.py 的核心配置
python
# Milvus 连接 URI
MILVUS_URI = os.getenv("MILVUS_URI", "http://localhost:19530")
# 数据库名
MILVUS_DB_NAME = os.getenv("MILVUS_DB_NAME", "default")
# Embedding 维度(与 text-embedding-v4 一致)
DEFAULT_DIMENSION = 1024
# 默认度量类型
DEFAULT_METRIC_TYPE = "COSINE"
# 默认集合名
DEFAULT_COLLECTION_NAME = "rag_demo"
C. 常见问题排查
Q: 连接 Milvus 报错 "connection refused"
A: 检查 Docker 是否在运行:docker ps | grep milvus。如果没有启动,执行 docker compose up -d,等待 15 秒后再试。
Q: 插入数据时维度不匹配报错
A: 检查 DEFAULT_DIMENSION 是否为 1024,与 Embedding 模型输出维度一致。1024 维向量不能插入 768 维的 Collection。
Q: Collection 创建后查询返回 0 条数据
A: 确认是否执行了 insert 操作。Collection 创建后是空的,需要插入数据才有内容。
Q: 创建索引需要多久
A: 少量数据(几百条)几乎瞬间完成。百万级数据需要几秒到几分钟,取决于索引类型和硬件性能。
Q: 在 Windows 上无法使用 Milvus Lite
A: Milvus Lite 不支持 Windows。解决方案:(1) 使用 Docker Desktop;(2) 连接远程 Milvus 服务;(3) 使用 WSL2 安装 Docker。
作者 :基于课程项目 rag_examples 总结
版本 :1.0
最后更新 :2026-06-24
适用课程:RAG 检索增强生成 - Milvus 向量数据库篇