news 2026/9/13 5:52:56

向量数据库底层揭秘:ANN、HNSW、LSH、PQ算法解析与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
向量数据库底层揭秘:ANN、HNSW、LSH、PQ算法解析与实战

向量数据库的底层秘密:ANN、HNSW、LSH、PQ 到底在解决什么问题?

这两年做 AI 应用,你会发现一个尴尬的事实:模型能力上来了,但你的知识库、记忆体、检索模块还停留在“关键词匹配”的阶段。无论是给大模型接企业知识库,还是做 AI 智能体的长期记忆,最终都绕不开一个问题:怎么在海量向量数据里,快速找到“最相似”的那一批?

向量数据库就是干这个的。它的核心不是存数据,而是用一套高效的索引结构和检索算法,把“暴力遍历所有向量”这件事变得尽可能快。你去看市面上主流的向量数据库,Milvus、Qdrant、Weaviate、Redis 的向量模块,底层说到底都是在对 ANN(Approximate Nearest Neighbor,近似最近邻)算法做工程化封装。

这篇文章,我尽量用大白话加实操视角,把这套东西的核心讲清楚。包括最常用的 HNSW、经典的 LSH、工业界标配的 PQ(乘积量化),以及最容易踩坑的相似度度量问题。看完之后,你应该能搞清楚:为什么向量数据库这么快、HNSW 到底好在哪里、什么场景该用哪个算法,以及当你往项目里引入向量检索时,哪些参数直接决定系统能不能跑起来。

这篇文章适合刚接触向量检索的开发者,也适合已经在用 Milvus 之类产品、但对内部机制一直“知其然不知其所以然”的朋友。

1. 向量数据库到底是个什么东西

先把你对“数据库”的固有印象放一边。

传统关系型数据库,存的是结构化数据,比如用户表、订单表,查找靠的是 B+ 树索引或者哈希索引。传统全文搜索引擎(比如 Elasticsearch),存的是文档,查找靠的是倒排索引。它们的共同点是:匹配逻辑是基于“精确值”或“关键词”的。

向量数据库不一样。它存的是向量——也就是一组浮点数。比如你把一篇文章用 OpenAI 的 embedding 模型转成一个 1536 维的向量,或者用 BGE 模型转成一个 1024 维的向量,这个向量本质上就是这段文本在高维空间里的“坐标”。

问题来了:怎么从这个坐标系统里找到“语义最接近”的另一个坐标?你不能像 SQL 那样写WHERE vector = ?,因为语义相似不是精确匹配,而且向量也不可能完全相等。真正的做法是算距离:两个向量在空间里距离越近,语义上就越接近。

所以向量数据库的检索逻辑,本质上是一个 KNN(K 近邻)问题:给定一个查询向量,在 N 个向量里,找出距离最近的 K 个。最笨的办法是全表扫描一一比较,N 如果是一百万,每次查询都要算一百万次距离,虽然现代 CPU 算距离不慢,但问题是你的业务不可能只承载一次查询,QPS 一上来就崩。

于是有了 ANN,也就是近似最近邻。核心思想很简单:不追求百分之百找到真正的最近邻,而是用“足够接近”的结果,换取数量级上的性能提升。这就像你在一个十万人城市找一个人,不挨家挨户敲门,而是先通过“同姓”“同小区”“同年龄层”这些特征快速缩小范围。ANN 的“缩小范围”策略,就是各个算法百花齐放的地方。

1.1 一次完整检索流程:召回、粗排、精排

实际工程里,向量召回通常不是单独用的。以企业知识库问答为例,典型流程是:

  1. 用户提一个问题,比如“我们的报销流程是什么?”
  2. 把问题文本通过 embedding 模型转成查询向量 q。
  3. 在向量数据库里用 ANN 索引召回 top-100 候选片段(注意,这里已经近似了,不是全量比对)。
  4. 有可能再结合关键词检索(BM25)做混合召回,把语义和字面两个维度的结果合并。
  5. 最后有一个重排(rerank)环节,用更精细的模型(比如 cross-encoder)在这 100 条候选里精排,选出 top-5 喂给大模型。

