KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

06 · 选型与运维 — keel 龙骨

前面五章把参数和代价都量过了,这一章收口到两个决策:用什么(pgvector / Milvus / faiss),以及什么时候重建索引。选型的答案不在「谁最先进」,而在你手上有什么、能接受什么代价。重建索引是一次性成本,但时机选错会把一次性成本变成线上事故。

前面五章把参数和代价都量过了,这一章收口到两个决策:用什么(pgvector / Milvus / faiss),以及什么时候重建索引。选型的答案不在「谁最先进」,而在你手上有什么、能接受什么代价。重建索引是一次性成本,但时机选错会把一次性成本变成线上事故。

现场

团队要上一个检索服务,三个候选摆在桌上:直接用项目里已经在跑的 PostgreSQL 加 pgvector;上一套独立的 Milvus;或者自己把 faiss 嵌进服务里。三个都被叫做「向量数据库」,但它们解决的不是同一层问题。

构建代价与落盘大小(本机实测)

同一批 10 万条 128 维数据,四种索引的构建与查询成本(06-build-cost.txt):

                   index | train_s |   add_s |  disk_MB | single_ms | batch_qps
           FlatL2 (每次全扫) |       - |   0.007 |    51.20 |    0.3078 |    6044.8
IVF-Flat nlist=256 np=16 |   0.449 |   0.082 |    52.13 |    0.1608 |   25025.8
   IVF-PQ nlist=256 m=32 |  27.976 |   0.313 |     4.26 |    0.2550 |   81292.9
HNSW M=32 efC=200 efS=100 |       - |   5.862 |    78.42 |    0.5281 |   27707.3

读完这张表要记住四点:

  1. Flat 的「建索引」是假的。0.007s 只是拷贝,因为它没有索引结构。它的代价推迟到了每次查询(batch_qps 只有 6044,四种里最低)。
  2. PQ 的训练最贵,但运行最便宜。IVF-PQ m=32 要训练 27.976s(对比 IVF-Flat 的 0.449s,差 60 倍),因为要迭代求解 m 个子空间的码本。换来的是落盘 4.26MB(IVF-Flat 的 1/12)和最高的批量吞吐 81292 QPS——扫压缩编码比扫原始向量便宜。
  3. HNSW 建得慢、占得多、单条查询还不算快。建图 5.862s,落盘 78.42MB(比数据本身 51.20MB 还大),单条查询 0.5281ms 比 IVF-Flat 的 0.1608ms 慢。它的优势要在高召回区间才显现(第 03 章:M=32, efSearch=400 召回 0.9364,IVF 同 QPS 下只有 0.6324)。
  4. 单条延迟和批量吞吐不是一回事。single_ms 是单核路径,batch_qps 是跨查询并行的墙钟吞吐。两者受线程调度影响都有波动,别拿一个去推另一个。构建耗时和落盘大小是确定性的,可信度最高。

容量与成本估算

把第 02 章的「每向量字节数」实测值拿出来,直接乘规模。N=1000 万、d=128 时:

索引 每向量字节(实测) 1000 万条估算 相对 Flat
Flat 512.0 4.77 GiB 1.00x
IVF-Flat(nlist=256) 521.3 4.85 GiB 1.02x
HNSW M=16 656.3 6.11 GiB 1.28x
HNSW M=32 784.2 7.30 GiB 1.53x
IVF-PQ m=16 26.6 254 MiB 0.05x
IVF-PQ m=32 42.6 406 MiB 0.08x

估算方法就一句话:先用 N × d × 4 算出原始向量内存,再按索引家族的倍率加上去。IVF 系几乎不额外占内存(它把向量原样存进倒排列表),HNSW 要额外付 30%~50% 的图开销,PQ 是数量级地砍内存但换召回。

两个容易算错的点:第一,这只是向量本身,标量字段、id、副本数、以及查询时的临时内存都不含在内,实际部署要再留余量。第二,副本数是乘法——3 副本意味着上面每个数字再乘以 3。

选型:三个候选各解决哪一层

维度 pgvector(PostgreSQL 扩展) Milvus(分布式服务) faiss(库)
形态 库内扩展,跟着 PG 走 独立集群,四层架构 + etcd/MinIO/WAL 进程内库,无服务
事务与一致性 单机事务,写后立即可见 分布式,一致性级别可选(默认 bounded staleness) 无,自己保证
过滤 WHERE 下推,走标准 planner partition / partition key 物理隔离 IDSelector 手动下推
规模上限 单机内存/磁盘,绑定 PG 实例 分片可水平扩 单机内存为主,DiskANN 可放盘
运维面 无新增组件,复用 PG 运维 需要维护 etcd、对象存储、WAL 无服务,但持久化/重试/监控全自己写
典型选择 已有 PG、数据量中小、要 SQL 和事务 亿级、多租户、要副本与滚动升级 离线批处理、嵌入式、自建服务的最底层

选型的判断顺序:

  1. 如果已有 PostgreSQL 且数据量在单机能扛的范围,先算 pgvector 的容量(上一个表 × 副本)。能装下、过滤走 WHERE、能接受单机,很多场景到这里就结束了,不需要新组件。本机 10 万条建 HNSW 索引 8.25s、索引 79MB,量级可以参考。
  2. 如果需要水平扩、多副本、跨租户物理隔离,或者数据量已经超过单机内存,上 Milvus。代价是要接受它带来的整条依赖链(第 04 章)和最终一致性。
  3. 如果是离线任务、或你要自己建一个检索服务,用 faiss 做内核,自己在上面加持久化、并发、重试。这条路最灵活也最费工。

