RAG 向量数据库设计指南:从入门到生产

做 RAG 系统,向量数据库是绕不开的一环。这篇文章从基础概念到生产调优,把向量数据库的设计原理、结构设计、索引选型、检索策略、参数调优讲清楚。


先说结论

如果你只记住三句话:

  1. 「关系库管数据,向量库管检索」------双存储、CQRS,别把所有字段都往向量库塞。

  2. 「HNSW 是 99% RAG 场景的最优索引」------别纠结了,先用它。

  3. 「混合检索 + Rerank 才是生产标配」------单靠语义或关键词都有致命盲区。

结论放这了,下面聊"为什么"和"怎么做"。


一、向量数据库是什么

一句话解释

向量数据库是专门存"一串数字"并快速找到"最像的那几串"的数据库。

在 RAG 场景里:文本 → Embedding 模型 → 变成一串浮点数(向量)→ 存进去。用户提问时,问题也变成向量,然后找语义最接近的文本片段。

和关系库的概念映射

关系型数据库 向量数据库 说明
Database Database 数据库实例
Table Collection 数据集合
Row Entity / Point 一条记录(向量 + 元数据)
Column Field 字段(向量字段或标量字段)
Index (B+ 树) Vector Index (HNSW/IVF) 底层结构完全不同
Partition Partition 分区,减少搜索范围

为什么 RAG 非要用向量库?

看个对比:

go 复制代码
关系库 LIKE: SELECT * FROM chunks WHERE content LIKE '%代码质量%'
→ 只能命中包含"代码质量"的文本,找不到 "提高程序可维护性的方法"

向量库 ANN: SEARCH(embedding("如何提升代码质量"), topK=5)
→ 通过语义相似度找到:
  "提高程序可维护性的方法"     (cosine=0.92)
  "代码重构的最佳实践"         (cosine=0.88)
  "编写高质量 Go 代码的技巧"    (cosine=0.85)

LIKE 是字面匹配,ANN 是语义理解。差距一目了然。


二、Collection 怎么设计

这是最实际的问题:向量库里该存哪些字段?

核心数据结构

go 复制代码
// ChunkVector 向量数据库中的分片记录结构
type ChunkVector struct {
    // 主键
    ID string// varchar(36), 与关系库 chunk.id 一致

    // 向量字段
    DenseVector  []float32// 稠密向量,语义检索
    SparseVector []float32// 稀疏向量,关键词检索(可选)

    // 标量过滤字段(检索前预过滤)
    KnowledgeBaseID string// 分区键,按知识库隔离
    DocumentID      string// 支持按文档过滤
    TenantID        string// 多租户隔离
    Enabled         bool   // 禁用标记

    // 辅助返回字段
    Content  string// 原文内容,省去回关系库查询
    Position int32// 文档内排序,用于上下文窗口扩展
}

设计原则:只冗余检索必须的字段

go 复制代码
应该存入向量库:
├── id                  → 桥接关系库
├── vector              → 核心检索能力
├── knowledge_base_id   → 几乎每次检索都需要过滤
├── document_id         → 按文档范围检索
├── tenant_id           → 多租户安全隔离
├── enabled             → 排除已禁用分片
├── content             → 省去回关系库取原文
└── position            → 上下文窗口扩展

不需要存入向量库:
├── token_count         → 管理统计用,检索不需要
├── word_count          → 同上
├── metadata (JSON)     → 复杂结构,向量库处理效率低
├── created_at          → 审计字段
└── updated_at          → 同上

向量库不是关系库的镜像,只放检索时用得上的字段。

双存储架构:CQRS 思想

关系库和向量库是 「CQRS」 架构中的两侧:

  • 「关系库 = Command Side」:写入、管理、事务、审计、聚合统计

  • 「向量库 = Query Side」:高性能语义检索

两者通过 chunk_id 桥接。关系库是 Source of Truth(唯一真实数据源),向量库是面向检索的数据投影。

冗余标量字段的价值在于避免检索后回关系库二次查询,延迟从 15ms~30ms 降至 ~12ms。一次 IO 搞定,不用来回跑。


三、向量索引:快和准的博弈

为什么需要索引?