认识到这个流程很重要,因为它决定了你对向量数据库的“要求”。在实际项目中,向量数据库保证的是召回阶段的“查得全”和“速度快”,它会故意召回多一些候选(比如 100 条),把精度问题丢给后面的精排去解决。这也是为什么很多向量数据库的默认 topK 设置都不会太小。

1.2 相似度度量怎么选:内积、余弦、欧氏距离

很多人上手向量数据库,第一个忽略的问题就是距离度量。你以为这不重要,实际上它直接影响你能不能召回正确结果。

  • 欧氏距离(L2):计算的是空间中的直线距离,数值越小越相似。它适合向量经过归一化处理的场景,对向量绝对位置敏感。
  • 内积(IP):数值越大越相似。适合向量没归一化、长度本身携带信息量的场景,比如某些推荐场景中向量模长代表热度。
  • 余弦相似度:衡量的是向量方向的一致性,范围是 -1 到 1,值越大越相似。它只关心方向,不关心模长,是文本语义场景用最多的。

关键坑在于:很多向量数据库的索引结构是为特定的距离度量设计的。比如 HNSW 在实现时,距离函数的选择会影响图的构建过程。你在 Milvus 里如果在创建索引时选了 L2,在查询时又指定用 IP 去算,返回的结果就是错的。我自己就见过同事在 Milvus 里建 collection 时用了 L2,然后查询时用 cosine 相似度去过滤分数小于 0.8 的结果,直接导致召回为空。

文本场景,我给你的建议是:embedding 模型如果没做归一化,优先用余弦;如果你的 embedding 本身已经 normalize 过了,那用内积其实等价于余弦,而且计算更快。欧氏距离在向量值本身有意义、需要区分绝对大小的时候更合适。

2. 三类最主流的 ANN 算法,思路完全不同

2.1 暴力搜索:所有方案的基线

在讲聪明算法之前,先提一下暴力搜索(Flat 索引)。所谓暴力搜索,就是不管索引结构,直接在全部向量上遍历计算距离,然后取最小。它是所有 ANN 算法的精度上限和速度下限。

  • 优点:100% 召回率,也就是不丢结果。
  • 缺点:数据量稍大就扛不住。

在 Milvus 里,对应的是 FLAT 索引类型。适合数据量很小(几万条以内)、对精度要求严格、或者作为 Baseline 测试的场景。实际项目里,我一般不推荐直接用 FLAT,除非你的数据量真的小到无所谓。

2.2 HNSW:当前综合体验最好的图算法

HNSW(Hierarchical Navigable Small World,分层可导航小世界图)的思想来源,可以追溯到“六度分隔”理论,也就是小世界网络。在实际图结构里,每个节点连接其邻居,只要连接关系足够合理,从任何一个点出发,几步之内就能到达目标附近。

HNSW 的聪明之处在于“多层”。

想象一个只有一层的图网络。如果这个图里每个节点都只连接最近的两个邻居,那么从起点到目标,路径可能会很长,因为路上只能一家一家地“跳”。但如果把图的尺度拉开——上层是很稀疏的“远距离通道”,下层是很密集的“短距离连接”,就可以做到:在高层快速接近目标区域,再到低层精细定位。

图 1(示意):

  • 第 0 层:包含所有向量,连接细密,负责精确定位。
  • 第 1 层:包含约 1/M_l的节点,连接更稀疏,负责快速跳过无关区域。
  • 第 2 层:更稀疏,属于“高速公路”。

这就是 HNSW 的“分层”含义——它结构上非常像跳表。构建时,每个节点会以指数衰减概率分配一个层数;搜索时,从顶层开始,在当前层的邻居里贪心找“距离查询向量最近的邻居”,然后下沉到下一层重复这个过程,直到第 0 层。

几个关键参数,我直接给经验值:

参数含义建议值
M每个节点的最大连接数,控制图的密度16~32,常见 16
efConstruction构建图时的动态候选集大小,越大图质量越高但构建越慢100~200,常用 200
efSearch查询时动态候选集大小,越大召回率越高但延迟越高查询时动态调整,常用 64~256

我踩过的一个很深坑:efSearch设太小。项目里用了 HNSW,结果召回率一直不达标,折腾半天,最后发现只是查询时 efSearch 设成了 10。其实 HNSW 的设计里,M决定索引构建质量,efSearch决定查询质量。召回率不够,优先调大efSearch,而不是去重建索引。

HNSW 的缺点也很明确:索引全在内存里,内存占用偏高;增量插入性能差一些。这在数据千万级以上时尤其明显,内存翻好几倍。另外如果你的数据流经常有大量实时删除和更新,HNSW 也显得笨重——它删除节点后图结构不会自动优化,需要周期性重建。

2.3 LSH:用哈希把“近邻”分到同一个桶

LSH(Local Sensitive Hashing,局部敏感哈希)的思路和 HNSW 完全不同,它是给向量做“哈希分桶”,但是呢,这个哈希函数是精心设计的,满足一个特性:两个越相似的向量,哈希到同一个桶的概率越大。

这和普通哈希正好相反——普通哈希要求“哪怕只差一位输入,输出也天差地别”,而 LSH 要求“越相似,哈希值越接近”。

以文本向量为例,最朴素的 LSH 实现是随机投影法(Random Projection)。它的哈希函数是随机生成一组超平面,然后判断向量在超平面的哪一侧。多个超平面组合成一个哈希值。相近的向量,大概率在每个平面上的相对位置一致,因此哈希值一致,被分到同一个桶。

查询时,只需要计算查询向量的哈希值,然后去对应的桶里做精确匹配就行。复杂度从 O(N) 降到了 O(桶内数量),大幅提高速度。

但 LSH 的问题也很明显,实际项目里我很少直接用它做主索引:

  1. 召回质量不稳定。哈希分桶是概率性的,而且桶与桶之间有“边界效应”——两个很相似的向量刚好落在桶边界两侧,就可能被分到不同桶。理论上可以通过多表(多个 LSH 函数并行)缓解,但代价是存储量乘以表数。
  2. 对高维数据不友好。维度越高,随机投影越难保持局部性,需要更多的哈希表才能维持召回率,内存直接起飞。

现在的向量数据库里,LSH 更多是作为某些特定场景的补充方案,比如均匀分布的低维向量,或者数据分布比较规律的情况。主流产品的默认索引里,很少是 LSH。

2.4 PQ:压缩向量,用查表代替计算

PQ(Product Quantization,乘积量化)的思路和前面两个都不一样,它不做索引结构,而是做“向量压缩 + 距离查表”。它的核心思想是,把高维向量拆成若干个子向量,对每个子向量空间单独做聚类(比如 KMeans),然后用聚类中心的编号来“编码”原始向量。

举个例子。一个 128 维的向量,拆成 8 段,每段 16 维。对每一段做 256 个聚类中心,也就是每一段可以用 8 个 bit 编码(2^8=256)。这样一来,一个 128 维向量(32 个 float,128 字节)可以被压缩成 8 个 byte。查询时的距离计算也不用重建完整向量,而是用“查表”的方式:把查询向量对应段与聚类中心的距离预先算好,存成表格,再根据目标向量的编码,从表里查出近似距离。

实际工程中,直接用 PQ 的情况少,更多是配合倒排索引使用,这就叫 IVF-PQ(Inverted File with Product Quantization)。流程是:

  1. 先对整个数据集做一次粗聚类(比如 KMeans,聚类数设为 nlist)。
  2. 每个向量归属一个聚类中心,建立倒排列表。
  3. 查询时,先找到查询向量最近的几个聚类中心(nprobe 参数控制搜几个簇),只在这几个簇内部用 PQ 做精确重排。

这在 Milvus、Faiss 里是最常见的工业级方案。它最大的优点是内存占用极小,适合亿级以上的大规模场景。缺点则是精度有损,尤其当编码数(m)太小的时候,召回质量会明显下降。

