04_向量数据库Milvus

向量数据库 Milvus 完全指南

面向 AI 初学者的系统性教程 | 从大模型应用开发岗位角度出发

基于课程项目 rag_examples/01_milvus_basics + milvus_config.py


目录


一、向量数据库基础

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. 开源免费,学生可自行搭建
  2. 功能完善,覆盖从入门到企业级所有场景
  3. 社区活跃,文档丰富
  4. 国产项目(中国开源的优秀代表),中文资料多

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)

这样做的好处

  1. 安全性:IP、密码不进入代码仓库
  2. 环境隔离:开发、测试、生产环境不同配置
  3. 灵活性:切换 Milvus 实例只需改 .env,无需改代码
  4. 团队协作:每个人的 .env 可以不同

2.5 Windows 注意事项

关键限制:Milvus Lite 不支持 Windows。

Milvus Lite 是一种嵌入式版本,可以直接通过 uri="milvus_demo.db" 启动一个本地文件型 Milvus 实例(类似 SQLite),但它使用了 Windows 不兼容的底层机制(mmap 相关)。

Windows 用户的解决方案

  1. 使用 Docker Desktop(推荐)

    • 安装 Docker Desktop for Windows (WSL2 backend)
    • 拉取并启动 Milvus 镜像
    • 通过 http://localhost:19530 连接
  2. 使用远程 Milvus 服务

    • 连接老师或团队搭建的共享 Milvus 实例
    • 或使用 Zilliz Cloud 的免费试用
  3. 使用 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?

  1. 明确的数据结构约束,避免数据混乱
  2. 支持更多标量字段类型和查询条件
  3. 提高标量过滤效率
  4. 代码自文档化,新成员容易理解

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,倒排文件)通过聚类来加速检索:

  1. 训练阶段:使用 K-means 算法将所有向量聚为 nlist 个簇(每个簇有一个中心点)

  2. 索引构建:为每个簇建立倒排列表,记录属于该簇的向量

  3. 搜索阶段:先找到最近的 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 = 1000

    nprobe 对性能的影响(以 nlist=1000 为例):
    nprobe=1 → 速度快,召回率约 85%
    nprobe=10 → 速度中等,召回率约 95%
    nprobe=50 → 速度较慢,召回率约 98%

5.4 HNSW:图索引

原理 :HNSW(Hierarchical Navigable Small World,分层可导航小世界)通过构建多层图结构实现高效的向量导航。

复制代码
HNSW 结构示意:

        第 2 层(顶层)
        ○────○────○         ← 稀疏连接,长距离跳跃
           ╱  │  ╲
        第 1 层(中间层)
      ○──○──○──○──○──○     ← 中等密度连接
        │  │  │  │  │
        第 0 层(底层)
    ○─○─○─○─○─○─○─○─○─○─○  ← 密集连接,包含所有节点

搜索过程

  1. 从顶层开始,快速跳到目标区域(长距离跳跃)
  2. 逐层下降,逐步缩小范围
  3. 到底层后,在目标区域附近精确搜索

这种方式类似于在 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%

关键优化策略

  1. 分区(Partition):按业务维度分区,搜索时指定分区,减少搜索范围
  2. 标量预过滤:先用标量条件过滤,再对结果做向量搜索
  3. 缓存热点数据:常用商品的向量缓存在内存中
  4. 渐进式索引:新插入数据先不建索引,批量建索引
索引性能对比汇总
索引类型 适用规模 检索延迟 召回率 内存占用 构建时间
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 支持 andorin 等复合条件:

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%' 模糊匹配(性能较差)

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 小时
优化经验
  1. 分区策略:按一级类目分区(服装、数码、家居等),搜索时先确定类目,减少搜索范围 80%
  2. 缓存策略:热门商品图片向量在应用层缓存,避免重复提取
  3. 渐进式建索引:新上架商品先不建索引(插入即可搜),每天凌晨集中重建索引
  4. 降级策略: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 篇
优化经验
  1. 分片策略:500 字/片是知识库场景的经验值,太长则语义混杂,太短则上下文不足
  2. 相似度阈值:设置 0.5-0.7 的阈值,过滤掉低质量结果,减少 LLM 幻觉
  3. 多字段索引:同时存储标题和内容的 Embedding,搜索时加权融合
  4. 标签降噪:对检索结果做去重(同一篇文档的不同片段只取最高分)

案例三: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 向量数据库篇