008-01:RAG 入门-向量存储与企业级向量数据库 milvus

本文是 refine-rag 系列教程的第八篇,我们来学习向量存储的核心技术和 Milvus 数据库的使用。 本文所有代码都在:github.com/zonezoen/re...

往期系列文章

目录

  • 前言
  • 为什么传统数据库搞不定向量搜索?
  • 向量数据库横向对比
  • 为什么选择 Milvus?
  • 快速搭建 Milvus
  • Milvus 核心概念
  • 实战示例
  • 学习路径

前言

前面我们学习了向量嵌入与检索,知道如何把文本转成向量、如何计算相似度。但有个问题:这些向量存在哪里?

如果只有几千条数据,存在内存里就够了。但如果有几百万、几千万条数据呢?这时候就需要专业的向量数据库。

向量数据库就像是为向量量身定制的"仓库管理系统",它不仅能存储海量向量,还能快速检索、高效管理。

为什么传统数据库搞不定向量搜索?

上一篇文章提到,向量本质上是一个浮点数数组,是机器理解文本语义的方式。

你可能会想:既然向量就是数组,那我存进 MySQL 的 JSON 字段或者直接存在内存 List 里不行吗?

答案是:小规模可以,大规模必死。

当你的知识库达到万级、百万级甚至亿级时,你会面临:

  1. 检索效率塌方(计算量过大) : 传统数据库查找数据是"相等判断",而向量搜索需要计算空间距离。如果你有 100 万条数据,每搜一次都要全表扫一遍并计算相似度O(n) 复杂,查询一次可能要几秒甚至几分钟。向量数据库通过 ANN(近似最近邻)算法(如 HNSW),将搜索效率提升到了毫秒级O(log n) 复杂度。
  2. 维度灾难(内存压力): 一个 1536 维的向量(OpenAI 标准)看起来不大,但几百万条叠在一起,会瞬间撑爆普通的服务器内存。向量数据库专门优化了数据的压缩与加载策略。
  3. 工程化缺失: 向量数据库不仅仅是"存数据",它还提供了成熟的 CRUD、多租户隔离、数据备份和水平扩展(Scale-out)能力,这些是简单的内存库(如 FAISS)难以胜任的。

核心对比:传统数据库 vs 向量数据库

传统数据库查找的是精准的数据,而向量数据库则更适合模糊搜索。

  • 传统数据库 (MySQL)
    • 你问:"有没有编号为 1024 的商品?"他秒回。
    • 你问:"有没有长得像苹果的手机?"他直接罢工"。
  • 向量数据库 (Milvus)
    • 他会把所有数据转化为空间坐标,告诉你:"虽然我没找到完全一样的,但有几个神似的数据。"

代码层面的直观感受

传统数据库(MySQL):侧重"准"

sql 复制代码
-- 精确查询:差一个字都搜不到
SELECT * FROM products WHERE category = '手机' AND brand = '苹果';

-- 范围查询:基于确定的数值边界
SELECT * FROM products WHERE price BETWEEN 5000 AND 8000;
  • 特点:擅长精确匹配,基于 B-Tree 索引,查询条件极其明确(非黑即白)。

向量数据库(Milvus):侧重"像"

python 复制代码
# 相似度查询:基于"语义距离"
results = client.search(
    collection_name="products",
    data=[query_vector],  # 搜索"长得像苹果的手机"对应的向量
    limit=10
)
  • 特点:擅长模糊搜索,基于向量索引(HNSW、IVF),返回的是 Top-K 个"最相似"的候选者。

向量数据库横向对比

目前主流的向量数据库有很多,我们来对比一下:

主流向量数据库对比

数据库 类型 开源 部署方式 性能 生态 推荐度
Milvus 专业向量库 本地/云 5/5 5/5 5/5
Qdrant 专业向量库 本地/云 4/5 4/5 4/5
Weaviate 专业向量库 本地/云 4/5 4/5 4/5
Pinecone 云服务 仅云 5/5 4/5 3/5
Chroma 轻量级 本地 3/5 3/5 3/5
FAISS 库(非数据库) 本地 5/5 3/5 3/5
pgvector PostgreSQL 插件 本地/云 3/5 4/5 3/5

详细对比

1. Milvus

优点:

  • 性能强大,支持亿级向量
  • 索引算法丰富(HNSW、IVF、DiskANN)
  • 支持混合检索(向量 + 标量过滤)
  • 云原生架构,易扩展
  • 社区活跃,文档完善
  • 国内团队开发,中文支持好