2.5 三种算法横向对比

为了让你更直观地选择,我做了一张表:

算法核心思路内存占用查询速度召回精度适用场景
HNSW多层图,近似最短路径极快千万级以下,对召回率要求高的在线场景
LSH哈希分桶快(依赖桶内数据量)中低极大规模且对精度要求不苛刻,或低维数据
IVF-PQ倒排 + 向量压缩快(依赖 nprobe)亿级以上,内存有限,允许一定误差

这不是绝对的,因为实际工程里还有各种魔改版本,比如 Milvus 的HNSW_SQHNSW_PQ等混合索引,目的就是结合 HNSW 的精度和 PQ 的内存优势。但作为入门,先把这三个核心思路吃透,后面看什么都顺。

3. 动手实战:用 hnswlib 搭一个可运行的向量检索 Demo

原理讲再多,不如跑一次代码。

这里我用一个非常轻量级的 Python 库——hnswlib,它是 HNSW 算法的 C++ 封装,接口简单,性能很好。同时用numpy生成模拟的 embedding 数据来演示整个构建和查询过程。按下面的步骤一步步来。

3.1 准备环境与数据

pip install hnswlib numpy

生成 10 万条 128 维的模拟向量数据:

import numpy as np import hnswlib dim = 128 num_elements = 100000 # 生成随机向量,模拟 embedding 数据 data = np.random.random((num_elements, dim)).astype(np.float32) # 生成 100 条查询向量 queries = np.random.random((100, dim)).astype(np.float32)

3.2 构建索引

# 初始化索引 # space 可选 'l2'(欧氏距离)、'ip'(内积)、'cosine'(余弦) p = hnswlib.Index(space='cosine', dim=dim) # 初始化索引结构 # max_elements 是索引最多容纳的元素数量 # ef_construction 是上面讲过的构建参数 p.init_index(max_elements=num_elements, ef_construction=200, M=16) # 添加数据 p.add_items(data) # 设置查询时的 efSearch 参数 p.set_ef(64)

每一步在干什么,说明一下:

  • space='cosine':告诉索引用余弦距离来计算向量之间的远近。如果换成 L2,同样的 HNSW 图,边的“距离”衡量方式会完全不同。
  • M=16:每个节点最多连 16 个邻居。M 越大,图越密,查询越慢,召回率越高;M 越小,图越稀疏,查询越快但容易丢邻居。
  • ef_construction=200:插入每个节点时,为了找到合适的邻居,会先找 200 个候选,然后从中择优。这个值越大,构建越慢,但图质量越高。

3.3 查询与评估召回率

# 查询前 10 个最近邻 labels, distances = p.knn_query(queries, k=10) # 输出第一条查询的结果 print("第一个查询向量的 top-10 结果索引:", labels[0]) print("对应的距离(余弦距离,越小越相似):", distances[0])

要直观感受 HNSW 的“近似”到底损失了多少,可以做一个召回率测试:先构建一个 FLAT 暴力索引,查询同一个 top-10 集合,然后计算 HNSW 的 top-10 与暴力搜索 top-10 的重合度。

# 用暴力搜索作为 ground truth(精确结果) flat_index = hnswlib.Index(space='cosine', dim=dim) flat_index.init_index(max_elements=num_elements, ef_construction=num_elements, M=16) flat_index.add_items(data) # 把 ef 设到最大的等效暴力搜索 flat_index.set_ef(num_elements) flat_labels, _ = flat_index.knn_query(queries, k=10) # 计算召回率 hit = 0 total = 0 for i in range(len(queries)): hit += len(set(labels[i]) & set(flat_labels[i])) total += 10 recall = hit / total print(f"HNSW 召回率:{recall:.2%}")

正常跑下来,召回率一般在 95% 以上。如果调低ef或者M,速度上去了,但召回率会掉下来。这就是精度与性能的权衡,这个代码完全可以拿来做实验,直观感受参数对召回率的影响。

