向量数据库选型:先弄懂 HNSW 在算什么,再决定要不要上专用库

目录


一、选型题问错了

做 RAG 的人几乎都会撞上同一个问题:向量数据库用哪个?

然后你会搜到一堆对比文章,列着五六个名字加一张特性表。这类表有个共同的毛病:半年后就过时了。 谁支持了稀疏向量、谁的分布式版本开源了、谁被收购了——你照着表选完,等你上线时表已经不准了。

更麻烦的是,它没回答你真正该问的问题。

这篇文章换一个问法:

不是「我该选哪个」,而是「我到底需不需要单独上一个」。

而这个问题,只有先弄懂向量检索在算什么,才能回答。所以本文的结构是:

  1. 先讲清向量检索的原理(大概是全文最长的部分)
  2. 由此推出瓶颈在哪
  3. 你拿着这几条,去量自己的数据
  4. 量完了,答案自己就出来了

本文刻意不给一张产品对比表,也不给”多少条数据以下就够用”这类阈值——原因在文末的核查方式里说明。它给你的是让你自己能算的那套东西。

另外一个前提:本文说的”默认答案”是 PostgreSQL + pgvector。不是因为它最好,而是因为大多数做 RAG 的项目本来就有 PG——简历、订单、知识库的元数据都在里面。复用已有的运维,不引入新组件,这件事本身就是巨大的优势。


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

2.1 先把「向量」这个词落地

向量就是”一串数字”。 不是数学课本里那个带箭头的几何对象,就是一串数,比如 [0.12, -0.83, 0.44, ...]。

Embedding 模型干的事,是把一段文字变成这样一串数字——而它的关键性质是:意思相近的文字,变成的数字串也相近。

打个比方:给每个词句在一张”意义地图”上定一个坐标。意思是”猫”的那些句子,坐标都聚在一片区域;意思是”财务报表”的,聚在另一片。“检索”就变成了”在地图上找离我最近的几个点”。

至于”两串数字怎么算相近”——最常见的是余弦相似度,你可以先理解成”看这两串数字的方向像不像”。具体公式本文不展开,因为它不影响任何选型决策。

2.2 最笨也最准的办法:全比一遍

如果只要找”最像的几条”,一个不需要任何索引的办法是:

把你库里的每一段话都算一遍相似度,然后排序取前 K 条。

这个办法有个好听的正式名字叫暴力检索(brute force / exact search)。它永远不会漏——因为每一条都真算过了。这个性质叫精确召回。

小规模时它完全够用。 几千条数据、偶尔查一次,暴力检索的延迟是毫秒级的——你甚至感觉不到它”笨”。

2.3 那为什么还需要索引

因为它是一条直线:数据量翻十倍,查询耗时也翻十倍。

这时候近似检索(ANN,Approximate Nearest Neighbor)登场了。它的思路是:

放弃”每次都保证找得最准”,换取”快得多”。

代价是召回率(recall)——本该找出来的那几条,可能漏掉一两条。

这是整个向量检索领域唯一真正重要的取舍:用”准”换”快”。 后面所有的索引结构、所有的参数,拧的都是这个旋钮。

(顺带说一句:很多产品页会强调”召回率 99%“。99% 听起来很高,但要看它是在什么数据、什么参数下测的。 这个数字离开上下文没有意义——这也是为什么本文不给你任何阈值。)


三、HNSW:用一张多层图把「逐个比对」省掉

HNSW 全称是 Hierarchical Navigable Small World(分层可导航小世界)。名字很绕,结构其实很好懂。

3.1 结构:一张分了层的图

先说”图”:把每个向量当成一个点,在相近的点之间连上边。 于是”找最近的邻居”就变成了”在这个图里走”。

HNSW 在这基础上加了一层设计——它把图分成好几层。按论文的原话,是

“multi-layer structure consisting from hierarchical set of proximity graphs (layers) for nested subsets”

翻译过来:分层,且上层是下层的子集。最下层有全部的点,越往上点越少。

打个比方:像一张交通图。

  • 最上层是航线图——只有几个大城市,点少但跨度大,可以一步跨很远
  • 中间层是国道网——城市多一些
  • 最下层是城市里的街道——每个点都有,但每次只能挪一小步

要走远路,先用航线一步跨到目标附近的大城市,再换国道,最后用街道走过去。这就是”分层”的全部意义。

3.2 搜索:从上往下走

搜索就是这样进行的——从最上层开始找,一路往下:

  • 在最上层里找离目标最近的点
  • 拿这个点当入口,进下一层继续找
  • 一层层下去,到最下层时,已经很接近真正的答案了