缺点:

  • 部署稍复杂(需要 etcd、MinIO)
  • 资源占用较高

适用场景:

  • 生产环境
  • 大规模数据(建议百万级以上使用)
  • 需要高性能和稳定性

2. Qdrant

优点:

  • Rust 编写,性能优秀
  • 部署简单(单二进制文件)
  • API 设计优雅
  • 支持 payload 过滤

缺点:

  • 社区相对较小
  • 中文文档较少
  • 大规模数据性能不如 Milvus

适用场景:

  • 中小规模项目
  • 快速原型开发
  • 喜欢 Rust 生态

3. Weaviate

优点:

  • 内置 GraphQL API
  • 支持多模态搜索
  • 自带向量化模块
  • 语义搜索能力强

缺点:

  • 学习曲线陡峭
  • 资源占用高
  • 配置复杂

适用场景:

  • 知识图谱应用
  • 多模态搜索
  • 需要 GraphQL

4. Pinecone

优点:

  • 完全托管,无需运维
  • 性能优秀
  • 易用性好

缺点:

  • 闭源,不可自部署
  • 费用较高
  • 数据在国外(延迟问题)

适用场景:

  • 不想自己运维
  • 预算充足
  • 海外用户

5. Chroma

优点:

  • 极简设计,易上手
  • 轻量级,适合开发
  • 与 LangChain 集成好

缺点:

  • 性能一般
  • 不适合大规模生产
  • 功能相对简单

适用场景:

  • 学习和原型开发
  • 小规模应用(<10 万条)
  • 快速验证想法

6. FAISS

优点:

  • Facebook 出品,算法先进
  • 性能极强
  • 支持 GPU 加速

缺点:

  • 不是数据库,只是库
  • 没有持久化(需自己实现)
  • 没有分布式支持

适用场景:

  • 研究和实验
  • 需要 GPU 加速
  • 自己实现存储层

7. pgvector

优点:

  • PostgreSQL 插件,易集成
  • 利用现有 PG 生态
  • 事务支持

缺点:

  • 性能不如专业向量库
  • 索引算法有限
  • 大规模数据吃力

适用场景:

  • 已有 PostgreSQL
  • 数据量不大(<100 万)
  • 需要事务支持

为什么选择 Milvus?

综合考虑,选择 Milvus 作为本教程的向量数据库,原因如下:

1. 性能强大

python 复制代码
# Milvus 可以轻松处理亿级向量
# 查询延迟:毫秒级
# QPS:数千到数万

2. 索引算法丰富

索引类型 适用场景 性能
FLAT 小数据集,完美准确率 2/5
IVF_FLAT 中等数据集,平衡 4/5
IVF_PQ 大数据集,内存受限 4/5
HNSW 高性能查询 5/5
DiskANN 超大规模,磁盘存储 4/5

3. 功能完善

  • 混合检索(向量 + 标量过滤)
  • 分区管理(Partition)
  • 多租户支持(Database)
  • 动态字段(Dynamic Field)
  • 全文检索(BM25)
  • 范围搜索(Range Search)
  • 分组搜索(Group Search)

4. 生态完善

python 复制代码
# 与主流框架无缝集成
from langchain_milvus import Milvus
from llama_index.vector_stores import MilvusVectorStore

5. 国内支持好

  • 中文文档完善
  • 社区活跃(国内团队)
  • 国内访问速度快
  • 技术支持响应快

6. 云原生架构

复制代码
Milvus 架构:
┌─────────────────────────────────────┐
│         Milvus Coordinator          │  协调层
├─────────────────────────────────────┤
│  Query Node  │  Data Node  │ Index  │  计算层
├─────────────────────────────────────┤
│    etcd    │    MinIO    │  Pulsar │  存储层
└─────────────────────────────────────┘
  • 存储计算分离
  • 易于水平扩展
  • 高可用性

7. 面试

现在很多企业也是用的这套方案,我们学习这些不就是奔着找工作去的吗?直接朝着目标前进不好吗?

快速搭建 Milvus

Docker Compose(推荐)

文件:docker-compose.yml

yaml 复制代码
version: "3.8"

