前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
返回 AI 情报前线
All News · 全部资讯9275
  • Self-Consistency:采样多次取多数票的推理术
  • 缓存贵的、重算便宜的:LLM输出缓存策略
  • Agent工具调用的安全防护要点
  • 搜索延迟拆解:毫秒级预算分配实战
  • 推理模型延迟预算分析
  • 推理 Effort 档位实际控制什么
  • 推理模型性价比曲线:如何算清每分正确率成本
  • 为什么推理能力在数学好过代码好过文本
  • 通过所有测试的代码仍可能让AI代理崩溃
  • Cursor 修 IDOR 易漏同文件其他路由
  • 可信数据是AI Agent规模化的关键瓶颈
  • Anthropic为Agent引入梦境推理能力引热议
  • Python 中缓存 LLM 响应:细节决定成败
  • Harness Engineering:打造 AI 编程助手的工作环境
  • 加向量数据库前,先用 NumPy 搞定 Embedding
  • 自实现令牌桶限流:比等 429 更优
  • 长时 AI 任务用后台任务队列:状态机比框架重要
  • asyncio 并发调用 40 个 LLM:代码少但坑多
  • AI 项目中 API 密钥的泄密风险与防护层级
  • 电话Agent工程实践:SIP/WebRTC音频路径全解析
  • pgvector索引调优:HNSW与IVFFlat成本精确分析
  • 多租户应用按客户计费:成本与可计费用量必须分开
  • PDF 解析难点深度剖析:字符间距阈值是万恶之源
  • Hugging Face开源OlmoEarth文本嵌入
  • 阿里开源 Qwen3.8:2.4T MoE、激活 95B、256K 上下文
  • MiniMax H3:单个 Transformer 替代视频生成完整流水线
  • DeepSeek V4 Pro 正式版发布:多项测试接近 Fable 5 水平
  • AI编程助手Lovable完成新一轮4亿美元融资,估值达133亿美元
  • MiniMax Music 3.0:开放权重生产级音乐生成模型
  • Microsoft 发布 MindTopo:VLMs 空间推理能力新基准
  • Grok 4.6 发布:剑指 GPT-5.6 Sol,主打长时间 Agent 任务
  • Grok 4.6 中文详解:训练数据、Agent 能力边界与定价
  • 市场份额报告:Google Gemini 份额从 12% 跌至 1.9%
  • 用Embeddings+Reranking+LLM构建可靠的内容分类流水线
  • 推理冷启动从10分钟降至秒级:容器镜像瘦身实战
  • Anthropic研究揭示:Claude Code旧版权限提示97%被机械通过
  • DeepSeek-V4-Pro-0813 悄然上线,支持思考与非思考模式
  • AI 生图 Prompt 审核实战:Node.js 调用 Chat JSON Schema 方案
  • 企业AI分析的隐藏陷阱:语义漂移问题深度剖析
  • AI 正在消除软件工程中层:代码看不懂、没人负责的团队困境
  • Qwen3.8-2.4T-A95B 模型发布
  • TraceMotive:本地优先的AI代理执行追踪调试工具
  • AI编程工具正在离开IDE:终端原生Agent工作流崛起
  • 个人开发者用AI编程的项目架构经验
  • FastAPI五个安全漏洞发现与修复全过程
  • 长文档AI审核的审计设计:Map-Reduce优于检索增强
  • AI Agent辅助发现SharePoint RCE漏洞链(CVSS 9.1)
  • 我用Claude Code将API的P99延迟降低一半
  • AI Agent读了你的secrets并删了生产数据库——PocketOS事故详解
  • 用聊天模型做金融内容审核:结构化输出设计实践
  • Prompt注入攻击原理与防御实践指南
  • 已加载 51 / 9275
8.0
热点
AI SCORE
技术实践2026-08-13 00:32

pgvector索引调优:HNSW与IVFFlat成本精确分析

dev.to · AI#pgvector#向量数据库#PostgreSQL
Editor brief · 编辑速览

从内存占用、构建时间、召回率角度量化对比pgvector两种索引类型,给出实际查询计划分析方法,避免选型误区。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

两种索引类型,以及各自的适用场景

