一文讲透向量数据库原理:IVF、HNSW、PQ 三大索引怎么工作

简介: 搞懂向量数据库原理,不用背参数。本文从距离怎么算讲起,拆解 IVF 分区、HNSW 分层图、PQ 压缩三类技术方案的工作方式,再走一遍一次查询的完整流水线

大家好,我是程序员天天困。

我见过太多 RAG demo 是这么写的:embedding 接口调一下,向量往数组里一塞,查询时 for 循环挨个算余弦相似度,取 Top-K。几千条数据的时候它跑得飞快,快到你以为架构已经完工。等知识库真灌进几百万个 chunk,单次查询两三秒,并发一上来内存直接爆——这时候你才会理解,向量数据库到底在替你解决什么问题。

这篇不堆产品榜单,我们把向量数据库原理掰开揉碎讲一遍:向量之间的"远近"怎么算、为什么传统索引难以直接应对高维向量、ANN 凭什么牺牲少量召回率换来数量级的速度提升,以及 IVF、HNSW、PQ 这三个天天见面的技术名词到底在干什么,最后走一遍一条查询在库里面的完整旅程。建议先收藏。

一、向量检索到底在算什么

向量数据库的故事,得从"两个向量怎么算远近"讲起——索引怎么建,全看距离怎么定义。

embedding 模型把一段文本、一张图压成一串浮点数,比如 1024 维。你可以把它想成高维空间里的一个坐标点:语义相近的内容,点的位置也挨得近。这个"近"怎么量化?本文先看三种常见度量:

1)L2 欧氏距离:两个点之间的直线距离,各维度差值平方和再开根号。在一些视觉特征和度量学习场景中比较常见。

2)余弦相似度:不看两点隔多远,看两个向量方向的夹角——方向完全一致就是 1。文本语义检索中很常见,因为语义往往更关心"话题方向",而不是向量长短。

3)内积(点积):余弦的未归一化版本。这里有个工程上的冷知识:向量先做 L2 归一化,内积就等于余弦——所以 Faiss 里用 IndexFlatIP 配归一化向量,算的就是余弦相似度。推荐系统场景爱用裸内积,因为向量模长可以顺手编码"热度"。

很多 embedding 模型和向量库示例会使用归一化后的向量,再计算余弦相似度或内积;但最终应以具体模型卡和训练目标为准。

距离定义清楚了,检索要做的事也就明确了:给定一个查询向量,从库里 N 个向量中找出距离最近的 K 个。这就是最近邻搜索。

那问题来了,给向量建个 B+ 树索引不行吗?我刚学这块的时候确实这么想过。

普通 B+ 树能快,前提是数据有稳定的"大小顺序"——数字、字符串是一维的,能排序。向量是上千维的浮点数组,很难定义一个既自然又能服务相似度查询的全序关系。普通哈希依赖精确等值命中,而两个浮点向量完全相等的概率约等于零,也无法直接表达"相近";当然,LSH(局部敏感哈希)是专门为近似相似检索设计的另一类 ANN 方法。高维空间中的维度灾难,会让许多传统的低维索引结构逐渐失去效率。

于是最朴素的方案只剩暴力搜索(Brute-Force):查询向量和库里每一个向量算一遍距离,排序取前 K。写出来就十行代码:

import numpy as np

def brute_force_search(vectors, query, k=10):
    """vectors: (N, dim) 全量向量;query: (dim,) 查询向量;都已归一化"""
    scores = vectors @ query          # 归一化后内积 = 余弦相似度
    top_k = np.argsort(-scores)[:k]   # 分数降序,取前 K
    return top_k, scores[top_k]

数据量小的时候,这就是最优解:结果 100% 精确,连索引都不用建。但算笔账你就笑不出来了:100 万条 1024 维向量,每条要做 1024 次乘加,一轮查询约 20 亿次浮点运算;光把这 4GB 向量从内存读一遍就要吃掉可观的内存带宽,数据放磁盘上更是灾难。100 个用户并发提问?CPU 排队到天亮。

暴力搜索没有错,它只是诚实地做了 O(N×dim) 的计算。要破局,得允许自己"不算那么准"。

二、ANN:牺牲少量召回率,换来数量级速度提升

ANN(Approximate Nearest Neighbor,近似最近邻搜索):不保证返回全局最优的 K 个近邻,而是靠预先构建的索引结构,只扫描一小部分向量就返回"足够接近"的结果。你可以理解为检索界的"差不多得了"——而且是真的差不多。

很多人看到"近似"两个字就心里发怵。但对 RAG 来说,你要的从来不是"全局第一像",而是"Top-K 里有答案"。

