这篇学完你能回答什么
- 「HNSW 是怎么做近似最近邻的?相比暴力检索牺牲了什么?」
- 「Milvus / Qdrant / pgvector / Chroma 怎么选?」
- 「知识库更新了,索引怎么增量更新?删除的文档怎么办?」
从一个真实故障讲起
两条典型的踩坑路径,你多半会遇到其中一条:
重装上阵:项目刚立项就上 Milvus 分布式集群,结果知识库才两万条文档,团队一半精力耗在维护 etcd、对象存储、消息队列这套依赖上,需求天天变,谁都不敢动索引结构。
轻装冒进:用 Chroma 跑了半年原型,数据涨到三百万条,某天线上检索延迟从 80ms 飙到 2 秒——单机 HNSW 索引内存放不下,扩容只能整库重建。
两种坑根子一样:选型时只看了「能不能跑起来」,没看「这条路能走多远」。
用 Chroma 跑原型
进程内嵌,运维极低,先跑起来再说
数据涨到三百万条
选型时只看了「能不能跑起来」
单机索引放不下断点
HNSW 图结构常驻内存,单机装不进
延迟 80ms → 2 秒
扩容只能整库重建
核心概念:向量库到底特殊在哪
普通数据库擅长精确匹配(WHERE id = 123)。向量检索要回答的是「跟这个向量最像的 10 个是谁」——在百万、亿级高维向量里做这件事,暴力算一遍全部距离在延迟上完全不可接受。
向量数据库 = 专为高维向量设计的存储与检索系统,核心能力是近似最近邻检索(ANN, Approximate Nearest Neighbor),配套元数据过滤、增删改和分布式扩展。
关键词是 Approximate(近似):用一点点召回率,换几个数量级的速度。 理解这个 trade-off 就理解了向量库的本质。
WHERE id = 123报得出主键就精确命中
换成「跟这个最像的 10 个是谁」,它答不了
专为高维向量设计的存储与检索系统
核心能力是近似最近邻检索,外加元数据过滤、增删改、分布式扩展
百万、亿级高维向量逐条比
结果最准,但延迟上完全不可接受
Approximate Nearest Neighbor
用一点点召回率,换几个数量级的速度
原理拆解:HNSW
| 参数 | 作用 | 调大的后果 | 改它要不要重建 |
|---|---|---|---|
M | 每个节点的邻居数(图的稠密度) | 召回↑、内存↑、构建慢 | 要重建 |
ef_construction | 建索引时的候选队列长度 | 索引质量↑、建索引更慢 | 要重建 |
ef_search | 查询时的候选队列长度 | 召回↑、查询变慢 | 不用,运行时直接调 |
一句很实在的工程回答:「召回不够,先调 ef_search 看看」—— 它是唯一能在线调的旋钮,不重建索引就能试。而 M 和 ef_construction 一旦建好就固定了。
其他索引:IVF 先聚类、只搜几个簇,内存友好但召回略低;DiskANN 把索引放 SSD,适合内存放不下的超大规模;量化(标量/乘积/二值)用精度换内存,能压到原来的几分之一。
ef_search 调太小会漏;删除只能软删,打标记跳过,删多了得重建。HNSW(Hierarchical Navigable Small World,分层可导航小世界)是目前最主流的 ANN 索引,pgvector、Qdrant、Milvus 默认都用它。
直觉:像查地图一样找最近的餐馆
想象你要从北京找上海某条街的一家店:
- 顶层:只有城市级节点(北京、上海、广州)。你一步跳到上海。
- 中层:上海的各个区。你跳到黄浦区。
- 底层:所有街道和店铺,你在附近小范围精找。
HNSW 就是这个结构——多层图,上层稀疏用来"长距离跳跃",下层稠密用来"精细定位"。每层的节点通过边连成一张"小世界"网络(任意两点间跳几步就能到),检索时从顶层入口出发,每层贪心地走向离目标更近的邻居,走不动了就下沉一层,直到底层输出 Top-K。
复杂度从暴力检索的 O(N) 降到大约 O(log N)——这就是它能撑起毫秒级检索的原因。
三个必须知道的参数
| 参数 | 作用 | 调大的后果 |
|---|---|---|
M |
每个节点的邻居数(图的稠密度) | 召回↑、内存↑、构建慢 |
ef_construction |
建索引时的候选队列长度 | 索引质量↑、建索引更慢 |
ef_search |
查询时的候选队列长度 | 召回↑、查询变慢(唯一能在线调的旋钮) |
面试要点:ef_search 是运行时参数,可以不重建索引直接调——「召回不够先调 ef_search 看看」是很实在的工程回答。而 M 和 ef_construction 一旦建好就固定了,要改得重建。
HNSW 的代价(追问重点)
- 内存开销大:图结构常驻内存,向量涨到千万级时内存可能成为瓶颈——上面「轻装冒进」那个故障就是这么来的。
- 召回不是 100%:本质是近似,
ef_search调太小会漏。 - 删除是软删除:图结构不好真删,一般打标记跳过,删多了需要重建(下面细讲)。
其他索引:IVF(先聚类再只搜几个簇,内存友好但召回略低)、DiskANN(索引放 SSD,适合超大规模内存放不下的场景)、量化(标量/乘积/二值量化,用精度换内存,能压到原来的几分之一)。知道它们的存在和适用场景即可。
工程实践(截至 2026-08)
选型对比
| 库 | 甜点区 | 运维负担 | 备注 |
|---|---|---|---|
| pgvector | 已有 PostgreSQL、百万级以内 | 极低(装个扩展) | 事务一致性好,向量和业务数据同库 JOIN;性能不及专用库 |
| Qdrant | 中等规模、性能优先 | 低(单二进制/docker run) | Rust 实现无 GC 停顿,元数据过滤强,单节点 P99 延迟通常最低 |
| Milvus | 亿级、真正的分布式 | 高(依赖 etcd/对象存储/消息队列) | 大规模场景验证最充分;2.5+ 内置 BM25 全文检索,一库搞定混合检索 |
| Weaviate | 混合检索、开发体验 | 中 | 原生 hybrid 查询带 α 参数 |
| Chroma | 原型、快速验证 | 极低(进程内嵌) | 不建议上生产:不支持混合检索、扩展性有限 |
| Elasticsearch / OpenSearch | 团队已有 ES 栈 | 中 | BM25 底子厚,向量能力后补 |
一句话决策:
数据量 < 100 万 且 已有 PostgreSQL → pgvector(零额外运维) 中等规模、要低延迟和强过滤 → Qdrant 亿级 / 要分布式 / 要一库做混合检索 → Milvus 只是原型 → Chroma,但规划好迁移
好消息是迁移成本没想象中高:embedding 结果是确定性的,换库不需要重新计算向量,导出向量和元数据重新写入即可,百万级通常半小时内搞定。所以「先用简单的,顶不住再迁」是合理策略——但前提是别在 Chroma 上跑到三百万条才想起来。
几个容易被追问的工程点
元数据过滤:预过滤 vs 后过滤。 带条件的查询(category = 'X' 且语义相似)有两种执行顺序:先过滤再搜(预过滤)或先搜再过滤(后过滤)。后过滤有个致命问题:Top-100 里可能只有 3 条满足条件,结果不够用。Qdrant、Milvus 默认预过滤;pgvector 要留意查询计划。这题是区分「用过」和「读过」的好探针。
增量更新与删除(对应 Q2-23):
- 新增:直接 upsert,HNSW 支持增量插入。
- 更新:删旧 + 插新,注意向量和原文要原子性对齐,否则会出现「检索到了但取不到原文」。
- 删除:多数实现是软删除(打标记跳过),空间不会立刻释放;删除比例高了会让图结构劣化、召回下降,需要定期 compact 或重建索引。
- 实用做法:用版本号/时间戳做元数据,检索时过滤旧版本,再异步清理——避免更新期间出现检索空窗。
维度与存储估算:1536 维 float32 单条约 6KB,100 万条约 6GB(还没算图结构开销)。这个粗算能力面试时很好用——顺便可以带出降维(T2-3)和量化两个优化方向。
| 库 | 甜点区 | 运维负担与备注 |
|---|---|---|
| pgvector | 已有 PostgreSQL、百万级以内 | 极低(装个扩展)。事务一致性好,向量和业务数据同库 JOIN;性能不及专用库。零额外运维,起步默认 |
| Qdrant | 中等规模、性能优先 | 低(单二进制/docker run)。Rust 实现无 GC 停顿,元数据过滤强,单节点 P99 延迟通常最低 |
| Milvus | 亿级、真正的分布式 | 高(依赖 etcd/对象存储/消息队列)。大规模场景验证最充分;2.5+ 内置 BM25 全文检索,一库搞定混合检索 |
| Weaviate | 混合检索、开发体验 | 中。原生 hybrid 查询带 α 参数 |
| Chroma | 原型、快速验证 | 极低(进程内嵌)。不建议上生产:不支持混合检索、扩展性有限 |
| Elasticsearch / OpenSearch | 团队已有 ES 栈 | 中。BM25 底子厚,向量能力后补 |
- 一句话决策
< 100 万且已有 PostgreSQL → pgvector;要低延迟强过滤 → Qdrant;亿级/分布式 → Milvus- 只是原型
- Chroma 可以,但规划好迁移 —— 别在它上面跑到三百万条才想起来
- 迁移没那么贵
- embedding 是确定性的,换库不用重算向量;导出向量和元数据重新写入,百万级通常半小时内搞定
- 预过滤 vs 后过滤
- 后过滤的致命处:Top-100 里可能只有 3 条满足条件。Qdrant、Milvus 默认预过滤,pgvector 要留意查询计划
- 增量更新
- 新增直接 upsert;更新 = 删旧 + 插新,向量和原文要原子对齐,否则「检索到了但取不到原文」
- 存储粗算
- 1536 维 float32 单条约 6KB,100 万条约 6GB —— 还没算图结构开销
面试视角
- 类比开场HNSW = 多层地图跳跃,上层跳远、下层精找
- 点明本质权衡「近似」= 用一点点召回换几个数量级速度
- 给参数和实用细节三个参数,其中
ef_search可在线调 - 选型先反问规模先问数据量和团队运维能力,再给结论
- 上来就说「我用 Milvus」,不问数据量 —— 反倒暴露没做过选型
- 只会讲 HNSW「快」,说不清怎么跳、为什么是 O(log N)
- 把「召回不是 100%」当成 bug,近似索引本来就会漏
- 答不出删除是软删除,也不知道删多了要 compact 或重建
- 被问量级只能含糊过去,1536 维 100 万条要多少 G 算不出来
- 先反问规模和运维人手再给结论,两万条文档不上分布式
- 讲得出顶层跳跃、下层精找、走不动就下沉这条路径
- 「召回不够先调
ef_search看看」—— 运行时参数,不用重建索引 - 主动说软删除会让图结构劣化、召回下降,要定期 compact
- 张口就来:1536 维 float32 单条约 6KB,100 万条约 6GB
Q2-07、Q2-08、Q2-23。小结与延伸
一句话总结:向量库的核心是用「近似」换速度,HNSW 靠多层小世界图把 O(N) 降到 O(log N);选型的第一性问题不是哪个库最强,而是你的数据量和团队愿不愿意长期运维一套分布式系统。
延伸:HNSW 原论文(Malkov & Yashunin);各库官方的索引参数调优文档;下一篇 T2-6 讲 Rerank——检索召回之后,怎么把最相关的顶到最前面。
继续深入
本篇归属第 2 章「RAG 工程化」,去做这一章的题。