pgvector 有两种索引类型,需要调节的参数一共四个。本文将推导每个参数的代价——内存占用可以精确计算,构建时间则遵循一种标度律(从一个小样本外推即可)——并提供用于测量召回率的 SQL,这个指标没有任何公式能给你,只能实测。

HNSW

HNSW(pgvector 0.5.0 及以上)构建的是一种分层邻近图。查询时从稀疏的顶层向下走到稠密的底层。它在几乎所有运行点上都能以更少的查询时间达到比 IVFFlat 更高的召回率,而且能优雅地处理插入操作。它的代价是构建缓慢,且索引是向量数据的完整副本。

IVFFlat

IVFFlat 通过 k-means 将向量划分为若干列表聚类,查询时只搜索最近的若干个探针(probe)。它的构建速度快得多,内存占用也低得多。但它有两个实际问题:必须在有数据的基础上构建,因为空表没有东西可供聚类;随着数据逐渐偏离它学到的聚类中心,召回率会持续下降,因此需要定期重建索引。参见 embedding drift 了解导致这种漂移的原因。

如何选择

诚实的默认选择是 HNSW。在以下情况下选择 IVFFlat:索引必须频繁且快速地重建、内存是刚性约束、或者在数千万行数据上创建索引而机器无法容纳 HNSW 图。除此之外,多花的构建时间都是值得的。

索引需要多少内存

这是可以推导的,而且推导结果比人人都在传的那条经验规则更有用。pgvector 中的 HNSW 索引,每行存储:一份向量副本、一组邻居指针,以及每条元组的页面开销。

向量本身

vector(d) 是 4d + 8 字节——每个 float32 分量 4 字节,加一个 8 字节的头部。

邻居指针

在标准 HNSW 中,一个元素在第 0 层最多有 2m 个连接,上层每层有 m 个。层级是按几何分布抽取的,抵达第 l 层的概率是 m^(-l),因此除零层以上的期望层级数为该级数的和:1/(m-1)。于是每个元素的期望连接槽位数为:

slots(m) = 2m + m/(m−1)

  m = 16   ->  32 + 1.07  =  33.1 slots
  m = 32   ->  64 + 1.03  =  65.0 slots
  m = 64   -> 128 + 1.02  = 129.0 slots

每个槽位存一个元组指针,6 字节。加上每个元素约 32 字节的索引元组和行指针开销。整体为:

bytes_per_row(d, m) ≈ (4d + 8)          向量有效载荷
                    + 6 × slots(m)     邻居指针
                    + 32               元组开销

假设全部可验证:float32 向量、6 字节元组指针、
标准 HNSW 层级分布、未计入页面填充余量。

来算一下

以 d = 1536 为例——这是若干主流 embedding 模型的维度——取 m = 16:

(4 × 1536 + 8) + 6 × 33.1 + 32
= 6152 + 199 + 32
= 6383 bytes per row

× 1,000,000 rows = 6.38 GB of index

而有趣的部分来了——如果把 m 增加到 64(调优指南建议这样做以提高召回率),会发生什么:

d = 1536, m = 64:  6152 + 774 + 32 = 6958 B  ->  6.96 GB   (+9%)
d =  384, m = 16:  1544 + 199 + 32 = 1775 B  ->  1.78 GB
d =  384, m = 64:  1544 + 774 + 32 = 2350 B  ->  2.35 GB   (+32%)

所以那个常见的警告——增大 m 内存代价很高——在 384 维时成立,而在 1536 维时基本不成立,因为此时向量副本占据主导,整个图结构只占索引的百分之三。如果你在高维模型上且召回率不够,m 是一个比指南暗示的更廉价的调节杠杆。如果你用的是低维模型,则不是。

用你自己的表验证推导

只需要一条查询,如果结果与公式相差超过约百分之十五,请相信查询结果:

SELECT pg_size_pretty(pg_relation_size('chunks_embedding_hnsw')) AS index_size,
       pg_size_pretty(pg_relation_size('chunks'))               AS heap_size,
       (SELECT count(*) FROM chunks)                            AS rows;

真正重要的结论

索引要快,就必须驻留在内存中。合理设置 shared_buffers,让机器的内存足以容纳上面的数字加上你堆的 working set。HNSW 遍历是一连串随机访问,而随机访问磁盘正是 RAM 与 NVMe 差异在每一条查询上都可见的场景。