一个常见的误判:把 faiss 和 Milvus 当成「同一层的两个选项」。faiss 是库,Milvus 是服务,Milvus 内部就用 faiss 系的索引。你要选的不是「用 faiss 还是 Milvus」,而是「要不要一个服务」。

重建索引的时机

索引是数据的派生物,数据变了索引就会失真。触发重建的情况有三类:

重建流程里最需要设计的是重建期间的服务连续性:

flowchart TD
    T["触发: 换模型 / 分布漂移 / 改构建期参数"] --> B["旁路构建新索引<br/>pgvector: CREATE INDEX CONCURRENTLY"]
    B --> V{"构建成功?"}
    V -->|"失败"| RB["放弃新索引<br/>保留旧索引继续服务"]
    V -->|"成功"| Q["同一查询集验收<br/>recall / p99 / QPS"]
    Q --> P{"达标?"}
    P -->|"否"| RB
    P -->|"是"| SW["原子切换: 新向量 + 新索引<br/>回滚方案同时准备"]
    SW --> M["监控线上 recall 漂移<br/>与索引大小"]
    RB -.->|"服务不中断"| SERV["旧索引仍在服务"]
    SW -.->|"切换出错"| RB

    style T fill:#e3f2fd,color:#0d3b66
    style B fill:#f1f8e9,color:#33691e
    style V fill:#fff3e0,color:#8a4b00
    style RB fill:#ffebee,color:#b71c1c
    style Q fill:#e8f5e9,color:#1b5e20
    style P fill:#fff3e0,color:#8a4b00
    style SW fill:#e8f5e9,color:#1b5e20
    style M fill:#e8f5e9,color:#1b5e20
    style SERV fill:#e8f5e9,color:#1b5e20

两个细节别省:验收必须用同一套查询集(否则前后不可比),回滚方案要和新索引同时准备好(切换出错能立刻退回旧索引)。重建是一次性成本,但你要在低峰做——本机 10 万条建 HNSW 要 5.8s,PQ 训练要 28s,按这个比例外推到千万级,就是几十分钟到小时级。

副本与故障域

选型表里没写的一件事是副本。副本决定了两个东西:读吞吐的横向扩展能力,和单机故障时的可用性。

向量检索的副本和普通无状态服务不一样。无状态服务加副本只是多开几个进程;向量库加副本要把索引也复制一份——上面算的容量是单副本的,三副本就是三倍内存。所以「加副本提升 QPS」在向量检索里是一笔昂贵的买卖,先确认你的瓶颈是 QPS 还是延迟:如果单机 QPS 已经够、只是想要故障容错,加副本是对的;如果是想靠副本摊 QPS,先算清楚内存翻几倍值不值。

故障域也要分层看。第 04 章说过,Milvus 的可用性上限等于 etcd、对象存储、WAL 里最弱的那个。副本能扛住 Query Node 挂掉,但扛不住 etcd 抖动或对象存储不可达——那些是共享依赖,不是副本能覆盖的。做容量和可用性规划时,把「索引节点」和「共享依赖」分开列风险清单。

对 pgvector 这条路,副本就是 PostgreSQL 的流复制(见 PostgreSQL 工程课 06 章)。备库能扛读,但要注意备库的召回和主库一致的前提是索引也同步过去了——逻辑复制在向量类型上的行为和物理复制不同,上线前得验证一次。

监控

向量检索的监控和普通服务不一样,因为很多故障是静默的:

主动破坏

三件事都可以在本机复现,任选其一:

生产边界

动手

  1. 跑 06_build_cost.py,把 HNSW 的 M 改成 64,记录落盘大小和批量 QPS 的变化,算出「每多 10% 内存换来多少 QPS」。
  2. 用本章的「每向量字节 × N」公式,给你手上真实项目算一次容量(含副本数),和你的机器内存对一下,判断该选 Flat/IVF/HNSW/PQ 里的哪一档。
  3. 给一个假想的重建流程写检查清单:触发条件、旁路构建方式、验收查询集、切换与回滚步骤,各写一句可执行的话。

自测

  1. 为什么 PQ 的训练时间(27.976s)比 IVF-Flat(0.449s)贵两个数量级,但运行时的批量吞吐反而更高(81292 vs 25026 QPS)?
  2. HNSW 落盘 78.42MB 比原始数据 51.20MB 还大。如果按「数据大小」做容量规划,会在哪一步出问题?
  3. nprobe 可以热调、nlist 必须重建。为什么这两个参数在运维上要分开管理?
  4. 一个团队已经在用 PostgreSQL,数据量 200 万条。你会直接上 Milvus,还是先用 pgvector?给出你的判断依据和验证步骤。
  5. 「重建索引是一次性成本」这句话在什么情况下是错的?(提示:想想数据持续写入和分布漂移)

本课到此结束。回头看一遍:第 00 章你会算精确检索的账,第 01 章你会选度量,第 02 章你认识四个索引家族,第 03 章你会画召回-延迟曲线并定验收线,第 04 章你理解分布式向量库多出的层与一致性代价,第 05 章你处理过滤与混合检索,第 06 章你会选型、估容量、设计重建流程。这些判断就是这门课想给你的东西——下次 AI 给你生成一段向量检索代码时,你能看出它在哪一步偷了懒。

进入 keel 阅读