目录
- 一、选型题问错了
- 二、向量检索到底在算什么
- 三、HNSW:用一张多层图把「逐个比对」省掉
- 四、内存里到底存了什么
- 五、三个旋钮,各自拧的是什么
- 六、瓶颈在哪:四条你一定会撞上的线
- 七、所以,你自己该怎么量
- 八、结论:在你量过之前,默认答案就是 PG
- 九、反向判据:什么情况下你确实该上专用库
- 十、自检清单
- 附:本文的核查方式
- 附:延伸阅读
一、选型题问错了
做 RAG 的人几乎都会撞上同一个问题:向量数据库用哪个?
然后你会搜到一堆对比文章,列着五六个名字加一张特性表。这类表有个共同的毛病:半年后就过时了。 谁支持了稀疏向量、谁的分布式版本开源了、谁被收购了——你照着表选完,等你上线时表已经不准了。
更麻烦的是,它没回答你真正该问的问题。
这篇文章换一个问法:
不是「我该选哪个」,而是「我到底需不需要单独上一个」。
而这个问题,只有先弄懂向量检索在算什么,才能回答。所以本文的结构是:
- 先讲清向量检索的原理(大概是全文最长的部分)
- 由此推出瓶颈在哪
- 你拿着这几条,去量自己的数据
- 量完了,答案自己就出来了
本文刻意不给一张产品对比表,也不给”多少条数据以下就够用”这类阈值——原因在文末的核查方式里说明。它给你的是让你自己能算的那套东西。
另外一个前提:本文说的”默认答案”是 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 有三个参数,名字长得像,但作用位置完全不同。这是最容易记混的地方。
| 参数 | 默认值 | 什么时候定 | 拧它在调什么 |
|---|---|---|---|
m | 16 | 建索引时 | 每个点在每层最多连几条边 |
ef_construction | 64 | 建索引时 | 建图时候选列表多大 |
hnsw.ef_search | 40 | 查询时 | 搜索时候选列表多大 |
(以上默认值来自 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 列了各类型的维度上限:
| 类型 | 维度上限 |
|---|---|
vector | 2,000 |
halfvec | 4,000 |
bit | 64,000 |
sparsevec | 1,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 性能更好”:
- 你多半已经有 PG 了。 简历、订单、知识的元数据——RAG 应用要存的东西远不止向量。多一个数据库 = 多一套备份、监控、告警、扩容、故障演练。
- 向量检索的规模瓶颈是可以提前算出来的(§6.1)。它不像”高并发”那样要靠压测才能暴露——你拿计算器就能知道自己离撞线还有多远。
- 真撞线了再换,代价是可承受的。 这是本文最重要的一句:换向量库不是”重新实现一遍检索”,是把同一批向量重新灌一遍、重建一次索引。 你的 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(因为它是本文的”默认答案”,不点名没法论述)。其余一律用”专用向量库”这个范畴指代——按特征界定:独立部署、独立扩缩容、专用的索引结构。之所以不做产品对比,是因为那类表最容易过时,而本文没有实测,写了就是二手转述。
附:延伸阅读
- pgvector:官方仓库与文档 —— 本文关于 HNSW 参数与操作的所有数字都来自这里
- HNSW 原论文:Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs(Malkov & Yashunin)
- 本号相关:
- 《RAG 实战(一):RAG 是什么,以及环境怎么搭》 —— 那篇把你带上了 pgvector,这篇回答为什么是它
- 《RAG 实战(二):跑通最简 RAG 链路》
- 《RAG 实战(三):混合检索与 RRF 融合》 —— 向量检索不够用时,该补什么
- 《AI 应用开发面试笔记》 —— Q3.3 是同一道题的面试速查版,结论走向不同(那篇默认你要选一个)