暴力搜索有多恐怖?100 万条 1024 维向量,需要约 10 亿次浮点运算(~100ms)。1 亿条?大概 10 秒。

不建索引,等于让搜索引擎挨个翻页。

向量索引通过预处理数据结构加速搜索,用少量精度换数量级的速度提升。

HNSW(首选推荐)

「HNSW(Hierarchical Navigable Small World)」 是多层级的近邻图结构,也是目前 RAG 场景最主流的索引。

go 复制代码
Layer 2(最稀疏,长距离连接):  ● ─────────────── ●
Layer 1(中等密度):           ● ── ● ─────────── ●
Layer 0(最底层,所有节点):     ● ─ ● ─ ● ─ ● ─ ● ─ ● ─ ●

「搜索过程」:从最高层出发,粗粒度定位(像看国家地图),逐层下降,精度逐步提高(城市→街区→具体位置),在 Layer 0 找到最终结果。

类比:就像导航先定省,再定市,再定街道,不会在全国地图上找门牌号。时间复杂度 O(log N)。

  • 优势:查询最快、召回率 >95%、支持增量插入

  • 劣势:内存占用大

IVF 系列

先用 K-Means 将向量聚成 N 个簇,查询时只搜最近的 nprobe 个簇:

变体 特点 适用
「IVF_FLAT」 簇内暴力搜索 百万级,需较高精度
「IVF_SQ8」 簇内 8bit 量化 百万级,节省 ~75% 内存
「IVF_PQ」 簇内乘积量化 千万级,大幅压缩内存

DISKANN

类似 HNSW 但图结构存磁盘,内存只放 PQ 压缩向量做路由。适合 10 亿级且内存有限的场景。

FLAT(暴力搜索)

无索引,100% 召回率,O(N) 复杂度。仅适合 < 10 万条数据或作为精度基准。

索引选型对比

索引 构建速度 查询速度 召回精度 内存 适用规模
FLAT 最快 最慢 100% < 10万
IVF_FLAT 较快 百万级
HNSW 「最快」 「很高」 「高」 千万级
DISKANN 较慢 「十亿级」

选型口诀:< 100 万用 IVF_FLAT,100 万~5000 万用 HNSW(首选),> 5000 万用 DISKANN。


四、标量索引:向量搜索的前置过滤器

什么是标量索引?

为非向量字段(字符串、整数、布尔值)建立的索引,在向量搜索之前先缩小范围:

go 复制代码
Step 1 标量过滤: knowledge_base_id="kb_001" AND enabled=true → 100万→5万
Step 2 向量搜索: 在 5 万条上做 ANN → 返回 Top 5

先筛后搜,100 万变 5 万,搜索快 20 倍。

和关系库索引的区别

对比 关系库索引 向量库标量索引
底层 B+ 树、Hash Trie 树、倒排索引
操作 =, >, <, BETWEEN, LIKE =, >, <, IN, AND/OR
目的 加速条件查询 加速向量搜索前的预过滤
独立使用 可以 通常配合向量搜索

原理基本一致,角色不同------关系库索引独立工作,标量索引是向量搜索的"前置门卫"。


五、距离度量:怎么定义"相似"

三种主流度量方式:

L2(欧氏距离)

空间中两点直线距离。d = √[Σ(aᵢ - bᵢ)²],值域 [0, +∞),越小越相似。同时考虑方向和长度。

Cosine(余弦相似度)

衡量两个向量方向是否一致。cos(θ) = (A·B) / (|A|×|B|),值域 [-1, 1],+1 最相似。只关心方向(语义),忽略长度(文本长短)。

为什么 RAG 推荐 Cosine?因为文本 Embedding 的方向编码语义信息,长度只与文本长短有关。我们关心的是"说的是不是同一件事",不关心"文本是不是一样长"。

IP(内积)

IP = Σ(aᵢ × bᵢ),值域 (-∞, +∞),越大越相似。当向量已归一化时,IP 等价于 Cosine,但省去除法运算,更快。

选型建议

度量 适用 RAG 推荐
「Cosine」 文本检索(只关心语义方向) 首选
「IP」 向量已归一化时(等价 Cosine 但更快) 可选
「L2」 图像特征、地理位置 不推荐文本