论文对这一步的说法是:

“Starting search from the upper layer together with utilizing the scale separation boosts the performance”

“scale separation”(尺度分离)是这里的关键:每一层的边连接的是不同距离尺度上的点。上层负责大跨度,下层负责精调。

3.3 哪些点出现在哪些层?随机

这是 HNSW 最有意思的一个设计——不是人为挑选哪个点该在第几层。论文原话:

“The maximum layer in which an element is present is selected randomly”

服从一个指数衰减的概率分布。也就是说:

  • 所有点都在最下层
  • 有一部分点在倒数第二层
  • 更少的点在更高层
  • 越往上越少,是指数级的少

为什么随机反而好? 因为这样得到的图,边天然是”按距离尺度分开的”。而且——论文自己也点出了这个相似性——这套结构非常像跳表(skip list):

“Similarity of the algorithm to the skip list structure”

如果你写过跳表,HNSW 就是跳表的图版本:跳表用随机层高来省掉线性扫描,HNSW 用随机分层来省掉全图遍历。

好处就是论文说的那句:

“allows a logarithmic complexity scaling”

对数级。这是 HNSW 存在的全部理由。

⚠️ 一个容易踩的坑:论文只声称对数级,并没有给出”要跳几次”这种具体数字。你在别处看到”平均 3~5 跳”之类的话,多半是别人的实测——而实测的结论依赖数据集和参数,不能当通例。本文不给这个数。


四、内存里到底存了什么

到这一步,你已经有能力回答最实际的问题了:我的数据要占多少内存?

关键在于:HNSW 索引占的内存是两部分,不是一部分。

占内存的东西怎么算
① 向量本体维度 × 每维字节数 × 条数
② 图连接与 m(每层最大连接数)和条数成正比

① 向量本体可以直接算。以 fp32(每维 4 字节)为例:

100 万条 × 1536 维 × 4 字节 ≈ 6.1 GB

注意这个数字已经不小了。 而且这还只是”存下来”,没算索引、没算 PG 自身、没算查询时的临时空间。

② 图连接是很多人会漏掉的一项。pgvector 官方文档在对比 IVFFlat 时说得很直白——HNSW 是

“better query performance … (in terms of speed-recall tradeoff), but has slower build times and uses more memory”

多出来的那份内存就是图。 每个点要存 m 条边(默认 16),每条边是一个指向别的点的引用。层数越多、m 越大,这部分越大。

算总账的时候记得:

内存 ≈ 向量本体 + 图连接。 前者跟维度走,后者跟 m 走。

想让索引变小,两条路:① 降低存储精度(fp32 → fp16 直接砍半);② 减小 m(代价是召回率)。

pgvector 官方文档还给了一个省事的办法:如果你的维度很高或者想压体积,可以用半精度(halfvec)或者二进制量化。文档明确建议用它们来缩小索引。


五、三个旋钮,各自拧的是什么

HNSW 有三个参数,名字长得像,但作用位置完全不同。这是最容易记混的地方。

参数默认值什么时候定拧它在调什么
m16建索引时每个点在每层最多连几条边
ef_construction64建索引时建图时候选列表多大
hnsw.ef_search40查询时搜索时候选列表多大

(以上默认值来自 pgvector 官方 README。)

5.1 分清前两个和第三个

m 和 ef_construction 是”建房子”的参数——索引一旦建好,改它们就得重建。官方文档对 m 的定义是:

“the max number of connections per layer (16 by default)”

对 ef_construction 是:

“the size of the dynamic candidate list for constructing the graph (64 by default)”

ef_search 是”住进去以后”的参数——它可以在每次查询时改。官方定义:

“the size of the dynamic candidate list for search (40 by default)”

5.2 三个都指向同一个取舍

看官方对后两个代价的描述,会发现句式一模一样:

参数官方原话
ef_construction“A higher value of ef_construction provides better recall at the cost of index build time / insert speed.”
ef_search“A higher value provides better recall at the cost of speed.”

“better recall at the cost of …” —— 又是那个取舍,一分钱一分货。

  • 调高 ef_search:查得更准,但更慢。这是唯一能在上线后动态调的那个。
  • 调高 ef_construction:索引质量更好(同样参数下召回更高),但建索引更慢。
  • 调高 m:图更密,召回更好,但内存和建索引时间都涨。

5.3 一条省心的建议

官方文档对这三个参数给了一句很实在的话:

“The index options also have a significant impact on build time (use the defaults unless seeing low recall).”

先用默认值。 除非你真的测出来召回不够,否则别调。这不是敷衍——是因为调参的收益要在你自己的数据上测才作数,而多数人调的其实是心理安慰。


