跳转到内容
Akatshi's Blog
返回

向量数据库索引怎么选?聊聊 IVF 和 HNSW

向量数据库里存了几百万条数据,每次查询都像在大海捞针,速度慢得让人想摔键盘。原因很简单——向量相似度搜索本质上是在高维空间里找最近的邻居,如果暴力比对每一条数据,计算量是 O(N),数据量一大就彻底扛不住。

所以我们需要索引,用一些巧妙的策略来减少需要计算的距离次数。今天来看几个最主流的索引类型:FLAT、IVF、HNSW 和 LSH。它们思路完全不同,各有各的特性,用对了能让查询快如闪电,用错了也可能让你怀疑人生。

Table of contents

Open Table of contents

FLAT:暴力搜索,精度满分但速度感人

FLAT 其实不算是一种“索引”,它就是把所有向量原封不动地存下来,老老实实拿查询向量和每一条数据算距离,然后排序返回最近的几个。没有任何预处理,没有任何近似。

优点和缺点都极其纯粹:

所以 FLAT 只适合那种数据量很小(几万条以内)、对精度要求极其苛刻、查询频率也不高的场景。比如你刚搭了一个原型系统,数据还没多少,先用 FLAT 顶着;一旦数据规模上来,FLAT 就得退役了。

IVF:先把空间划分成小区,只搜最近的那几个

IVF 全称 Inverted File Index,倒排索引。思路很朴素:把整个向量空间划分成很多个小的区域(聚类),每个区域有一个中心点。查询的时候,先找到离查询向量最近的几个中心点,然后只在这些中心点对应的区域里搜索候选向量,其他的直接不看。

IVF聚类示意图 IVF查询示意图

打个比方,这就像图书馆把书按学科分类。你想找一本机器学习相关的书,不需要翻遍整个图书馆,只要走到“计算机科学”那个书架附近,再在几个相关的架子上找就行了。当然,这种粗分类可能会让你错过一本被放错位置的好书,但效率提升是实打实的。

假设有以下文档集合: doc1: “Hello world” doc2: “Hello Akatshi” doc3: “Hello IVF”

构建IVF索引后,结果如下:
Hello: [doc1, doc2, doc3]
world: [doc1]
Akatshi: [doc2]
IVF: [doc3]

具体来说,IVF 有两个关键参数:

nlist 越大,每个区域里的数据越少,查询时如果只探测少数几个区域,速度就越快,但召回率可能下降,因为可能漏掉隔壁区域里真正最近的邻居。nprobe 越大,探测的区域越多,召回率越高,但计算量也上去了。

这就像在“快”和“准”之间架一杆秤,需要根据自己的业务需求找到那个平衡点。

在 Milvus 中主要有如下 IVF 索引类型,主要区别点在压缩率提升与精度平衡:

索引类型构建参数搜索参数压缩技术内存占用召回率
IVF_FLATnlist(范围[1,65536],默认128)nprobe(范围[1,nlist],默认8)、max_empty_result_buckets(范围[1,65535],默认2)无压缩最高(与原始数据相近)最优
IVF_SQ8nlist(范围[1,65536],默认128)nprobe(范围[1,nlist],默认8)、max_empty_result_buckets(范围[1,65535],默认2)标量量化(SQ),将FLOAT(4字节)转换为UINT8(1字节)中等(减少70-75%内存消耗)次之
IVF_PQnlist(范围[1,65536],默认128)、m(乘积量化因子数,需满足dim mod m=0)、nbits(范围[1,64],默认8)nprobe(范围[1,nlist],默认8)、max_empty_result_buckets(范围[1,65535],默认2)乘积量化(PQ),将高维向量分解为低维子向量并量化最低最低

IVF 的优缺点很鲜明:

第一次用 IVF 的时候,我图省事把 nlist 设得很大,nprobe 设得很小,结果查询是快了,但召回率掉到 70% 多,业务上完全没法用。把 nprobe 调上去,速度慢得跟暴力搜索差不多。这参数调起来或许是一门玄学(

HNSW:多层图结构,像坐电梯一样快速逼近目标

HNSW 全称是 Hierarchical Navigable Small World。它的思想可以类比成跳表(Skip List),但用在了图结构上。 HNSW架构示意图 简单来说,HNSW 构建了一个多层的图。最底层包含所有数据点,每往上一层,节点数量就指数级减少,但节点之间的连接更“长”,能跨越大段距离。查询的时候,从最高层开始,沿着边做贪心搜索,快速跳到离目标近的区域;然后下降到下一层,再继续贪心搜索,逐步逼近。到了最底层,再在局部小范围内精细搜索。

这有点像你在一座陌生的城市找一家小店。先看大地图,开车上高速,快速接近目标区域;到了附近,换小路,慢慢找。HNSW 的层级设计就是让你在高层快速缩小范围,在低层保证精度。

HNSW 也有几个核心参数:

和 IVF 不同的是,HNSW 不需要预先划分聚类,它是靠图的结构来导航的。所以它天然适应数据分布的不均匀性,不会因为某个聚类太大而拖累查询。

HNSW 的优缺点:

用 HNSW 最直观的感受是:一旦建好索引,查询是够快,够准。建索引的过程让人十分折磨,尤其是几百万条数据的时候,看着进度条慢慢爬。而且内存吃得厉害,我 48G 的机器差点不够用。

LSH:局部敏感哈希,用哈希函数把相似向量撞进同一个桶

LSH 全称 Locality-Sensitive Hashing,局部敏感哈希。它的思路和前面几种完全不同:不靠聚类,也不靠图,而是靠哈希函数。

普通的哈希函数希望把不同的输入尽可能均匀地散开,哪怕输入只差一个字符,输出也天差地别。但局部敏感哈希反其道而行之:它设计出一类特殊的哈希函数,让相似的向量经过哈希后更有可能落到同一个桶里。这样查询的时候,只需要计算查询向量的哈希值,找到它所在的桶,然后只在这个桶(或者邻近的几个桶)里做精确比对。

打个比方,这就像把每个人按照大致身高分成几个组:150-160 一组,160-170 一组,等等。你想找一个 165 左右的人,直接去 160-170 那个组里找就行,不用满世界跑。虽然这个分组很粗糙,可能漏掉一些边界上的人,但效率极高。

LSH 的实现方式有很多种,常见的有基于随机投影、基于量化等。它的关键参数通常包括:

LSH 的优缺点:

LSH 在工业界曾经风光过一阵,后来随着 HNSW 这类图方法的兴起,在一些场景下被替代了。但在超大规模、内存极度受限、或者对延迟要求极高的场景下,LSH 依然有自己的用武之地。

到底选哪个?

这些索引没有绝对的优劣,关键看你的约束条件是什么。我整理了一下自己的经验:

什么情况下选 FLAT:

什么情况下选 IVF:

什么情况下选 HNSW:

什么情况下选 LSH:

实际使用中会把不同的索引结合起来,如先做 IVF 粗筛,再做精确计算,或者用 PQ 压缩向量来降低内存。但单从 FLAT、IVF、HNSW 和 LSH 这几个基础索引来说,上面的判断基本够用。

调参的一点小建议

对HNSW,把 M 设成 16 或 32,efConstruction 设成 200 左右,efSearch 设成 100 或更高,然后跑一下测试集看召回率和延迟。HNSW 对参数不算特别敏感,默认值通常能拿到不错的结果。

IVF 的话,nlist 一般设置在 sqrt(N) 到 4*sqrt(N) 之间,比如 100 万数据,nlist 设 1000 到 4000。nprobe 从 10 开始往上调,观察召回率和延迟的变化。注意 nlist 太大或太小都不好,需要根据数据分布调整。

LSH 的调参相对麻烦,因为哈希函数的选择和参数设置跟数据分布、距离度量强相关。应该先从简单的随机投影 LSH 开始,逐渐增加哈希函数个数和哈希表个数,在召回率和内存之间找一个平衡点。如果效果不理想,再考虑换其他 LSH 变体。

Warning

最终参数要用你自己的数据和查询分布来评测。我这的参数只能参考,你的数据可能完全不一样。

总结

FLAT、IVF、HNSW 和 LSH 代表了四种不同的索引哲学:

希望这篇分享能帮你在选索引时少走点弯路。如果你也在调这些参数,欢迎留言交流,我很好奇大家怎么在“快”和“准”之间做取舍。


分享这篇文章:

上一篇
文本生成里的采样参数三件套:top-k、top-p 和 temperature