精度怎么衡量?常看召回率 recall@K:拿暴力搜索的结果当真值,计算 ANN 返回的 K 个结果与真实 Top-K 的重合比例。比如 recall@10 = 95%,表示平均每条查询的真实 Top-10 中,有约 9.5 个结果被 ANN 找回。很多工程场景会把 recall@10 调到 95%~99% 左右,同时把延迟从秒级压到毫秒级;具体数值仍取决于数据、硬件和参数。

这里有个流传很广的误解:看到"95% 召回率",就以为 5% 的查询会彻底失败。其实它描述的是整体结果的平均重合程度,而不是简单地说有 5% 的查询完全失效。对 RAG 来说,边界位置的少量交换通常影响有限,但真正重要的答案如果没有进入候选集,仍然会影响最终回答。人脸支付、重复内容去重这类"漏一个就是事故"的业务,则需要针对 recall@1 等指标单独设计。

可能有人会问:召回率掉到 95%,我的 RAG 会不会变傻?

先分清你要的是 recall@1 还是 recall@10。RAG 一般取 K=5~20 个片段,大模型通常能容忍一定噪声,但召回率下降是否影响答案,仍要看具体数据和评测结果。真不放心,就把 HNSW 的 efSearch 或 IVF 的 nprobe 调大,用一点延迟换召回——这两个旋钮后面都会讲。最靠谱的办法,是抽一批自己业务里的真实 query 跑一轮真值对比,别信任何脱离数据的"默认参数"。

ANN 工程里常见两种主路由方式,外加一种可以叠加的压缩技术,正好对应本文三个主角:

  • 图导航派:把向量连成一张近邻关系图,查询时沿边跳向目标——代表是 HNSW;
  • 聚类分区派:先把向量分桶,查询只搜最近的几个桶——代表是 IVF;
  • 压缩技术:把每条向量压成几个字节,让同样的内存装下更多数据——代表是 PQ。它通常与 IVF 或 HNSW 组合使用,而不是单独负责路由。

提前剧透一句:IVF 和 HNSW 主要回答"搜哪些向量",PQ 主要回答"向量如何更省空间、更快地计算近似距离"。它们回答的不是同一个问题,这也是全文最重要的伏笔。

三、IVF:先分区,再进门找

IVF 的思想一点都不新鲜——它就是快递分拨中心。

IVF(Inverted File Index,倒排文件索引):先用聚类算法把向量空间切成若干个簇,每个簇用一个质心代表;查询时只探测离查询向量最近的几个簇,簇内再做精确计算。类比快递:你的包裹从不会被送到全国每个网点去问"这是谁的",它先到目标城市的分拨中心,再层层下沉到片区网点。

建索引是离线活:对全量向量跑 K-Means,聚出 nlist 个簇,记下每个簇的质心,再挂一张倒排表——质心编号指向簇内的向量列表。

查询分两步:先让查询向量和 nlist 个质心各算一次距离,挑出最近的 nprobe 个簇;然后只在这 nprobe 个簇的成员里算精确距离。

省多少?如果各个簇的大小大致均衡,1000 万条向量、nlist 设 4096、nprobe 设 32 时,平均扫描量约为 32/4096 ≈ 0.8%——候选集从一千万压到八万左右。再配合后面要讲的 PQ,距离计算的成本还能继续下降。

两个旋钮的脾气得摸清楚:

  • nlist(簇数):可以把 √N 到 4√N 作为初始试探范围,但它不是硬公式。簇越多,每簇通常越小,查询扫描量可能下降,同时训练和质心搜索的成本会上升,边界召回也可能变得更敏感;
  • nprobe(探测簇数):召回和延迟的总开关。调大就是多搜几个簇,更准但更慢,线上一般按延迟预算反推。

IVF 的软肋也得说:聚类边界是硬切割。假设查询向量恰好站在两个簇的界线上,真正的最近邻躺在隔壁簇里,而你没探测那个簇——它就不会进入这次查询的候选集。数据分布严重不均、某些簇大得过于集中时,效果也会打折。另外 K-Means 要先训练,数据持续大量涌入、分布发生漂移后通常需要重新训练或调整,增量更新的灵活性往往不如 HNSW。

四、HNSW:像坐高铁一样在图上跳

HNSW(Hierarchical Navigable Small World,分层可导航小世界图):目前很多向量数据库中的常用默认索引。它把向量组织成一张多层近邻图——底层包含全量节点,越往上节点越少,连接通常也更偏向长距离导航;查询从顶层进入,逐层向下搜索。