services:
  # ----------------------------------------------------------------
  # 1. etcd - 元数据存储
  # ----------------------------------------------------------------
  etcd:
    image: quay.io/coreos/etcd:v3.5.5
    container_name: milvus-etcd
    environment:
      - ETCD_AUTO_COMPACTION_MODE=revision
      - ETCD_AUTO_COMPACTION_RETENTION=1000
      - ETCD_QUOTA_BACKEND_BYTES=4294967296
      - ETCD_SNAPSHOT_COUNT=50000
    volumes:
      - ./milvus_data/etcd:/etcd
    command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
    healthcheck:
      test: ["CMD", "etcdctl", "endpoint", "health"]
      interval: 30s
      timeout: 20s
      retries: 3

  # ----------------------------------------------------------------
  # 2. MinIO - 对象存储
  # ----------------------------------------------------------------
  minio:
    image: minio/minio:RELEASE.2023-03-20T20-16-18Z
    container_name: milvus-minio
    environment:
      MINIO_ACCESS_KEY: minioadmin
      MINIO_SECRET_KEY: minioadmin
    ports:
      - "9001:9001"
      - "9000:9000"
    volumes:
      - ./milvus_data/minio:/minio_data
    command: minio server /minio_data --console-address ":9001"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 20s
      retries: 3

  # ----------------------------------------------------------------
  # 3. Milvus Standalone - 向量数据库
  # ----------------------------------------------------------------
  milvus-standalone:
    image: milvusdb/milvus:v2.5.4
    container_name: milvus-standalone
    command: ["milvus", "run", "standalone"]
    security_opt:
      - seccomp:unconfined
    environment:
      ETCD_ENDPOINTS: etcd:2379
      MINIO_ADDRESS: minio:9000
      # ----------------------------------------------------------------
      # 开启认证(设置密码)
      # ----------------------------------------------------------------
      # 取消下面两行注释即可开启用户认证
      # COMMON_SECURITY_AUTHORIZATIONENABLED: "true"  # 开启认证
      # COMMON_SECURITY_DEFAULTROOTPASSWORD: "your_password_here"  # 设置 root 用户密码
      # 
      # 开启后,Python 连接时需要提供 token:
      # client = MilvusClient(
      #     uri="http://localhost:19530",
      #     token="root:your_password_here"
      # )
    volumes:
      - ./milvus_data/milvus:/var/lib/milvus
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
      interval: 30s
      start_period: 90s
      timeout: 20s
      retries: 3
    ports:
      - "19530:19530"
      - "9091:9091"
    depends_on:
      - "etcd"
      - "minio"

启动步骤

bash 复制代码
# 1. 进入目录
cd rag/08-向量存储/

# 2. 启动所有服务
docker compose up -d

# 3. 查看服务状态
docker compose ps

# 4. 查看日志(等待启动完成)
docker compose logs -f milvus-standalone

# 看到这行说明启动成功:
# [INFO] Milvus Proxy successfully started

验证安装

python 复制代码
from pymilvus import MilvusClient

# 连接 Milvus
client = MilvusClient(uri="http://localhost:19530")

# 列出数据库
databases = client.list_databases()
print("数据库列表:", databases)  # ['default']

print("✓ Milvus 连接成功!")

停止服务

bash 复制代码
# 停止服务(保留数据)
docker compose stop

# 停止并删除容器(保留数据)
docker compose down

# 停止并删除所有数据
docker compose down -v
rm -rf milvus_data/

服务端口说明

服务 端口 说明
Milvus 19530 gRPC 接口(主要)
Milvus 9091 HTTP 监控接口
MinIO 9000 对象存储 API
MinIO 9001 Web 控制台
etcd 2379 元数据存储

访问 MinIO 控制台

浏览器打开:http://localhost:9001

  • 用户名:minioadmin
  • 密码:minioadmin

可以看到 Milvus 存储的向量数据。

Milvus 核心概念

1. Database(数据库)

类比:图书馆的不同楼层

python 复制代码
# 创建数据库
client.create_database(db_name="my_rag_system")

# 切换数据库
client.use_database(db_name="my_rag_system")

# 列出所有数据库
databases = client.list_databases()

用途:

  • 隔离不同业务
  • 多租户管理

2. Collection(集合)

类比:图书馆的书架

python 复制代码
# 创建集合(快速模式)
client.create_collection(
    collection_name="documents",
    dimension=128  # 向量维度
)

特点:

  • 必须包含向量字段
  • 类似 MySQL 的表

3. Schema(表结构)

类比:书籍的目录格式

python 复制代码
from pymilvus import CollectionSchema, FieldSchema, DataType

# 定义字段
fields = [
    FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
    FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=128),
    FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=1000),
    FieldSchema(name="source", dtype=DataType.VARCHAR, max_length=100),
]