构建时间作为标度律

没有人能告诉你构建需要多久,因为这取决于你的 CPU、内存带宽和并行 worker 数量。但可以陈述的是它如何随规模伸缩,这就足够规划一次迁移了。

构建图的过程是插入 N 个元素。每次插入做一次贪婪下降穿过上层,然后是第 0 层搜索,期间维持 ef_construction 个候选元组存活,并从每个候选最多扩展 m 个邻居,每次扩展都要计算一个 d 维距离。因此每次插入的工作量正比于 ef_construction × m × d,而跳数随 log N 增长:

build_time ∝ N × log N × ef_construction × m × d

由此得到四条实用规则。将 ef_construction 加倍大约使构建时间翻倍,但完全不改变索引大小。将 m 加倍大约使构建时间翻倍,索引大小变化如前文推导。将维度加倍大约使构建时间翻倍。将行数加倍则略多于翻倍。

规划一次真实构建:先做样本再外推

在一个 10 万行的副本上构建索引,然后按比例外推:

T(N) ≈ T(n) × (N/n) × (ln N / ln n)

样本:100,000 行构建耗时 95 秒。
目标:10,000,000 行。

T = 95 × (10,000,000 / 100,000) × (ln 10^7 / ln 10^5)
  = 95 × 100 × (16.12 / 11.51)
  = 95 × 100 × 1.40
  = 13,300 s ≈ 3 h 42 min

此估算仅在目标构建也能容纳在 maintenance_work_mem 中时有效。
如果发生溢出,此估算毫无意义,实际时间是它的若干倍。

这个警告才是关键。在开始大规模构建之前,把 maintenance_work_mem 设置为前一小节约来的内存值之上——要留足余量,因为构建过程持有的内存比最终索引更多——并将 max_parallel_maintenance_workers 设置为你能腾出的核心数。用以下方式监控构建过程:

SELECT phase,
       round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS pct
FROM pg_stat_progress_create_index;

在你自己的表上测量召回率

召回率无法推导。它取决于你 embedding 的内在维度和聚类特性,这是你的语料和模型的属性,别人数据上的召回率数字跟你毫无关系。所以,去测量它。以下在一次 psql 会话中对你的实际表运行:

BEGIN;

-- 200 个探针向量。用留出的真实查询更好;
-- 如果你有查询日志,嵌入其中 200 条来使用,而不要用表行。
CREATE TEMP TABLE probes AS
  SELECT id, embedding FROM chunks ORDER BY random() LIMIT 200;

-- ground truth:精确 top-10,强制禁用索引。
SET LOCAL enable_indexscan = off;
CREATE TEMP TABLE truth AS
  SELECT p.id AS probe, t.id AS hit
  FROM probes p
  CROSS JOIN LATERAL (
    SELECT c.id FROM chunks c
    WHERE c.id <> p.id
    ORDER BY c.embedding <=> p.embedding
    LIMIT 10
  ) t;

-- 近似:用索引,在生产中计划的 ef_search 下。
SET LOCAL enable_indexscan = on;
SET LOCAL enable_seqscan   = off;
SET LOCAL hnsw.ef_search   = 40;
CREATE TEMP TABLE approx AS
  SELECT p.id AS probe, t.id AS hit
  FROM probes p
  CROSS JOIN LATERAL (
    SELECT c.id FROM chunks c
    WHERE c.id <> p.id
    ORDER BY c.embedding <=> p.embedding
    LIMIT 10
  ) t;

SELECT round(100.0 * count(a.hit) / count(*), 2) AS recall_at_10_pct
FROM truth t
LEFT JOIN approx a ON a.probe = t.probe AND a.hit = t.hit;

ROLLBACK;

在 hnsw.ef_search 为 20、40、100、200 和 400 时各跑一遍,你就有了在真实数据上、约十分钟内得到的自己的召回率-延迟曲线。这条曲线是选择这些参数的唯一依据。

关于探针选择的一个警告

从索引表中抽取的探针是一种乐观测试:每个探针本身就是图中一个连接度很高的节点,它的邻域很容易到达。真实查询落在文档之间的缝隙上,得分更低。如果这些数字对你的决策重要,用你日志中的真实查询来嵌入。

ef_search:上线后才调的旋钮