我的理解方式,是把它当成三级路网:

  • 顶层是高铁网:全国就几十个站,站与站之间一跳上千公里。从北京去上海,你绝不会先走路出京;
  • 中层是地铁城际:站上百个,负责把你送进目标片区;
  • 底层是街道:所有地址都在这一层,站站停,负责最后一百米的精确定位。

查询一个向量的过程,就是"先坐高铁、再换地铁、最后走路":从顶层某个入口节点出发,在上层反复查看邻居,移动到更接近目标的节点,直到这一层无法继续改善;然后把当前结果带到下一层,重复这个过程。到底层后,算法会维护一个候选集做更充分的搜索,最后返回候选集里距离最近的 K 个。

它为什么快?大致有两层原因。第一,分层结构类似跳表:上层较稀疏的连接帮助搜索快速进入目标区域,不必从底层遍历大量节点;第二,搜索时会维护一个大小受 efSearch 控制的候选集,探路节点数通常远小于 N。图的小世界结构让路径通常较短,配合候选集搜索可以降低陷入局部最优的概率,但它并不保证严格的全局最优结果。

三个参数值得记住:

参数 管什么 调大的收益与代价
M 每个节点每层最多连几条边(部分实现的底层会放宽到 2M) 边越密召回通常越高,内存和建图时间同步上涨,常用 16~32
efConstruction 建图时的候选集宽度 图的质量更好,建图更慢
ef(efSearch) 查询时的候选集宽度 召回通常更高、延迟更大,工程上一般设为不小于 K

HNSW 是默认答案,但"默认"不等于"免费"——它的价签贴在内存上。

图结构要常驻内存:向量本身存一份,每个节点几十条边的邻居编号又是一份。粗算一下:100 万条 1024 维 float32 向量,光向量数据就是 4GB;换成现在常见的 4096 维 embedding,16GB 起步,边的开销还得往上加。所以你会观察到一个规律:数据量到亿级、内存吃紧时,所有方案都得向磁盘或者向量化压缩要空间——这就是 PQ 出场的原因。

五、PQ:它不是索引,是压缩术

先说一个很多教程没讲透的事:严格来说,PQ 是一种量化压缩方法,而不是负责路由和缩小搜索范围的独立索引结构。

PQ(Product Quantization,乘积量化):把高维向量切成若干段,每段独立用聚类中心编号替代原始浮点值,从而把一条向量从几 KB 压到几十字节。IVF 和 HNSW 主要回答的是"搜哪些向量",PQ 主要回答的是"每条向量如何用更少的字节表示",同时也会改变距离计算方式并引入量化误差——三者可以叠加使用。

类比成填收货地址:精确位置是"东经 121.47、北纬 31.23",一串高精度浮点数;但日常我们写"上海市黄浦区南京东路某号"——省、市、区、街道几级编号组合起来,用很短的编码就能近似表达全国任意位置。PQ 干的就是这件事。

具体三步:

1)切段:把 D 维向量均匀切成 M 段。比如 128 维切 16 段,每段 8 维;

2)子空间聚类:对每一段的子向量单独跑 K-Means,通常聚 256 个中心。于是每段都能用一个编号(0~255,正好 1 字节)代替原来的 8 个浮点数;

3)编码:每条向量从 128 个浮点数(512 字节)变成 16 个字节编号——32 倍压缩。4096 维向量按 64 段算,16KB 能压到 64 字节。

压成编号之后距离怎么算?这是 PQ 最巧妙的部分,叫 ADC(非对称距离计算):查询向量不压缩,照样切成 M 段;预先算好每一段的查询子向量到该段 256 个中心的距离,拼成一张 M×256 的查找表;库里的向量只存着 M 个编号,近似距离就是"查 M 次表、加起来"。浮点乘加变成了查表加法,这也是亿级向量粗排能跑在普通机器上的原因。

在实际的 IVFPQ 中,常常不是直接量化原始向量,而是先减去所属簇的质心,对残差向量进行 PQ 编码,这通常能进一步降低量化误差。

代价当然是精度:编号是中心的近似,量化误差天生存在。所以工程上 PQ 的产出只配当"粗排分",不能当最终结论——记住这句话,下一节马上要用。

把 IVF 和 PQ 拼起来就是 IVFPQ:IVF 负责把扫描范围从全库缩到几个簇,PQ 负责让簇内向量缩到原来几十分之一的体积。一个管"少看",一个管"装下",这就是大规模向量场景中常见的组合。DiskANN 则是另一类磁盘图方案:把主要图结构和部分数据放在 SSD 上,并结合压缩和缓存减少内存占用,具体实现不等同于简单地给 HNSW 加一层 PQ。

六、一次查询的完整旅程