大部分 Embedding 模型输出已归一化,此时 IP 和 Cosine 效果一致。选 Cosine 不会错。


六、分区策略

底层原理

分区把 Collection 的数据物理分割为多个子集,每个子集拥有独立的向量索引:

go 复制代码
chunks Collection
  ├── Partition: kb_001 → 独立 HNSW 图(10万向量)
  ├── Partition: kb_002 → 独立 HNSW 图(50万向量)
  └── Partition: kb_003 → 独立 HNSW 图(2万向量)

检索 kb_002: 只搜索 50 万向量的 HNSW 图,而非全部 62 万

RAG 场景怎么分?

knowledge_base_id 分区。检索天然按知识库隔离,分区利用率最高。


七、稠密向量 vs 稀疏向量

稠密向量(Dense Vector)

由深度学习 Embedding 模型生成,捕捉语义信息:

go 复制代码
输入: "机器学习是人工智能的一个子领域"

输出(1024维): [0.23, -0.45, 0.67, 0.12, ..., -0.01]
                 每一维都有非零值 → "稠密"

特征:
• 维度固定(768/1024/1536),由模型决定
• 几乎每一维都是非零值
• 每一维含义隐式,不可解释
• 擅长:同义词、上下位关系、语义类比
• 存储:1024 维 × 4 bytes = 4 KB / 条

稀疏向量(Sparse Vector)

由 BM25 或学习型模型生成,捕捉关键词匹配信息:

go 复制代码
词表有 250,000 个词,输入: "机器学习是人工智能的一个子领域"

输出(只展示非零部分):
{机器:2.3, 学习:1.8, 人工智能:3.5, 领域:2.1, 子:0.8, ...}
其余 249,993 维都是 0 → "稀疏"

特征:
• 维度 = 词表大小(3万~25万),大部分为 0
• 非零维度通常只有几十个
• 每一维含义明确(第 N 维 = 第 N 个词的权重)
• 只存非零位置和权重 → ~50 × 8 bytes ≈ 400 bytes / 条

对比

对比 稠密向量 稀疏向量
生成方式 深度学习模型 BM25 / SPLADE
维度 768 ~ 3072 3万 ~ 25万
非零比例 ~100% < 0.1%
每维含义 隐式不可解释 明确对应具体词
擅长 语义理解、同义词 精确关键词、专有名词
弱点 可能忽略低频专有词 无法理解语义等价

稠密向量理解"你想问什么"(语义),稀疏向量精确匹配"你说了什么"(关键词)。两者组合,才是完整的检索能力。


八、混合检索 + Rerank:生产标配

混合检索架构

生产级 RAG 同时利用稠密向量和稀疏向量:

go 复制代码
用户 Query
                        │
             ┌──────────┴──────────┐
             ▼                     ▼
      Embedding 模型          BM25/SPLADE
             │                     │
             ▼                     ▼
       稠密向量检索            稀疏向量检索
       (语义相似度)            (关键词匹配)
             │                     │
             └──────────┬──────────┘
                        ▼
                  融合排序 (RRF)
                        ▼
                  Rerank 重排序(可选)
                        ▼
                   最终 Top K

融合排序:RRF 算法

「RRF(倒数排名融合)」 是最常用的融合算法,简单有效:

go 复制代码
公式: RRF_score(d) = Σ 1/(k + rank_i(d))     k=60(平滑常数)

举个例子:
  文档 X: 稠密排第1, 稀疏排第5 → RRF = 1/61 + 1/65 = 0.03177
  文档 Y: 稠密排第3, 稀疏排第2 → RRF = 1/63 + 1/62 = 0.03200
  → Y 排前面(两边都靠前 > 一边极前一边较后)

RRF 的好处是只看排名,不看分数,不用操心两路检索分数量纲不同的问题。

另一种方式是**「加权求和」** :score = w₁ × dense_score + w₂ × sparse_score,需要先归一化分数,典型权重 0.7:0.3。

Rerank 重排序

融合后用 「Cross-Encoder」 模型对候选结果精排:

go 复制代码
Embedding 模型(Bi-Encoder):  分别编码 Query 和 Chunk → 速度快,精度有限
Cross-Encoder(Reranker):    同时编码 Query+Chunk   → 速度慢,精度更高

流程: 混合检索取 Top 20 → Rerank 精排 → 输出 Top 5

召回是海选,Rerank 是终面。海选要快要全,终面要准要精。

完整代码实现

go 复制代码
// HybridSearchWithRerank 混合检索 + 重排序
func (s *SearchService) HybridSearchWithRerank(
    ctx context.Context,
    knowledgeBaseID string,
    query string,
    topK int,
) ([]*SearchResult, error) {
    denseVec, err := s.embedder.EmbedDense(ctx, query)
    if err != nil {
        returnnil, fmt.Errorf("embed dense: %w", err)
    }
    sparseVec, err := s.embedder.EmbedSparse(ctx, query)
    if err != nil {
        returnnil, fmt.Errorf("embed sparse: %w", err)
    }

    // 取 4 倍候选量供 Rerank 筛选
    candidates, err := s.vectorRepo.HybridSearch(
        ctx, knowledgeBaseID, denseVec, sparseVec, topK*4,
    )
    if err != nil {
        returnnil, fmt.Errorf("hybrid search: %w", err)
    }

    reranked, err := s.reranker.Rerank(ctx, query, candidates, topK)
    if err != nil {
        return candidates[:topK], nil// 降级:Rerank 挂了就用原始结果
    }
    return reranked, nil
}

九、HNSW 参数调优

HNSW 是推荐索引,参数怎么调?这里拆解三个关键参数。

构建参数(创建索引时设定,不可更改)

「M(每个节点最大连接数)」

M 值 图密度 内存 召回率 推荐场景
4~8 稀疏 较低 资源受限
「16」 适中 「通用推荐」
32~64 密集 很高 高精度要求

M 翻倍,图内存约翻倍。

「efConstruction(构建时搜索宽度)」

新向量插入时搜索多少候选节点来选邻居。越大 → 图质量越好,构建越慢。推荐 128~512。

构建只做一次,值得多花时间。

搜索参数(每次查询时可调)

「ef / efSearch(动态候选列表大小)」

这是 HNSW 搜索时维护的一个按距离排序的候选列表,ef 决定列表容量:

go 复制代码
ef = 10:  搜索范围小 → ~2ms  → 召回率 ~85%
ef = 64:  搜索范围中 → ~5ms  → 召回率 ~95%  ← RAG 推荐
ef = 256: 搜索范围大 → ~15ms → 召回率 ~99%
ef = N:   等价暴力搜索        → 召回率 100%

关键规则:ef 必须 ≥ topK。候选列表装不下需要的结果数量,就白搜了。

搜索过程:取列表中最近的未访问节点 → 查看其邻居 → 比列表最差的更近则替换入列表 → 重复直到无法改善 → 取前 K 个。

参数推荐速查

场景 M efConstruction ef
原型/测试 8 64 32
「生产通用」 「16」 「256」 「64~128」
高精度要求 32 512 256

「调优思路」

  1. 先用 FLAT 索引获取 100% 精确结果作为基准

  2. 调整 HNSW 参数,用 recall@K 衡量召回率

  3. 在满足延迟 SLA 的前提下最大化召回率


十、数据生命周期管理

写入流程

go 复制代码
文档上传
  → [关系库] 创建 Document (status=pending)
  → [异步任务] 解析 → 切片
  → [关系库] 批量创建 Chunks
  → [Embedding] 调用模型批量向量化
  → [向量库] 批量 Upsert(每批 500 条)
  → [关系库] 更新 Document.status=completed
go 复制代码
func (r *VectorRepo) UpsertChunks(ctx context.Context, chunks []*ChunkVectorData) error {
    const batchSize = 500
    for i := 0; i < len(chunks); i += batchSize {
        end := min(i+batchSize, len(chunks))
        batch := chunks[i:end]
        // 构建列数据并 Upsert...
        if _, err := r.client.Upsert(ctx, "chunks", "", columns...); err != nil {
            return fmt.Errorf("upsert batch %d: %w", i/batchSize, err)
        }
    }
    return nil
}

删除与更新

