在向量数据库中,如果要遍历整个数据库进行精确对比(KNN,K-Nearest Neighbors),计算量太大且在海量数据下极度耗时。因此,向量数据库通常采用近似最近邻算法(ANN, Approximate Nearest Neighbor),通过牺牲微小的精确度前提,跨越式提升查询速度并降低内存开销。
以下是目前向量检索中使用最广泛的几种主流 ANN 算法详解:
1. HNSW (Hierarchical Navigable Small World)
全称是"层次导航小世界",这是当前工业界最主流、综合性能最好的基于图结构(Graph-based)的近似最近邻(ANN)算法。几乎所有主流向量数据库(Milvus、Pinecone、Qdrant等)的默认算法都是 HNSW 或其融合变种。
1.1 核心思想:小世界网络 + 跳表
HNSW 的名字由两部分核心理论组成:
- NSW(Navigable Small World,小世界网络):著名的"六度空间理论"指出,世界上任意两个人之间总能通过不超过六层的人际关系建立联系。在向量空间中,NSW 构建了一张图,通过这张图,哪怕节点数以亿计,从任一节点出发通过"贪婪路由(不断找离目标更近的邻居)"也能用极少的步数到达目标节点。
- Hierarchical(层次化,即跳表思想) :单层的 NSW 图在数据量极大时,贪婪搜索容易陷入局部最优,或者因为跨度太短导致查找步数过多。HNSW 巧妙地借鉴了"跳表(Skip List)"的结构,把单层图做成了多层图 :
- 上层节点极其稀疏:连线跨度极大,充当"高速公路",主要负责快速跨越广阔的向量空间,几步之内锁定目标的大致区域。
- 底层节点非常密集:包含了数据库中的绝对全部数据点,充当"城市里的小街小巷",主要负责在局部区域内进行细致全面的精准搜索。
1.2 搜索与查询过程 (Search)
整体策略:大处锁定,小处精修
在这个多层结构中,水平方向的同层链接 用于横向贪婪移动,尽可能接近目标;而垂直方向的跨层下钻则用于缩小搜索粒度。具体过程如下:
- 最高层入场:搜索始终从图的最高层(最稀疏的一层)的一个固定入口点开始。
- 单层贪婪搜索(水平赶路) :在当前层,计算查询向量与当前节点周围所有 MMM 个相邻节点的距离。如果发现有邻居离目标更近,就顺着连线跳过去;持续这个过程,直到发现没有任何邻居比当前位置更近。此时,便达到了该层的局部最优解(即本层离目标最近的地方)。
- 层级下钻(跨层精搜) :一旦在当前层找到了"局部最优解",这就意味着本层已经无法更靠近目标。于是把这个节点作为新的起点,直接垂直往下钻入下一层(一张更细密的新网络)。
- 重复直至底层 :带着上层的最优结果进入下层后,继续在更密的网络中重复"水平跳跃逼近 →\rightarrow→ 局部最优 →\rightarrow→ 下钻"的过程,直到到达第 0 层(包含所有节点的最底层)。在第 0 层完成最后的扩大范围搜索,找到的 Top-K 个最近节点集即为最终查询结果。
1.3 关键控制参数 (Parameters)
HNSW 的性能可以通过以下几个关键参数进行动态调优(这是业务做性能调整的核心):
M(图的度数 / 最大连接边数) :控制每个节点在每一层最多可以拥有多少条连接(连线数)。M越大,图越密实,搜索通道越多、也就越准,但非常非常吃内存。常见的取值范围通常在 16 到 64 之间。ef_construction(构建时的搜索范围 / 探索因子) :在将新数据插入并构建图索引时,节点需要寻找最合适的 MMM 个邻居来进行连线。这个参数决定了算法在寻找邻居时的候选备选名单(Candidate List)的最大容量 。- 通俗解释 :假设要交 16 个朋友,你是随便看 20 个人就匆匆结交,还是认真面试 200 个人再精挑细选?
ef_construction就是这个面试名单的长度。 - 影响 :值设置得越大,算法考察的候选节点越多,图构建得质量(连线科学性)就越高,最终搜索的召回率就越高;但也意味着每次插入数据时的计算量骤增,建库和数据写入速度会严重变慢。它用来权衡"建库速度"和"图质量"。
- 通俗解释 :假设要交 16 个朋友,你是随便看 20 个人就匆匆结交,还是认真面试 200 个人再精挑细选?
ef_search或ef(搜索时的候选范围) :在最终查询阶段(主要是在底部的 0 层)跟踪的最近邻居节点的最大数量。ef_search越大,找到真实最近邻的概率越高(召回率大幅提升 ),但要在更多节点间计算距离,相应的查询耗时会变长(延迟增加)。
1.4 主要特点评估
- 优点 :
- 极高的召回率(Recall)与精度:在同等的低延迟要求下,其准确度远超大部分基于聚类(IVF)和哈希(LSH)的算法。
- 查询延迟极低(Low Latency) :多层跳跃架构使得它的查询时间复杂度近似于对数级别 O(logN)O(\log N)O(logN),哪怕数据量暴增,查询耗时也不会线性突增。
- 缺点 :
- 内存消耗极大(High Memory Footprint):这是 HNSW 最大的痛点。除了要将庞大的原始高维向量放入内存计算距离外,整个多层图结构的每一个"邻居指针(边)"都需要占据可观的运行时内存空间。这也是为什么常常把 HNSW 结合前面提到的 PQ(量化)一起来用。
- 构建和更新成本高:每当一个新向量入库时,都需要经历一次复杂的自顶向下查找,并进行连接、甚至还要切断旧连接的重新布线操作。这让 HNSW 面对高并发写入(Write TPS)时比较吃力。
2. IVF (Inverted File Index)
倒排文件索引(Inverted File / Inverted Index),在向量检索场景下,这是一种经典的基于数据聚类(Clustering)的分区搜索机制。它的核心逻辑是"物以类聚",通过把庞大的数据切分为多个小区域(桶),来避免漫无目的地全局扫描。
2.1 核心思想:K-Means 聚类与分桶
如果我们要在一个百万人口的城市里找一个人,逐户敲门(KNN暴力扫描)显然太慢;更快的方法是,我们直接去这个城市特定的几个"行政区"里找。
- 聚类划分 :在数据入库建立索引时,算法会运用 K-Means 等聚类算法,将极其广阔的整个多维向量空间,硬生生地切分成 KKK 个独立的区域(称为 Voronoi 簇或 Cell/Bucket)。
- 中心点(Centroids):每一个划分出来的区域,都会计算并记录一个"区域几何中心点"。这个中心点代表了该区域内所有向量的大致方向和位置。
- 倒排映射:系统会像字典一样建立一张表------记录每一个"中心点 ID"旗下,到底包含了哪些具体的数据向量(这就是名字中 Inverted File 倒排文件的由来)。
2.2 搜索与查询过程 (Search)
- 定位大致区域 :当用户传入一个查询向量(Query)时,第一步绝不是 去和所有的底层数据算距离。算法首先让这个 Query 去和上面提到的 KKK 个"中心点"计算距离。
- 挑选最近的桶 :找出距离 Query 最近的 NNN 个中心点(这个 NNN 由查询参数
nprobe控制)。这就相当于我们在全市选定了最可能藏匿目标的几个小区。 - 桶内精搜:锁定这少数几个区域(桶)后,算法只会把这几个桶里的所有数据解包出来,与 Query 逐一计算距离,最后进行排序得出 Top-K。那些没有被选中的几百上千个桶,里面的数据连碰都不会被碰一下。
2.3 关键控制参数 (Parameters)
nlist(或 K):建索引时的参数,表示你打算把整个空间切分成多少个区域(桶)。切得越多(桶越小),未来单次搜索要遍历的数据就越少(越快),但找错区域导致漏掉正确答案的风险会上升。nprobe(探测桶数) :查询时的核心参数,指代"每次查询,我要挑选几个最近的桶进去翻找"。- 权衡 :如果
nlist=1024,你设nprobe=1,速度最快,但如果真实最近邻恰好长在隔壁的桶里,你就永远找不到它了(漏召回);如果你设nprobe=1024,那就等同于暴力全表扫描(KNN),完全没有提速效果。调整nprobe是 IVF 算法在"延迟"和"召回率"之间走钢丝的关键。
- 权衡 :如果
2.4 主要特点评估
- 优点 :
- 查询极快,吞吐量大:它从根本上通过"物理隔离"大幅削减了绝大部分不需要比较的数据量。
- 内存占用相对较小:比起图结构(HNSW)每条数据都需要保存大量邻居指针关系,IVF 只需要维护一个中心点列表和归属映射,极其省内存。
- 缺点 :
- 严重的"边界效应(Edge Effect)"带来的漏召回:向量空间的边界通常非常模糊。一个查询点虽然物理上离 A 簇的中心近,但他极有可能跟 B 簇边缘上的某条数据距离最近(仅仅一墙之隔,但跨越了聚类边界)。如果搜索时 B 簇没被选中,真正最相似的数据就会悲剧性地被漏掉。
3. PQ (Product Quantization)
即乘积量化,严格来说它不是一种直接寻找近邻的索引结构,而在本质上是一种极为高效的数据压缩与降维技术 。在向量检索工业界,通常极少单独使用 PQ,而是将其与前面的 IVF 算法缝合,组成 IVF-PQ 这一称霸大规模向量检索的利器方案。
3.1 核心思想:向量切片与密码本替换
我们知道,要保留极其精确的浮点小数(如 0.85233...)是非常占内存的。如果一条数据是 512 维的 float32 浮点数,存储它需要 512×4512 \times 4512×4 Bytes = 2KB。十亿条数据就要 2TB 内存,这极其昂贵。
PQ 的压缩哲学是:我不存你原本的浮点数,我只存你的"照片编号"。
- 切割向量 (Slicing):拿到一个原始的 512 维的超长向量,比如切成 4 段,每一小段变成了 128 维的短子向量。
- 局部聚类建立密码本 (Codebook):把所有历史数据切出来的"第一段子向量"丢在一起做 K-Means 聚类,找出最具代表性的 256 个中心点形态。这 256 个中心点就编上号(比如 ID:0 到 255)。对四段都这么做,这样我们就得到了 4 本局部"密码本"。
- 量化替换 (Quantization) :现在任何一条原始的 512 维浮点数入库,我都把它切开,用密码本里离它长得最像的那个模板片段的 ID 来替换 。这样一来,原来那长长的极度消耗内存的 512 维浮点数,现在只需要存 4 个由
[0~255]组成的极小数字(每个仅占 1 Byte)。- 压缩比惊人:2KB →\rightarrow→ 4 Bytes!
3.2 距离计算:查表法 (Look-Up Table, LUT)
既然数据被压缩成了 ID,那怎么计算查询向量与数据库某条原数据的距离呢?这里才是 PQ 的灵魂所在,它巧妙地把运行时的高强度计算 转换成了极廉价的内存读取:
- 第一步(准备小抄):当用户传入一个 Query 向量时,算法先把 Query 同步切成 4 段。此时系统还不去碰那几亿条库存数据。
- 第二步(预计算生成 LUT) :Query 的第一段,先跑去跟自己的第 1 号密码本里的所有 256 个代表模板提前死磕,分别计算出它们真实的数学距离(浮点数),并把结果登记在一张小表里(这就叫做查找表 LUT,记录着
{ 模板0: 距离5.2, 模板1: 距离2.1, ..., 模板255: 距离19.8 })。Query 剩下的 3 段也如法炮制,生成一共 4 张这样的小表。这步计算量极小(仅四段 ×\times× 256 次)。 - 第三步(极速扫大库) :现在准备工作做完了,开始大海捞针!面对库里的 NNN 亿条被压缩得只剩四个编号的数据(比如某条数据记录为
[id:12,id:135,id:3,id:200])。CPU 要计算这条数据跟 Query 的距离时:- 根本不去做几十次的乘法减法算欧氏距离。
- 它只是像查字典一样,去第一张 LUT 表翻到
ID=12那个位置,抄下它对应的距离值(比如 0.9);去第二张 LUT 表翻到ID=135的位置(比如 2.4);接着取出ID=3的缓存距离,加上ID=200的缓存距离。 - 把这四个读到的现成数字直接相加:
0.9 + 2.4 + ...即可。
这样一个原本需要执行 512 次乘法、减法、平方根的高算力过程,被魔术般地变成了:查阅 4 次表格的缓存数字并执行 3 次简单的加法。在十亿级别的数据规模上,这种 O(1) 的查表速度直接让查询性能起飞。
3.3 主要特点评估
- 优点 :
- 极限压缩内存:通过调整切分的段数(M)和每个密码本的总数(K),PQ 可以把巨大的向数据库压缩到原来的十分之一甚至百分之一,使得一台普通服务器就能塞下几亿条数据。
- 极速距离计算:耗费 CPU 的距离数学运算被预先计算并缓存在 LUT 表里,遍历时变成了极其廉价且快速的 O(1) 内存读取操作。
- 缺点 :
- 精度有损(Lossy):因为把长长的小数简化成了一个固定的"代表号",原本精微的空间距离肯定被抹平了一些。这就意味着查询出来的结果不可避免地带有一点误差(这也是为什么经常用它做广撒网的第一波粗排过滤,最后再拿取出的几十条原数据做一次精确的重排计算)。
4. LSH (Locality-Sensitive Hashing)
局部敏感哈希(Locality-Sensitive Hashing),这是一种基于概率设计的、极为轻量级的降维与快速检索算法。它的哲学是:用空间换取碰撞的概率。
4.1 核心思想:反其道而行的"哈希碰撞"
在传统计算机科学里,哈希函数(如 MD5 / SHA256)的核心诉求是"防碰撞"和"雪崩效应"------哪怕原文件仅仅改动了一个标点符号,算出来的哈希码也必须完全大相径庭。
但 LSH 偏偏反其道而行,它故意设计了一种"非常容易引发碰撞"的特殊哈希函数(常见的做法是使用多个随机的超平面,去切割高维空间):
- 局部敏感 :意思是,如果两个向量在原始高维空间中长得越像(距离越近) ,那么它们经过这个特殊哈希函数计算后,得到相同哈希值(落入同一个哈希桶)的概率就极高。
- 反之,如果两个向量离得很远,它们被哈希到一起的概率就极小。
- 就这样,原本要在高维空间里算半天的问题,被简化成了:"只要我们两哈希算出来的值一样,我们就一定是近邻"。
4.2 搜索与查询过程 (Search)
- 查表入桶 :当用户传入一个 Query 向量,不需要去逐个对比库里的数据,而是直接把 Query 塞进 LSH 哈希函数里"算一卦",得出一个哈希值(比如算出来是
Hash_9527)。 - 桶内提取 :直接去哈希表里,把
Hash_9527这个桶打开。由于设计原理保证了近邻大概率落在一起,这个桶里面装着的历史数据,就是我们要找的疑似最相似的候选集。 - 距离比对(可选):把这个桶里为数不多的几十个候选人拿出来,简单计算一下真实的距离做个排序,Top-K 就能交付了。
4.3 关键控制参数 (Parameters)
为了控制准确率,LSH 往往不是用一个哈希函数,而是用一组:
- 哈希位长(Hash Length / K):把多少个单一的哈希函数拼接成一个长哈希码。变长会让条件更苛刻,碰撞变难。这会导致单个桶里的数据变少(查询变快),但漏掉目标的概率也会增加。
- 哈希表个数(Number of Tables / L):为了防止一次算哈希失误漏找,干脆并行建立好几张无关的哈希表。查询时分别在好几张表里算哈希、找桶并取并集。表越多,没漏找的概率(召回率)越高,但内存也会成倍增加。
4.4 主要特点评估
- 优点 :
- 极快的数据写入与建库速度 :一条新数据来了,算一次哈希直接塞进桶里即可,这是 O(1)O(1)O(1) 的插入极速。它不需要像 HNSW 那样辛苦地到处认亲戚连线。它非常适合那些写入量大得离谱、源源不断的流式数据处理。
- 对超高维特征有不错的抵抗力。
- 缺点 :
- 不够精确,极其看脸(概率驱动):这就跟掷硬币一样,即使两个点很近,也有小概率不幸被哈希空间的那一刀刚好切开,分到了不同的桶里。一旦分在不同桶里,你这辈子查询都不会找到它了。
- 在工程实践中的地位退滑:在保证同样高的召回率(比如 95% 以上)前提下,HNSW 这类基于图的算法比 LSH 跑得更快且更稳定。所以,除了极端流处理场景或者学术界研究,现代主流向量数据库很少将纯血的 LSH 作为默认检索算法。