把前面所有零件拼起来,你会发现一个反直觉的事实:在需要精排的方案里,近似索引通常只负责海选,决赛常常还是原始浮点向量之间的比较。

下面以"带标量过滤、使用 IVFPQ、最后回捞原始向量精排"为例,说明一条典型旅程;HNSW 或 IVFFLAT 的实际步骤会有所不同:

向量数据库原理拆解:一条查询从 embedding 到 ANN 粗筛、PQ 粗排、原始向量精排的完整流水线

1)向量化:用户提问经 embedding 模型变成查询向量;

2)标量过滤:如果带了"只查 2026 年之后的文档""只在退货政策类目里找"这类条件,就用元数据把不符合条件的候选剔掉(实际系统里可能先过滤、边搜索边过滤,也可能搜索后过滤并扩大候选集)。这步常被忽略,但生产环境天天用;

3)ANN 粗筛:HNSW 沿图下钻,或 IVF 探 nprobe 个簇,从百万级候选里捞出几百到几万个"大概近"的向量;

4)PQ 粗排:如果使用了 PQ,这一步用查表距离给候选快速打分排序——便宜,但粗糙;在不少实现里,它与 IVF 扫描本身就是同一个阶段;

5)原始向量精排(rerank):如果粗筛粗排用的是量化近似距离,就取前几百名捞回原始浮点向量,算精确距离重新排序——前面环节省下的"近似",在这一步被兜了底。需要注意,精排意味着系统还要保存原始向量,或能从外部存储回捞;如果索引本身存的就是原始向量(FLAT、HNSW、IVFFLAT),距离从一开始算的就是精确距离,这一步其实已经融进搜索过程里了;

6)返回 Top-K,拼进 prompt 交给大模型。

为什么要绕这一圈?因为精确距离贵、近似距离便宜:让昂贵的精确计算只发生在几百个候选身上,而不是全库。数据量小、用 FLAT 暴力索引时,粗筛、粗排、精排是同一步;规模上来之后,这条"漏斗"才是向量数据库毫秒响应的真正来源。

七、这些方案怎么选

到这里,可以把几类常见方案摆在一起了:

索引 核心思路 召回与延迟 内存 适合规模
FLAT 不建索引,暴力全算 100% 精确,数据小才快 原始向量全量存,无额外开销(基准) 10 万以内
HNSW 分层图导航 通常能以较低延迟获得较高召回,具体取决于参数和数据 高,原始向量 + 图结构通常需要较多内存 百万~千万
IVF 聚类分桶,只探近簇 召回随 nprobe 可调,延迟取决于候选规模 与 FLAT 接近,只多质心和倒排表 千万~亿
IVF+PQ 分桶 + 量化压缩 量化距离有误差,常配合原始向量精排 压缩编码占用低;若要精排,还需保存或回捞原始向量 亿级以上

表里的规模只是粗略经验,实际边界还会受到向量维度、硬件、并发量、过滤条件和延迟目标影响,最终应以真实数据压测为准。

我的决策路径很简单:

  • 10 万条以内:通常先评估 FLAT,简单、精确,未必需要额外索引;
  • 百万到千万、内存放得下:通常优先评估 HNSW,再根据召回、延迟和更新特征调参;
  • 上亿向量、内存预算紧张:可以评估 IVF+PQ,或者 DiskANN 这类磁盘图方案;
  • 已经在用 PostgreSQL、数据量几十万:pgvector 足够,没必要为它引入一整套专用基础设施。

说到底,可以把向量数据库粗略理解成:ANN 索引引擎 + 向量存储 + 标量过滤 + 持久化与分布式调度。RAG 检索知识库、Agent Memory 召回历史对话,语义层的动作都是同一个:把内容变成向量,再根据相似度找回相关上下文。

向量数据库原理听着唬人,其实不是什么黑魔法,它只是把三件事做到了极致——少算点(IVF 分桶、HNSW 跳图)、算粗点(PQ 压缩)、最后再算准(精排兜底)。选型时别被参数表吓到,先问自己两个问题:数据量多大、内存多少,答案基本自己就浮出来了。


我是程序员天天困,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:你们项目里向量库用的是哪种索引,踩过 recall 不够或者内存爆掉的坑吗?

相关文章
人工智能 缓存 前端开发
12212 66
人工智能 自然语言处理 安全
1159 0
Web App开发 人工智能 API
1465 1
人工智能 JavaScript 开发工具
4845 17
人工智能 Java BI
1543 1
人工智能 JavaScript 测试技术
2473 2
开发工具 Swift git
1989 6
人工智能 JavaScript 测试技术
1220 4