m 和 ef_construction 在构建时固定,改任何一个都得重建索引。hnsw.ef_search 是一个会话级 GUC,每条查询都可以改,而这才是你几乎所有调优工作应该发生的地方。

SET LOCAL hnsw.ef_search = 100;   -- 默认 40,最大 1000

SELECT id, content
FROM chunks
ORDER BY embedding <=> $1
LIMIT 10;

它控制第 0 层搜索维持多少个候选。查询代价大致与它线性相关,召回率则呈边际递减上升。它至少要等于你的 LIMIT;如果查 top-50 却把它设为 10,是一个静默的召回率灾难。请用 SET LOCAL 而不是 SET,否则这个值会在连接池中保留并作用于下一条无关的请求——在 AI 工作负载的连接池一文中有详细讨论这个陷阱。

IVFFlat 的选型(如果你用它)

IVFFlat 有一个构建参数和一个查询参数,官方自己的指导是一个不错的起点:百万行以下按 rows/1000 作为 lists 数,之后按 sqrt(rows);probes 从 sqrt(lists) 起步。

-- 5,000,000 行:lists = sqrt(5e6) ≈ 2236
CREATE INDEX chunks_embedding_ivf
  ON chunks USING ivfflat (embedding vector_cosine_ops)
  WITH (lists = 2236);

SET LOCAL ivfflat.probes = 47;   -- sqrt(2236)

背后的算术需要理解:N 行数据划分到 lists 个分区,每个分区约含 N/lists 个向量,搜索 probes 个分区时精确扫描约 probes × N / lists 个向量。按上面的数字,约为 47 × 5,000,000 / 2236 ≈ 105,000 个向量每次查询,即表的百分之二。把 probes 加到等于 lists,你就用更多的步骤重新发明了顺序扫描。

IVFFlat 召回率失败的 dominant 原因

真实最近邻落在了一个你没有探到的聚类中,再跑多少遍也找不到它。在任何大批量加载后重建索引,用上面的召回率测试套件(它不关心你用哪种索引类型),如果在你的延迟预算内无法达到可接受的召回率,这就是切换到 HNSW 的信号。

调节哪个参数,按什么顺序

四个参数对你注意力的价值并不均等,上面的推导解释了原因。按以下顺序逐个尝试,在召回率测试报告出一个你能接受的数字时停下。

hnsw.ef_search,第一位,通常也是唯一一位

改它没有代价,按查询生效,不需要重建。查询代价大致与它线性相关,所以从 40 加到 100 花的是大约 2.5 倍的遍历工作量——而对于一个驻留在内存中的索引,是几毫秒变成了稍微多几毫秒。大多数以为自己在做索引调优的团队,ef_search 是 40,而他们的召回率需求需要的是 200。

ef_construction,在提高 ef_search 出现平台期时用

它改变的是图的质量而非搜索的努力程度,所以它提升的是 ef_search 所要冲击的天花板。它使构建时间线性增长,但不改变索引大小,这是两个需要重建的参数中更便宜的一个。从 64 加到 200 是常见且站得住脚的做法。

m,在图本身太稀疏时用

提高它会增加连接数,在 embedding 高维且聚类不佳时帮助最大。用前文的内存推导来决定是否负担得起:在 1536 维时,把 m 翻两番(16→64)花费索引大小的百分之九;在 384 维时花费三分之一。这种不对称性正是做算术而不是follow经验规则的的全部理由。

维度,它根本不是 pgvector 的参数

维度减半,索引内存减半,构建时间减半,查询代价大致也减半,同时发生。如果你的模型支持截断,这是本文所有参数中最大的杠杆,而且应该在任何上面三个参数之前就被纳入讨论。

一件事要抵制:一次改两个参数然后重建一次

重建足够昂贵,把它们打包的冲动很强烈,但这会让你无法归因结果。改一个,用测试套件测量,把数字记下来。四个测量点比一个靠直觉得出且现在无法辩护的配置有价值得多。

延伸阅读

pgvector From Install to First Query

Storing Embeddings: Types, Precision and Row Size

Migrations on a Table With 50 Million Vectors

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
电话Agent工程实践:SIP/WebRTC音频路径全解析
下一篇
多租户应用按客户计费:成本与可计费用量必须分开