六、瓶颈在哪:四条你一定会撞上的线

到这里,“该不该上专用库”这个问题已经可以拆成四条具体的、可验证的线了。

6.1 内存

第一节算过了:向量本体 + 图连接。

这条线的特点是——它是可以提前算准的。维度、条数、精度、m,四个数一乘就知道了。你在自己笔记本上拿计算器就能估出来,不需要跑任何压测。

6.2 建索引的耗时

这条线的表现形式很有意思。官方文档说:

“Indexes build significantly faster when the graph fits into maintenance_work_mem”

而一旦装不下,PostgreSQL 会在日志里主动打一条提示:

hnsw graph no longer fits into maintenance_work_mem after 100000 tuples

后面还跟着一句:构建 “will take significantly more time”。

注意那个 100000 tuples——它不是建议值,是”这次构建已经装不下了”的实测汇报。 你在自己的日志里看到这句,就是撞线的信号。

配套的还有两条:

  • 官方建议 把数据灌完再建索引:“it’s faster to create an index after loading your initial data.”
  • 建索引可以并行:max_parallel_maintenance_workers 默认 2,max_parallel_workers 默认 8

而构建进度是可以实时看的——pg_stat_progress_create_index,HNSW 的两个阶段分别叫 initializing 和 loading tuples。卡在 loading tuples 很久,说明你正在撞这条线。

6.3 建索引会阻塞写入

这条是运维层面的,但很致命。官方文档:

“In production environments, create indexes concurrently to avoid blocking writes.”

所以:

CREATE INDEX CONCURRENTLY idx_embedding ON documents
USING hnsw (embedding vector_cosine_ops);

CONCURRENTLY 的代价是更慢、且失败会留下无效索引,但它不锁写。这条不是”要不要上专用库”的问题,是”用 pgvector 也要知道”的问题。

另外文档还提了一句:HNSW 索引的 VACUUM 可能很慢,建议先 REINDEX INDEX CONCURRENTLY 再清。

6.4 维度上限(这条最容易被忽略)

pgvector 官方 README 列了各类型的维度上限:

类型维度上限
vector2,000
halfvec4,000
bit64,000
sparsevec1,000 个非零元素

注意 2,000 这个数。 现在不少 embedding 模型的输出维度是 3072——直接超过 vector 的上限,必须改用 halfvec。

这不是选型问题,是建表时就会撞上的硬约束。而且它是好事:在你写 CREATE TABLE 的那一刻就能发现,不用等到上线。


七、所以,你自己该怎么量

前面六节都在讲原理。现在把它变成你可以动手做的事。

先问三个问题,答案都在你手里:

问题怎么得到答案
① 我会有多少条向量?不是”以后可能有几百万”,是上线的第一个月会有多少条。文档、工单、商品——你有确切数字
② 每条多少维?什么精度?你选的 embedding 模型决定维度(查模型卡)。精度是你自己定的
③ 查询量多大?能不能忍延迟?是内部工具(一天查几百次),还是对外的核心接口(每秒几百次)

然后拿第 ① 和第 ② 直接乘出内存,跟你的机器比一比:

内存 ≈ 条数 × 维度 × 每维字节数  +  图连接开销

这一步用计算器就能完成。 如果你算出来离机器上限还有很大余量,那 6.1 这条线你没撞上。

剩下三条线,各有各的查法:

  • 建索引耗时 → 看日志里有没有 hnsw graph no longer fits into maintenance_work_mem 那句话
  • 阻塞写入 → 不用量,直接用 CONCURRENTLY(这条是常识,不是取舍)
  • 维度上限 → 建表时就会报错,撞上就换 halfvec

做完这三步,你手上有了一份针对自己的清单。 这时候再回头看”用哪个向量数据库”,你会发现问题已经变了——它不再是个悬空的技术偏好,而是变成了”我撞上了哪几条线,要解决哪一条”。


八、结论:在你量过之前,默认答案就是 PG

本文不给”多少条以下够用”这种阈值。 原因很简单:那个阈值取决于你的维度、精度、m、机器内存、以及你能忍的延迟——它是我给不了的数,但你自己算得出来。

而这正是本文想说的:

在你量过之前,默认答案就是 PG。