3.4 换个方案试试:用 Faiss 实现 IVF-PQ

再看一下工业级方案 Faiss 怎么实现 IVF-PQ,这样你对工程里的主流方案有直观感受:

pip install faiss-cpu
import faiss import numpy as np dim = 128 num_elements = 100000 data = np.random.random((num_elements, dim)).astype(np.float32) # 使用 IVF-PQ:1024 个聚类中心,每个向量编码成 16 字节 nlist = 1024 m = 16 quantizer = faiss.IndexFlatIP(dim) index = faiss.IndexIVFPQ(quantizer, dim, nlist, m, 8) index.train(data) index.add(data) # 查询时搜索最近邻的 8 个聚类中心 index.nprobe = 8 D, I = index.search(data[:5], k=10) print(I)

这里的逻辑是:nlist=1024意味着把数据聚成 1024 个簇,m=16意味着把一个 128 维向量切分为 16 段,每段 8 维,每段用 256 个聚类中心编码。如果数据量大,这个索引的内存占用大概只有原始向量的 1/8 到 1/16。

4. 工程落地时绕不开的选型与调优问题

原理通了,代码跑了,下一步就是在真实项目里选型和调优。这一部分我说的都是实战经验,不一定写在官方文档里。

4.1 怎么选:HNSW 还是 IVF-PQ 还是混合

一个核心的决策依据是数据量和内存预算

如果数据量在百万级以内,内存不紧张,HNSW 是综合最优解,实现简单、召回率高、查询快。

如果数据量到了千万级以上,或者索引需要常驻内存但机器内存有限,IVF-PQ 或 HNSW-PQ 这类“有损压缩”方案就该上了。代价是精度损失和召回率波动,需要接受。

如果你的业务是“导入大量历史数据 + 增量实时写入”混合模式,需要注意:HNSW 的增量插入会导致图结构逐渐劣化,官方推荐定期重建索引。所以这种场景下,可以考虑对热数据用 HNSW,冷数据用 IVF-PQ,分层处理。

4.2 参数调到什么程度算“好”

各参数经验值:

参数名适用算法经验区间调参逻辑
efConstructionHNSW100~300建索引时用,越大图质量越高,构建越慢
MHNSW8~48空间换时间,M 翻倍,内存约增 20%
efSearchHNSW64~512查询时调,越大召回越高,延迟越高
nlistIVF数据量开根号左右比如 1000 万数据,nlist ≈ 10000
nprobeIVF-PQ8~256每增大一倍,速度约为原来的一半,但召回率提升递减
mPQ1/8 向量维度编码越小,压缩越大,召回越低

调参宗旨是:先保证召回率达标,再看性能要不要优化。很多人上来就把 efSearch 调到 16 追求极致性能,结果召回率掉到 80%,业务直接不可用。

4.3 Milvus、Redis 等产品选型参考

现在的向量数据库产品非常多,但底层索引大多基于 Faiss、HNSWLib 这类组件。区别主要在工程能力上。

产品底层索引特点
MilvusFaiss、HNSWLib、DiskANN功能全,支持混合检索和标量过滤,适合大规模生产
Qdrant自研 HNSW 实现轻量,Rust 编写,RESTful API 友好
WeaviateHNSW 为主自带模块化插件,和 GraphQL 深度整合
Redis Stack模块化实现 HNSW适合轻量级、已有 Redis 技术栈的场景
pgvectorIVFFlat、HNSW适合 PostgreSQL 用户,不需要额外引入数据库

如果你是做 AI 智能体的知识库,团队已有 PostgreSQL,数据量不大,pgvector 是最快上手的方案。如果数据量大、检索要求高、且有独立的向量检索服务需求,Milvus 是更靠谱的选择。

5. 常见问题与排查技巧实录

最后把这几年被问得最多、我自己也踩过的坑整理成一张速查表,建议收藏。