go 复制代码
// 按文档删除
func (r *VectorRepo) DeleteByDocumentID(ctx context.Context, docID string) error {
    return r.client.Delete(ctx, "chunks", "", fmt.Sprintf(`document_id == "%s"`, docID))
}

// 更新内容:需要重新向量化
// 1. 更新关系库(Source of Truth)
// 2. 重新 Embedding
// 3. Upsert 到向量库

一致性保证

关系库是 Source of Truth,写操作先写关系库,再同步向量库:

go 复制代码
func (s *ChunkService) UpdateContent(ctx context.Context, chunkID, newContent string) error {
    if err := s.chunkRepo.UpdateContent(ctx, chunkID, newContent); err != nil {
        return fmt.Errorf("update db: %w", err)
    }
    newVec, err := s.embedder.Embed(ctx, newContent)
    if err != nil {
        _ = s.chunkRepo.MarkNeedResync(ctx, chunkID) // 标记待同步
        return fmt.Errorf("re-embed: %w", err)
    }
    if err := s.vectorRepo.Upsert(ctx, chunkID, newVec, newContent); err != nil {
        _ = s.chunkRepo.MarkNeedResync(ctx, chunkID)
        return fmt.Errorf("upsert vector: %w", err)
    }
    returnnil
}

兜底策略:定时对账任务扫描 need_resync 标记的分片,重新向量化并同步。不能指望每次都一次成功,关键是有兜底。


十一、向量数据库选型

特性 Milvus Qdrant Weaviate pgvector
部署复杂度 高(依赖 etcd/MinIO) 低(单二进制) 最低(PG 插件)
最大数据量 十亿级 亿级 亿级 千万级
混合检索 原生支持 原生支持 原生支持 需配合 tsvector
分区/多租户 Partition Key Collection Multi-tenancy 表级别
标量过滤 表达式 Payload Filter SQL WHERE
GPU 加速 支持 不支持 不支持 不支持
Go SDK 标准 SQL

选型建议

  • 「起步 / 数据量 < 1000万」pgvector------一套 PG 搞定,运维最简,适合团队小、预算有限

  • 「生产级 / 数据量 > 1000万」Milvus(大规模首选)或 Qdrant(轻量高性能)

  • 「全功能平台」Weaviate------内置模块化能力,开箱即用

如果数据量 < 500 万,pgvector 够用了,少一套系统少一份运维负担。


十二、生产环境最佳实践

容量规划

go 复制代码
单条向量内存估算(HNSW, M=16, 1024维):
  向量数据:  1024 × 4 bytes              = 4,096 bytes
  HNSW 图:  2 × M × 4 bytes × layers     ≈ 512 bytes
  标量字段: ~200 bytes(ID + 过滤字段)
  ──────────────────────────────────────
  合计: ~5 KB / 条

  100 万条 ≈ 5 GB 内存
  1000 万条 ≈ 50 GB 内存

先算账再选机器。不少团队上线后才发现内存不够,已经来不及了。

写入优化

  • 「批量写入」:每批 500~1000 条,避免单条插入

  • 「异步写入」:文档处理流水线异步执行,不阻塞用户操作

  • 「写入后等待」 :Milvus 需要 Flush() 确保数据持久化,LoadCollection() 确保可搜索

查询优化

  • 「预过滤」:利用标量索引在向量搜索前缩小范围

  • 「合理设置 ef」:在延迟 SLA 内最大化召回率

  • 「限制返回字段」:只返回需要的字段,减少网络传输

  • 「连接池」:复用向量库连接,避免频繁建连

监控指标

指标 含义 告警阈值建议
查询延迟 P99 99% 请求的响应时间 > 100ms
召回率 与暴力搜索结果的重合度 < 90%
内存使用率 向量索引占用内存比例 > 80%
写入 QPS 每秒写入向量数 根据业务基线
查询 QPS 每秒检索请求数 根据业务基线

高可用部署

  • 「Milvus」:支持多副本、分片,通过 etcd 做元数据管理

  • 「Qdrant」:支持分布式部署和副本集

  • 「pgvector」:依赖 PostgreSQL 自身的主从复制


十三、常见问题排查 FAQ

