本文是 refine-rag 系列教程的第八篇,我们来学习向量存储的核心技术和 Milvus 数据库的使用。
往期系列文章
目录
- 前言
- 为什么传统数据库搞不定向量搜索?
- 向量数据库横向对比
- 为什么选择 Milvus?
- 快速搭建 Milvus
- Milvus 核心概念
- 实战示例
- 学习路径
前言
前面我们学习了向量嵌入与检索,知道如何把文本转成向量、如何计算相似度。但有个问题:这些向量存在哪里?
如果只有几千条数据,存在内存里就够了。但如果有几百万、几千万条数据呢?这时候就需要专业的向量数据库。
向量数据库就像是为向量量身定制的"仓库管理系统",它不仅能存储海量向量,还能快速检索、高效管理。
为什么传统数据库搞不定向量搜索?
上一篇文章提到,向量本质上是一个浮点数数组,是机器理解文本语义的方式。
你可能会想:既然向量就是数组,那我存进 MySQL 的 JSON 字段或者直接存在内存 List 里不行吗?
答案是:小规模可以,大规模必死。
当你的知识库达到万级、百万级甚至亿级时,你会面临:
- 检索效率塌方(计算量过大) :
传统数据库查找数据是"相等判断",而向量搜索需要计算空间距离。如果你有 100 万条数据,每搜一次都要全表扫一遍并计算相似度O(n) 复杂,查询一次可能要几秒甚至几分钟。向量数据库通过 ANN(近似最近邻)算法(如 HNSW),将搜索效率提升到了毫秒级O(log n) 复杂度。 - 维度灾难(内存压力) :
一个 1536 维的向量(OpenAI 标准)看起来不大,但几百万条叠在一起,会瞬间撑爆普通的服务器内存。向量数据库专门优化了数据的压缩与加载策略。 - 工程化缺失 :
向量数据库不仅仅是"存数据",它还提供了成熟的 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}")
运行结果
加载 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': '烈焰猿, 炎猿'}
学习路径
- 简易 RAG 学习
- LCEL 语法学习
- LangChain 读取数据
- LangChain 读取文本数据
- LangChain 读取图片数据
- LangChain 读取 PDF 数据
- LangChain 读取表格数据
- 文本切块
- 向量嵌入与检索
- 向量存储 ← 当前
- 检索前处理
- 索引优化
- 检索后处理
- 响应生成
- 系统评估
项目地址
本文所有代码示例都在 GitHub 开源:
https://github.com/zonezoen/refine-rag
欢迎 Star 和 Fork,一起学习 RAG 技术!