症状可能原因解决方案
召回结果明显不相关距离度量选错(如该用余弦却用了 L2)重建索引,统一 space 参数
召回率一直上不去efSearch 太小或 nprobe 太小调大 efSearch/nprobe,观察召回变化
索引构建极慢efConstruction 太大或 M 太大适当降低 efConstruction 到 100,M 到 16
内存占用爆炸HNSW 的 M 偏大 + 数据量超预期用 IVF-PQ 或者加节点扩容
删除数据后检索变慢HNSW 图结构劣化定期重建索引,或者业务层做“软删除”
为什么加了标量过滤后变慢向量索引无法直接做多条件过滤Milvus 使用标量倒排索引,或调整过滤比例后选择底层实现
更新向量后检索结果不对旧向量未被真正删除,图里存在“幽灵”节点检查产品删除语义,必要时触发索引重建

还有一个很重要的独门经验是:在生产环境上线前,一定要做一个“召回率回归测试集”。用一批已知正确答案的 query,把向量数据库的召回率作为监控指标。这个测试集可以在改参数、升级版本、换向量模型之后自动跑一遍,防止“怎么变差了都没发现”。

我从去年开始在多个项目里强制要求加这个回归测试。一个真实案例是,有次同事升级了 embedding 模型,旧向量没重新生成,结果线上检索结果质量大幅下降,但系统本身没报错,要不是回归测试根本发现不了。

写到这里,回头看这个领域,你会发现,向量数据库从来不是“新技术”,它更像是把老算法(KNN、聚类、哈希、图论)在大规模数据和新场景下重新做得更极致。HNSW、LSH、PQ 各有各的适用面,没有银弹,只有理解和权衡。

我个人在实际项目中的体感是:如果只是做个 Demo 或者数据量很小,别折腾复杂索引,FLAT 都够用。一旦数据量跨过百万级,优先上一个经过验证的索引方案(HNSW 优先),并从一开始就把召回率监控、参数调优流程搭好。这套东西,越早做,后面越省心。

最后分享一个小技巧:当你不确定某个索引的参数怎么配,先用数据集的 1/10 做一轮小规模测试,观察召回率和延迟的曲线,再决定全量参数配置。磨刀不误砍柴工,这个习惯能帮你省掉大量线上调试的时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 5:49:46

算法工程师简历重构:从调库到业务闭环的底层逻辑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 5:49:44

STM32F407驱动DHT11的微秒级时序实现方案

简介:本资源是一套基于STM32F407单片机(HAL库)驱动DHT11数字温湿度传感器的完整实验例程,面向嵌入式初学者与STM32开发入门者,解决单总线传感器在Cortex-M4平台上的协议实现、时序控制与数据解析等核心问题。压缩包共2…

作者头像 李华
网站建设 2026/9/13 5:49:37

2026西安化工产品成分分析检测排名 TOP5 CMA 资质提供含量检测、纯度检测、元素分析 联系方式推荐

西安的化工产品成分分析检测市场,机构林立、鳞次栉比,却也鱼龙混杂。化工企业、新材料厂商、日化生产工厂、橡塑制造业以及食品医药企业的研发质检部门,在筛选检测服务时,稍有不慎便容易误入无正规资质的机构。这类机构出具的成分…

作者头像 李华
网站建设 2026/9/13 5:49:21

Android代码混淆技术:R8核心机制与Gradle配置详解

1. Android混淆技术演进与R8核心机制2008年ProGuard作为首个Android官方推荐的代码混淆工具问世时,我还在用Eclipse开发Android 1.5应用。当时面对仅有的-keep选项和基础优化功能,开发者需要手动编写大量规则来保护关键代码。直到2018年Google I/O大会宣…

作者头像 李华
网站建设 2026/9/13 5:48:20

嵌入式全栈能力图谱:从STM32裸机到Linux驱动与AI部署

1. 这套“7980元嵌入式教程”到底值不值?一个干了12年嵌入式的老兵拆解真实价值 我带过37个应届生转岗嵌入式,也给6家汽车电子、工业控制、医疗设备公司做过技术顾问。看到这个标题——“(已离职)冒死上传!已经替大家…

作者头像 李华