# 创建 Schema
schema = CollectionSchema(
    fields=fields,
    description="文档集合",
    enable_dynamic_field=True  # 允许动态字段
)

# 创建集合
client.create_collection(
    collection_name="documents",
    schema=schema
)

4. Partition(分区)

类比:书架的不同层

python 复制代码
# 创建分区
client.create_partition(
    collection_name="documents",
    partition_name="tech_docs"
)

# 插入数据到指定分区
client.insert(
    collection_name="documents",
    data=[...],
    partition_name="tech_docs"
)

# 只在指定分区搜索(速度更快)
results = client.search(
    collection_name="documents",
    data=[query_vector],
    partition_names=["tech_docs"]
)

优势:

  • 提高搜索速度
  • 方便数据管理
  • 节省内存

5. Index(索引)

后续的文章会介绍如何选择索引类型和距离度量方式。

类比:图书馆的索引系统

python 复制代码
# 创建索引
index_params = client.prepare_index_params()
index_params.add_index(
    field_name="vector",
    index_type="HNSW",  # 索引类型
    metric_type="L2",   # 距离度量
    params={
        "M": 16,
        "efConstruction": 200
    }
)

client.create_index(
    collection_name="documents",
    index_params=index_params
)

常用索引:

  • FLAT:小数据集,暴力搜索
  • IVF_FLAT:中等数据集
  • HNSW:高性能
  • DiskANN:超大规模

6. Load/Release(加载/释放)

类比:从仓库搬到阅览室

python 复制代码
# 加载到内存(必须!)
client.load_collection(collection_name="documents")

# 查询(从内存读取,快)
results = client.search(...)

# 释放内存
client.release_collection(collection_name="documents")

为什么需要 load?

  • 向量搜索需要大量计算
  • 从内存读取比磁盘快 100 倍
  • 兼顾持久化和性能

实战示例

完整示例:黑神话悟空妖怪数据库

文件:a-working-sample.py

python 复制代码
import logging
import pandas as pd
from pymilvus import MilvusClient, DataType, FieldSchema, CollectionSchema
from sentence_transformers import SentenceTransformer
from tqdm import tqdm

# 隐藏日志
logging.getLogger("pymilvus").setLevel(logging.CRITICAL)

# ------------------------------------------
# 1. 准备数据
# ------------------------------------------
data_records = [
    {
        "monster_id": "BM001",
        "monster_name": "虎先锋",
        "location": "竹林关隘",
        "difficulty": "High",
        "synonyms": "猛虎妖, 虎妖",
        "description": "在竹林关卡中出现的猛虎型妖怪,力量强大。"
    },
    {
        "monster_id": "BM002",
        "monster_name": "火猿",
        "location": "火山洞窟",
        "difficulty": "Low",
        "synonyms": "烈焰猿, 炎猿",
        "description": "生活在火山洞窟的猿类妖怪,只是插科打诨的小兵。"
    },
]
df = pd.DataFrame(data_records)

# ------------------------------------------
# 2. 连接 Milvus
# ------------------------------------------
client = MilvusClient(uri="http://localhost:19530")
collection_name = "Wukong_Monsters"

# ------------------------------------------
# 3. 加载 Embedding 模型
# ------------------------------------------
print("加载 embedding 模型...")
embedding_model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
sample_embedding = embedding_model.encode(["示例文本"])[0]
vector_dim = len(sample_embedding)
print(f"向量维度: {vector_dim}")

# ------------------------------------------
# 4. 定义 Schema 并创建集合
# ------------------------------------------
fields = [
    FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
    FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=vector_dim),
    FieldSchema(name="monster_id", dtype=DataType.VARCHAR, max_length=50),
    FieldSchema(name="monster_name", dtype=DataType.VARCHAR, max_length=100),
    FieldSchema(name="location", dtype=DataType.VARCHAR, max_length=100),
    FieldSchema(name="difficulty", dtype=DataType.VARCHAR, max_length=20),
    FieldSchema(name="synonyms", dtype=DataType.VARCHAR, max_length=200),
    FieldSchema(name="description", dtype=DataType.VARCHAR, max_length=500),
]

schema = CollectionSchema(
    fields,
    description="Wukong Monsters",
    enable_dynamic_field=True
)

if not client.has_collection(collection_name):
    client.create_collection(collection_name=collection_name, schema=schema)

# ------------------------------------------
# 5. 创建索引
# ------------------------------------------
index_params = client.prepare_index_params()
index_params.add_index(
    field_name="vector",
    index_type="AUTOINDEX",
    metric_type="L2"
)
client.create_index(collection_name=collection_name, index_params=index_params)