Q1: 检索结果不准确(召回率低)

  • 检查 Embedding 模型是否适合当前语言和领域

  • 提高 HNSW 的 ef 参数(如从 64 提到 128)

  • 检查距离度量是否匹配(文本应用 Cosine,而非 L2)

  • 检查分片策略是否合理(分片太大会稀释语义)

Q2: 检索速度慢

  • 检查是否加载了向量索引(Milvus 需 LoadCollection

  • 利用分区和标量过滤缩小搜索范围

  • 降低 ef 值(在可接受的召回率范围内)

  • 检查是否有热点分区导致负载不均

Q3: 内存不足

  • 使用 IVF_SQ8 或 IVF_PQ 替代 HNSW 压缩内存

  • 降低 HNSW 的 M 参数

  • 考虑 DISKANN(磁盘索引)

  • 清理不再需要的 Collection 或 Partition

Q4: 数据不一致(关系库和向量库不同步)

  • 确保写入顺序:先关系库,再向量库

  • 实现 need_resync 标记 + 定时对账任务

  • 关键操作添加幂等性保证(Upsert 而非 Insert)

  • 添加数据校验监控(定期比对两库记录数)

Q5: 新增文档后搜索不到

  • Milvus:确认执行了 Flush()LoadCollection()

  • 检查 enabled 字段是否正确设置

  • 确认向量化是否成功(检查异步任务状态)

  • 检查分区键是否正确(错误的分区键会导致搜索不到)


附录:核心术语速查

术语 全称 解释
ANN Approximate Nearest Neighbor 近似最近邻搜索,以少量精度换取速度
HNSW Hierarchical Navigable Small World 多层级近邻图索引,查询速度最快
IVF Inverted File Index 基于聚类的倒排文件索引
PQ Product Quantization 乘积量化,向量压缩技术
SQ Scalar Quantization 标量量化,将 float32 压缩为 int8
RRF Reciprocal Rank Fusion 倒数排名融合,多路结果合并算法
CQRS Command Query Responsibility Segregation 命令查询职责分离架构模式
ef Exploration Factor HNSW 搜索时的候选列表大小
nprobe Number of Probes IVF 搜索时探测的簇数量
Embedding --- 将文本转为固定维度向量的过程/结果
Cross-Encoder --- 同时编码 Query+Doc 的精排模型
Bi-Encoder --- 分别编码 Query 和 Doc 的模型(即 Embedding)

写在最后

向量数据库概念多,但核心逻辑很清晰:

  1. 「双存储 CQRS」:关系库管数据,向量库管检索,别搞混

  2. 「HNSW 首选」:99% RAG 场景够用,参数 M=16, ef=64~128 起步

  3. 「混合检索」:稠密 + 稀疏 + RRF 融合,别只靠语义

  4. 「Rerank 精排」:从 80 分到 95 分的最后一公里

  5. 「先算账再选型」:数据量决定你该选 pgvector 还是 Milvus

相关推荐
TDengine (老段)2 小时前
中原油田引入时序数据库 TDengine:写入性能提升、存储成本下降 85%
大数据·数据库·人工智能·时序数据库·tdengine·涛思数据
IT邦德2 小时前
Oracle 26ai搭建ADG Far Sync日志备库
数据库·oracle
Crazy CodeCrafter2 小时前
现在做服装,实体和电商怎么选?
大数据·数据库·人工智能·微信·开源软件·零售
一江寒逸3 小时前
零基础从入门到精通MongoDB(下篇):进阶精通篇——吃透高级查询、事务、索引优化与集群架构,成为MongoDB实战高手
数据库·mongodb·架构
sa100273 小时前
一键获取淘宝天猫商品评论:API 接口实战与多语言实现教程
数据库·oracle
huanmieyaoseng10033 小时前
Linux安装达梦数据库DM8
linux·运维·数据库
香蕉鼠片3 小时前
Mysql进阶篇
数据库·mysql·oracle
数厘3 小时前
2.12 sql 数据插入(INSERT INTO)
数据库·sql·oracle
薛定e的猫咪3 小时前
2026 年 4 月实测:OpenAI Codex 保姆级教程,从安装到 MCP、Skills 与多智能体协作
前端·数据库·人工智能