理由有三条,没有一条是”PG 性能更好”:

  1. 你多半已经有 PG 了。 简历、订单、知识的元数据——RAG 应用要存的东西远不止向量。多一个数据库 = 多一套备份、监控、告警、扩容、故障演练。
  2. 向量检索的规模瓶颈是可以提前算出来的(§6.1)。它不像”高并发”那样要靠压测才能暴露——你拿计算器就能知道自己离撞线还有多远。
  3. 真撞线了再换,代价是可承受的。 这是本文最重要的一句:换向量库不是”重新实现一遍检索”,是把同一批向量重新灌一遍、重建一次索引。 你的 embedding 模型没变、维度没变、查询逻辑没变。

“以后可能要换”不该成为”现在就上”的理由。 你付出的是一整套新的运维负担,换来的是一个你可能几个月都用不到的余量。


九、反向判据:什么情况下你确实该上专用库

只讲”别上”是不负责任的。 下面这几条,撞上任何一条,专用库就不是过度设计,而是必需品——注意每一条都是已经撞上的线,不是”可能会撞”:

你已经撞上的线该上专用库的理由
内存:向量+图算下来超过单机内存,需要分片专用库的水平扩展是内建的,PG 的分片要自己做
延迟:单机查不动,需要独立扩缩容查询节点专用库的读和写是可以分开扩的
隔离:向量查询把业务库的 IO 打满了这时候”复用已有运维”反而成了缺点——你正在拖垮业务库
多租户:要给不同客户做逻辑/物理隔离专用库有现成的租户模型
混合检索:要向量 + 全文 + 结构化条件一起排需要看具体实现,某些方案在这块做得更顺
维度:超过 2,000 且你不想用 halfvec有些专用库对高维的支持更宽松

这张表的读法:左边那一列是判据,不是猜想的场景。

“我们业务会增长”不是判据,“我们的向量查询已经在拖垮业务库了”才是。


十、自检清单

读完这篇,如果下面每一条你都能答上来,你就有能力自己做这个决定了:

  • 暴力检索和近似检索的区别是什么?各自”放弃”了什么?
  • HNSW 为什么要分层?上层和下层的点、边有什么不同?
  • “哪些点出现在哪些层”是随机决定的——为什么随机反而好?它像哪种你熟悉的数据结构?
  • 论文只声称了什么级别的复杂度?它有没有给出具体的跳数?
  • HNSW 索引的内存由哪两部分构成?各自跟什么成正比?
  • m、ef_construction、ef_search 三个参数,哪个是查询时能改的?各自调高的代价是什么?
  • 日志里出现 hnsw graph no longer fits into maintenance_work_mem 说明什么?
  • 为什么生产环境建索引要用 CONCURRENTLY?
  • vector 类型的维度上限是多少?为什么这个数字对选 embedding 模型有影响?
  • 你自己那三个数(条数、维度、查询量)分别是多少?算出来的内存是多少?
  • 如果明天要换向量库,你需要重做哪些事?哪些事不用重做?

附:本文的核查方式

数字与结论的来源:

内容来源
HNSW 参数默认值(m=16、ef_construction=64、ef_search=40)pgvector 官方 README(2026-10-16 拉的 master,文档自述版本 v0.8.7)
“better recall at the cost of …” 两处代价描述同上,逐字引用
“use the defaults unless seeing low recall”同上
hnsw graph no longer fits into maintenance_work_mem after 100000 tuples同上(文档给出的日志提示原文)
维度上限 vector 2000 / halfvec 4000同上
CREATE INDEX CONCURRENTLY 的建议同上
HNSW 分层结构、随机分层、指数衰减论文 arXiv:1603.09320 摘要,逐字引用
“allows a logarithmic complexity scaling”、“Similarity of the algorithm to the skip list structure”同上
内存估算公式按上述事实自行推导,非引述

关于本文为什么不给阈值:

这是有意的。本文没有做任何实测——没有装 PG、没有灌数据、没有压测。所以:

  • 凡是”文档说”的,都标了出处;
  • 凡是需要实测才能得出的结论(比如”多少条以下够用""延迟大概多少毫秒”),本文一律不给。

网上那些阈值,大多来自别人的实测环境。换一套数据、换一个维度、换一台机器,结论就不一样了——照搬反而危险。本文选择给机制,让你在自己的环境里算。

本文不涉及(有意略过):余弦相似度等距离度量的具体公式、IVFFlat 与 PQ/SQ 等索引类型的对比、Milvus / Qdrant / Weaviate 等具体产品的特性、分片与复制方案。

关于”不列产品”:本文只点名了 pgvector(因为它是本文的”默认答案”,不点名没法论述)。其余一律用”专用向量库”这个范畴指代——按特征界定:独立部署、独立扩缩容、专用的索引结构。之所以不做产品对比,是因为那类表最容易过时,而本文没有实测,写了就是二手转述。


附:延伸阅读