向量数据库里的 HNSW、LSH、PQ 到底在说什么?一个老程序员给你的大实话
这两年只要你在搞 AI 应用,尤其是做大模型知识库、RAG 检索、推荐系统这类东西,肯定绕不开"向量数据库"这四个字。去翻文档、看技术方案,刷几篇文章,满屏都是 Milvus、Chroma、Qdrant,然后紧接着就是 HNSW、LSH、PQ 这一堆抽象术语。我最早看到这些概念的时候也是一头雾水,每个字母都认识,放在一起完全不知道在讲什么,看官方文档能看睡着。
后来因为项目里要处理千万级别的向量检索,硬着头皮把这块啃了一遍,又把 Milvus、Qdrant 这些开源项目翻来覆去调参,才算是把这些东西串起来了。今天我就用做项目的角度,把这些概念掰开了讲清楚。这篇文章不是从文档里抄定义,而是告诉你它们到底怎么工作、适合什么场景、怎么选型,以及我在实际项目中踩过的坑。
这几种东西说白了,都是为了让计算机在成千上万、甚至上亿条向量数据里,快速找到跟你要查询的那条最相似的记录。如果你能理解这个目的,接下来的内容就不会绕晕。
1. 先搞明白:向量检索到底在干什么
1.1 万物皆可向量化
现在大模型火起来之后,文本、图片、音频这些非结构化数据,都被模型转换成了一串固定长度的数字数组,比如[0.12, 0.56, -0.23, ...],这就是"向量"。
通俗点说,向量就是数据在数学空间里的坐标。两个向量越接近,就代表它们在语义上越相似。比如你搜"小猫",模型会把"猫咪"和"橘猫"的向量拉得很近,而"卡车"的向量离得就很远。做知识库问答时,用户提问会被转成一个向量,然后拿着这个向量去库里面找最匹配的那一块文本片段,再拼给大模型做回答。这个过程就是"向量检索"。
1.2 最简单的方法为什么不可行
最原始的检索方法是暴力搜索,把所有向量挨个算一遍相似度。算法上这叫最近邻搜索(NN),效果绝对准确,但问题是计算量太大。
假如你有 1000 万条向量,来一条查询请求,就要算 1000 万次向量之间的距离,每次还涉及几十上百维的浮点运算。就算你的机器再厉害,光是一次请求就能把 CPU 打满,延迟扛不住。所以工业界真正用的是一个变体,叫近似最近邻搜索(ANN)。
ANN 不追求绝对准确,而是用"索引结构"来大幅缩小搜索范围,牺牲一点点精度,换来几个数量级的速度提升。HNSW、LSH、PQ 这三个词,本质上就是三种不同的"索引方案"。理解清楚这一点,后面你读任何数据库的文档都会轻松很多。
2. HNSW 详解:靠"图"找路的算法
2.1 从跳表到多层图
HNSW 的全称是 Hierarchical Navigable Small World,中文叫"层级可导航小世界网络"。这是目前应用最广泛、综合表现最好的索引算法,Milvus 和 Qdrant 的默认索引基本都是它。
它解决的核心问题是:怎么在一堆"点"里面快速找到目标点。你可以想象成在一个陌生城市里找人,你不可能挨家挨户敲门,而是先去主干道询问方向,再到次干道细化,最后进入小胡同精确定位。HNSW 正是基于这个思路设计了多层图结构。
上面几层是最稀疏的,连接的都是距离很远的点,像一个城市的几条主干道。下面几层是密集的网络,精确连接每个点之间的关系。查询时从最顶层出发,在每个层内通过"贪心算法"找最近的点,然后逐步下探到下一层继续找,直到到达最底层。整个过程就像从高空俯瞰城市,一层层缩小包围圈,最后精准定位到目标。
2.2 关键词参数和实验数据
HNSW 有两个非常重要的参数,一个是M,代表每个节点最多连接的邻居数;一个是efConstruction,表示建图时考虑候选集的大小。
我在实际项目中测试过不同参数对检索效果的影响,这里整理了一些经验值供参考:
| 参数 | 设置 | 效果感受 |
|---|---|---|
| M = 16,efConstruction = 200 | 默认配置,适合大多数人 | 索引大小适中,召回率约95%以上,速度不错 |
| M = 32,efConstruction = 400 | 高精度偏好 | 召回率提升至98%以上,但内存占用几乎翻倍 |
| M = 8,efConstruction = 100 | 低资源环境 | 召回率降到90%左右,速度更快,索引体积小 |
另外还有一个efSearch参数,是查询时的候选大小,这个值越高召回率越高,但查询延迟也会上升。经验是efSearch通常设置在 100 到 300 之间,效果比较均衡。
2.3 优点和致命的短板
HNSW 的优势是查询速度快、召回率高、无需训练阶段。数据来了直接就能建索引,这对增量更新特别友好。但它的缺点也很明显:内存开销非常大。因为你要把整张图和所有节点的连接关系都加载到内存里,千万级别的向量就能吃掉几十 GB 内存。
如果你做的是亿级别的数据量,除非内存非常宽裕,否则不建议直接用原版 HNSW,需要考虑 PQ 或者磁盘索引方案来配合。
3. LSH 详解:给向量加"签名"的哈希魔法
3.1 哈希函数的奇妙性质
LSH 全称是 Locality-Sensitive Hashing,中文叫"局部敏感哈希"。它的思路跟传统哈希完全不同。
传统的哈希函数(比如 MD5)追求的是:输入哪怕有一点不同,输出结果也会天翻地覆。但 LSH 追求的是:输入越相似,输出的"指纹"(哈希值)越有可能一样。简单来说,LSH 会给每个向量算出一串固定长度的"签名",签名相同的向量大概率是近邻。
打个比方,两个人去了同一家健身房、吃同一家餐厅、喜欢同一个乐队,那他们大概率住在同一个城市。LSH 就是用这种"特征相似性"来做预归类的。算完签名之后,LSH 会把拥有相同签名的向量放到同一个桶里,查询时只查这个桶,其他桶就直接跳过。
3.2 LSH 的代价
LSH 最大的问题在于"召回率不稳定"。因为签名是概率性的,有时候明明相似的两个向量,由于哈希函数投影方向的原因,会被分到不同的桶里,就永远检索不到了。
我曾在大概 500 万条数据上跑过一个 LSH 实验,跟 HNSW 做对比。数据是 768 维的文本向量,结果如下:
| 方法 | 召回率@10 | 平均查询延迟 |
|---|---|---|
| HNSW (默认参数) | 97.2% | 8.6 ms |
| LSH (32 位签名) | 86.5% | 12.4 ms |
| LSH (64 位签名) | 90.8% | 19.7 ms |
从测试数据看,LSH 的优势并没有想象中那么大,反而是调整签名位数让性能波动比较明显。位数高召回好,但存储和计算开销也随之变大;位数低又不准。
3.3 什么时候该用 LSH
虽然综合表现不错,但 LSH 并没有 HNSW 那么全面。它廉价的优势在于:支持海量数据的内存受限场景、不需要像 HNSW 那样维护复杂的图结构、适合分布式系统做预分区。
比如做重复图片检测,或者大规模文本去重,用 LSH 先粗筛出候选集,再用精确的距离计算做二次精排,效果会很好。如果你在做向量检索的全流程架构,把 LSH 用在"粗筛"阶段、配合更精确的算法做"精排",是比较常见的做法。
4. PQ 详解:压缩存储的量化方案
4.1 乘积量化的核心思想
PQ 全称是 Product Quantization,中文叫"乘积量化"。HNSW 和 LSH 解决的问题是"怎么快速找",但 PQ 解决的问题是"怎么省空间地存"。
我们知道,向量数据库的查询瓶颈经常不在计算,而在内存带宽和磁盘 IO。当数据量大到内存装不下时,性能就会急剧下降。PQ 的思路就像是把高清照片压缩成 JPEG:把每个高维向量切成若干段,每一段用"聚类中心"来代替,从而压缩存储体积。
假设一条 768 维的向量,每个维度是 4 字节的浮点数,总共占用 3072 字节。用 PQ 把它切成 96 段,每段选出一个 256 个聚类中心里的代表 ID,每段只需要一个字节存储。压缩后占用的空间为 768 字节(96 段乘以 1 字节),直接压缩了 75% 的体积。
4.2 完整的PQ流程
PQ 分为训练、编码、查询三个阶段:
- 训练阶段:从数据集中抽样一部分向量,把每个子向量段做 K-Means 聚类,生成"码本"。这个码本相当于一本字典,里面存着所有聚类中心的向量。这个阶段必需提前完成。
- 编码阶段:把所有向量按照码本转换为对应的聚类中心 ID,原始向量可以丢弃。
- 查询阶段:用户查询向量无需编码成 ID,而是将查询向量切成同样的段,跟每个聚类中心做距离计算,生成一个"距离查表",再根据每个候选向量的 ID 组合出近似距离。这种方式叫非对称距离计算(ADC),因为查询向量和库向量处在不同的表示空间中,但计算精度比双方都压缩要高得多。
4.3 PQ 的致命问题和改进
PQ 最核心的问题是因为压缩导致的信息损失。想象一张高清照片被压缩成马赛克,个别细节肯定丢失了。在向量检索的场景中,PQ 的召回率比 HNSW 低不少,特别是数据的分布比较零散时更明显。
后来工业界搞出了 PQ 的许多变种,比如 OPQ(Optimized Product Quantization)通过旋转矩阵让每段的数据分布更均匀,IVF-PQ 则是先用聚类做粗筛再在桶内做 PQ 精算。Milvus 里的IVF_PQ就是这个思想,先用倒排索引粗筛出接近的桶,再在桶内做编码匹配。
5. 三者对比与选型实战建议
5.1 一张表看明白差异
为了照顾新朋友,我先明确一个认知框架:在向量数据库里,HNSW 是一种"图索引",LSH 是一种"哈希索引",PQ 是一种"量化索引",三者解决的重点不同。把它们放在一起对比:
| 维度 | HNSW | LSH | PQ |
|---|---|---|---|
| 核心思路 | 多层图导航搜索 | 相似哈希分桶 | 向量压缩量化 |
| 适合数据量 | 千万级以下 | 大规模数据预筛 | 亿级海量数据 |
| 内存占用 | 高 | 中 | 低 |
| 查询精度 | 高 | 中低 | 中低 |
| 索引构建速度 | 慢(建图要吃资源) | 快 | 快(聚类较耗,之后很快) |
| 是否需要训练 | 不需要 | 不需要 | 需要 |
| 常见场景 | 通用检索,默认首选 | 去重、粗筛、分布式 | 超大存储量场景 |
5.2 结合 Milvus、Chroma、Qdrant 怎么选才不踩坑
如果你用的是 Milvus,它的架构比较灵活,支持多种索引。默认配置下我建议直接用 HNSW,因为它是内存索引,性能最稳定。用hnsw时,我建议把M调到 16 左右,太高会浪费内存。如果向量数据超过千万,先把index_type换成IVF_PQ,用训练好的码本来降低内存压力,但一定要用小批量数据提前测试召回率,别等上线了才发现不准。
如果你用的是 Chroma,它比较轻量,通常用于本地开发或原型验证。Chroma 的底层实现基于 HNSW,封装得比较好,但它的设计目标是开发便捷,而不是处理亿级海量数据。数据量超过几百万条时,Chroma 的性能下滑比较明显。我的经验是它很适合做个人知识库或者小团队内部工具,数据量大了赶紧迁移到 Milvus 或 Qdrant。
如果你用的是 Qdrant,它默认索引也是 HNSW,但 Qdrant 有个特色:支持配置使用二进制量化(Binary Quantization)或者标量量化(Scalar Quantization)来压缩向量。这些方案本质上是 PQ 思想的变种。在 Qdrant 里调节quantization_config参数,比如设置scalar类型,可以把内存占用降低 4 倍左右,但召回率可能下降 0.5 到 2 个百分点。如果你的业务对精度要求不是极端高,这个取舍非常划算。
5.3 混合索引是工业级常规操作
真正做得成熟的系统,一般不会只依赖一种索引,而是采用混合策略。
最常见的方案是"粗筛 + 精排"。用 PQ 或者 IVF 把海量数据快速缩小到几千条候选集,然后用 HNSW 在这些候选里做精细检索,再用精确的向量距离计算做最终排序。这就像你先用百度地图找到某个城市,再开车到街道,最后敲门找人,一层比一层精确。
还有一种是"按场景拆分"。比如在推荐系统里,召回阶段用的是HNSW,追求速度;精排阶段用的是暴力计算(因为候选集已经很小);而长期存储和备份数据用 PQ 压缩格式存盘。这套组合拳既保证了效果,也控制了成本。
6. 实操中的常见问题与避坑经验
6.1 索引参数不是越大越好
很多人第一次用 HNSW,恨不得把M设成 64,efConstruction设成 1000,以为精度越高越好。我刚开始也干过这事,结果一个 100 万条的数据集,建索引就花了半小时,内存直接飙到 8 GB,查询速度也没快多少。
后来我总结了一个经验公式:先按M = 16起步,efConstruction = 200,测一下召回率。如果召回率不够,先调efSearch(查询参数),效果不明显再往上调M。千万别一上来就把所有参数拉满。参数越高,索引构建时间和内存占用是指数级上升的。
6.2 PQ 训练样本的选择
PQ 需要训练码本,训练集怎么选决定了后续量化效果的好坏。如果训练集跟真实分布差异大,比如用了全是英文的数据训练,上线后却要去检索中文数据,那聚类中心就偏了,码本表达力会很差,检索精度惨不忍睹。
我的建议是训练集的采样量至少覆盖 1% 到 5% 的总数据量,而且必须跟线上数据同分布。实在不行,可以用全量数据中随机抽样的方式来做训练,虽然训练时间长一点,但稳。如果线上数据是增量式的,隔一段时间需要重新训练一次码本,否则数据分布漂移后精度会慢慢劣化。
6.3 召回率指标别只看 Top1
在实际项目中评估这三种方案,不能只看 Top1 准不准,要看recall@10、recall@100这些指标。因为向量检索通常是第一轮筛选,后面可能还有重排模型兜底。只要 Top100 里包含正确结果,重排模型就能捞回来。所以宁可牺牲一点 Top1 的精度,也要保证 Top100 的高召回,这才是让整体系统效果最稳的做法。
我见过团队为了追求单条结果的绝对正确,把索引参数调到极高,结果整体延迟从 50ms 升到 300ms,用户根本不买账。实际上多数场景 90% 到 95% 的召回率已经完全够用,把剩下交给精排才是工程化的正确思路。
6.4 别忽视距离计算方式的坑
还有一个容易被忽略的点:不同类型索引默认使用的距离度量不一样。有的库默认是余弦距离(Cosine),有的是欧氏距离(L2),有的支持内积(IP)。如果你换了一个向量数据库,没有统一修改距离度量,看起来索引建得很正常,但检索结果怎么都不对。
建议无论用 HNSW、LSH 还是 PQ,都要检查两个东西:一是向量的归一化情况,二是库的metric_type配置。文本向量用余弦距离更合适,图片向量有时用 L2 更好。如果选错了距离函数,就算索引算法再好,效果也差得离谱。
6.5 从日志与监控中定位问题
我在生产环境里维护向量服务时,遇到过线上召回率突然下降、但索引没有重建的情况。最后排查出来是同一批向量的 embedding 模型悄悄更新了,新出的向量分布跟旧索引不一致。这种问题靠代码看大概率发现不了,必须对 embedding 版本做严格管理。升版本后要对存量数据统一重算向量并重建索引。
另一个容易出问题的点是并发。HNSW 这种图结构在高并发写入时可能会出现锁竞争,写入和检索互相卡顿。后来我调整为尽量做批量写入,或者把写入压力分散到索引副本上,线上才稳定下来。
最后再分享一个小技巧:如果你刚开始做向量检索选型,不确定用 HNSW 还是 PQ,先用开源库跑个基准测试。拿你自己业务里最典型的一万条数据,分别建索引,压测一下QPS和recall@10。十分钟就能出结果,远比你读三天文档猜参数靠谱。我实际测下来,大部分日活百万以下的应用,一个配置合理的 HNSW 就完全够用了,真正需要折腾 PQ 的量级,一般是千万级以上的大系统。这两者就像是城市里的出租车和地铁——出租车灵活直达,但一到高峰期就堵;地铁容量大、稳定,但你必须先走到地铁站(付出训练和量化成本)。认清你自己的数据量级和业务需求,选型就不会出大错。