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

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

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

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

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

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

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

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

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

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

复制代码
# 相似度查询:基于"语义距离"
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. 性能强大

复制代码
# 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. 生态完善

复制代码
# 与主流框架无缝集成
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

复制代码
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"

启动步骤

复制代码
# 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

验证安装

复制代码
from pymilvus import MilvusClient

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

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

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

停止服务

复制代码
# 停止服务(保留数据)
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(数据库)

类比:图书馆的不同楼层

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

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

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

用途:

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

2. Collection(集合)

类比:图书馆的书架

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

特点:

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

3. Schema(表结构)

类比:书籍的目录格式

复制代码
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(分区)

类比:书架的不同层

复制代码
# 创建分区
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(索引)

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

类比:图书馆的索引系统

复制代码
# 创建索引
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(加载/释放)

类比:从仓库搬到阅览室

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

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

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

为什么需要 load?

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

实战示例

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

文件:a-working-sample.py

复制代码
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': '烈焰猿, 炎猿'}
相关推荐
杨云龙UP2 小时前
Oracle Data Pump实战:expdp/impdp常用参数与导入导出命令整理_20260406
linux·运维·服务器·数据库·oracle
想唱rap3 小时前
线程池以及读写问题
服务器·数据库·c++·mysql·ubuntu
爱丽_3 小时前
B+ 树范围查询为什么快:页分裂/合并、索引设计与 SQL 写法优化
数据库·算法·哈希算法
better_liang4 小时前
每日Java面试场景题知识点之-MySQL索引
java·数据库·mysql·性能优化·索引
AgCl234 小时前
MYSQL-4-DQL数据查询语言-3/14-15
数据库·mysql
别抢我的锅包肉4 小时前
【MySQL】第五节 - 事务实战详解:从基础到并发控制(附 Navicat 可运行实验脚本)
数据库·mysql
AgCl235 小时前
MYSQL-5-DCL数据查询语言-3/16
数据库·mysql
IvorySQL5 小时前
PostgreSQL 技术日报 (4月7日)|内核开发新动态,多项功能优化落地
数据库·postgresql·开源
IvorySQL5 小时前
PostgreSQL 技术日报 (4月6日)|内核补丁与性能优化速递
数据库·postgresql·开源