# ------------------------------------------
# 6. 插入数据
# ------------------------------------------
for start_idx in tqdm(range(len(df)), desc="插入数据"):
    row = df.iloc[start_idx]
    
    # 准备向量文本
    doc_parts = [str(row['monster_name'])]
    if row['synonyms']:
        doc_parts.append(f"(别名:{row['synonyms']})")
    if row['location']:
        doc_parts.append(f"场景:{row['location']}")
    if row['description']:
        doc_parts.append(f"描述:{row['description']}")
    doc_text = ";".join(doc_parts)
    
    # 生成向量
    embedding = embedding_model.encode([doc_text])[0]
    
    # 插入数据
    data_to_insert = [{
        "vector": embedding.tolist(),
        "monster_id": str(row["monster_id"]),
        "monster_name": str(row["monster_name"]),
        "location": str(row["location"]),
        "difficulty": str(row["difficulty"]),
        "synonyms": str(row["synonyms"]),
        "description": str(row["description"])
    }]
    
    client.insert(collection_name=collection_name, data=data_to_insert)

# ------------------------------------------
# 7. 加载到内存
# ------------------------------------------
print("\n加载 Collection 到内存...")
client.load_collection(collection_name=collection_name)
print("✓ Collection 已加载")

# ------------------------------------------
# 8. 向量搜索
# ------------------------------------------
search_query = "高难度妖怪"
search_embedding = embedding_model.encode([search_query])[0]

search_result = client.search(
    collection_name=collection_name,
    data=[search_embedding.tolist()],
    limit=3,
    output_fields=["monster_name", "location", "difficulty", "synonyms"]
)

print(f"\n搜索结果 '{search_query}':")
for hits in search_result:
    for hit in hits:
        print(f"  - {hit['entity']}")

# ------------------------------------------
# 9. 条件查询
# ------------------------------------------
query_result = client.query(
    collection_name=collection_name,
    filter="difficulty == 'Low'",
    output_fields=["monster_name", "location", "difficulty", "synonyms"]
)

print(f"\n难度为 Low 的妖怪:")
for result in query_result:
    print(f"  - {result}")

运行结果

bash 复制代码
加载 embedding 模型...
向量维度: 512

插入数据: 100%|████████████| 2/2 [00:00<00:00, 10.5it/s]

加载 Collection 到内存...
✓ Collection 已加载

搜索结果 '高难度妖怪':
  - {'monster_name': '虎先锋', 'location': '竹林关隘', 'difficulty': 'High', 'synonyms': '猛虎妖, 虎妖'}

难度为 Low 的妖怪:
  - {'monster_name': '火猿', 'location': '火山洞窟', 'difficulty': 'Low', 'synonyms': '烈焰猿, 炎猿'}

学习路径

  1. 简易 RAG 学习
  2. LCEL 语法学习
  3. LangChain 读取数据
    1. LangChain 读取文本数据
    2. LangChain 读取图片数据
    3. LangChain 读取 PDF 数据
    4. LangChain 读取表格数据
  4. 文本切块
  5. 向量嵌入与检索
  6. 向量存储 ← 当前
  7. 检索前处理
  8. 索引优化
  9. 检索后处理
  10. 响应生成
  11. 系统评估

项目地址

本文所有代码示例都在 GitHub 开源:

github.com/zonezoen/re...

欢迎 Star 和 Fork,一起学习 RAG 技术!

复制代码
相关推荐
独自破碎E2 小时前
【面试真题拆解】Java锁机制:synchronized、ReentrantLock、锁升级、可重入锁
java·开发语言·面试
Java水解2 小时前
Spring Boot 数据仓库与ETL工具集成
spring boot·后端
Cache技术分享2 小时前
355. Java IO API -去除路径中的冗余信息
前端·后端
beata2 小时前
Spring Boot基础-3:Spring Boot 4.x 配置文件全攻略与多环境切换
spring boot·后端
Oneslide2 小时前
yum离线库制作
后端
用户7344028193422 小时前
Spring Boot 配置线程池详解,并使用@Async,执行异步方法
后端
Oneslide2 小时前
disql 会提示「输入 xxx 的值:」
后端
Oneslide2 小时前
达梦数据库开启自动提交
后端
锦木烁光2 小时前
从零构建工业级流水号引擎:Spring Boot 2.7 + Redis 原子递增 + AntD Pro 全栈实战
后端