pgvector新版本向量检索评测:HNSW索引性能与调优实战

发布时间:2026/10/17 8:39:16
pgvector新版本向量检索评测:HNSW索引性能与调优实战
pgvector新版本的向量检索评测是我最近两周一直压在手里的一件事。业务里有一个用户向量召回场景数据量从百万级涨到接近千万级旧的索引方案撑不住查询耗时我不得不把眼光放到扩展的新版本上。这篇文章不是什么官方评测也不是翻译文档就是我基于一个模拟生产的测试环境从0到1把pgvector新版本在向量检索上的表现完整跑了一遍的记录。这篇东西适合三类人看正在纠结向量数据库选型的技术负责人被pgvector召回率和查询性能折磨过的后端开发以及想搞清楚新版到底值不值得升级的DBA。如果你只是想来背个参数结论可以直接跳到第4和第5节如果你想弄清楚为什么是这个参数为什么这个场景要这么配建议从头到尾读一遍整个过程我都踩了些坑也把排查思路写进去了。1. 评测思路为什么要盯上新版本1.1 这次评测的原始诉求我手里的场景并不复杂一张内容表每一条记录对应一个由某个文本模型生成的128维浮点向量用户提交一条查询请求拿到查询向量后在这张表里做近似最近邻检索取top-100结果再做过滤、排序和重排。数据量刚过百万的时候业务还算轻量老版本配合IVFFlat索引勉强能跑。但数据量冲到几百万之后几个核心指标同时出现问题查询P99延迟从几十毫秒抬升到几百毫秒召回率波动明显索引重建时间也越来越不可接受。在这种背景下我关注pgvector新版本的核心目的有三点第一索引构建效率能否支撑频繁的批量数据更新第二查询吞吐和召回率是否能回到一个稳定区间第三新版本有没有带来更省内存、更快扫描的新特性能让我在不换数据库的前提下延长这套架构的使用寿命。评测不是单纯为了追新而是业务压力倒逼出来的。1.2 新版本相比旧版的关键变化从版本演进看pgvector的变化主要体现在索引类型和查询执行路径上。旧版本里大家用得最多的是IVFFlat它把向量空间分成若干列表查询时先定位候选列表再精确计算但列表数量、探针数量的参数选择直接影响召回率调参空间很大而且构建索引前需要训练聚类数据分布变化后召回质量会明显下降。新版本里HNSW索引已经成为更主流的选择。HNSW的直观理解是“多层跳跃链表图索引”上层图连接稀疏用来快速定位接近目标向量的区域底层图连接密集用来做精细的近邻搜索查一层不满足就往下层走走的是一条多级路由路径。这套设计让HNSW在构建时不需要单独训练阶段插入过程直接建图对数据分布变化的容忍度更高。当然HNSW也不是没有代价图结构带来的内存开销比IVFFlat大构建期间对资源的占用也更明显。所以评测的第一步就是把这两种索引放在同一份数据上用同样的查询压力去对比而不是依赖文档里的理论描述。1.3 评测范围和量化指标我给自己划定了一个相对完整的评测范围包括环境部署、数据装载、索引构建、查询性能、召回率、参数敏感性、常见故障排查七个方面。指标上重点看四个数索引构建耗时、单查询平均延迟、P99延迟以及top-10召回率。召回率的计算方式需要说明一下这里我用的是“暴力精确扫描结果”作为基准答案。也就是同一个查询向量先不带索引强制全表扫描找到真正的最近邻集合再用HNSW索引去查比较两个结果集合的重叠度。这个做法在千万级数据上有点耗时但为了拿到可信的召回率数据这步不能省。2. 环境准备与数据装载2.1 测试环境的构建方式我没有去搭一套很复杂的集群就用一台普通的测试服务器8核处理器、16GB内存、SSD存储操作系统是常见的Linux发行版。数据库方面我选了一个比较稳定的PostgreSQL版本然后在这个版本上编译安装pgvector源码包避免包管理器自带的旧版本干扰评测结果。如果你也想复现我的过程安装这一步可以直接用包管理器# 以Debian/Ubuntu系为例 sudo apt-get update sudo apt-get install postgresql-16 postgresql-server-dev-16 # 再从源码编译安装pgvector git clone pgvector源码仓库 cd pgvector make sudo make install编译完成后在目标数据库里执行CREATE EXTENSION vector;如果这一步没报错说明扩展安装成功功能已经可用。注意PostgreSQL大版本之间扩展二进制是不兼容的。如果你换了大版本号需要重新编译安装pgvector旧版本安装路径下的二进制文件不会被新版本沿用。2.2 数据集构造和装载策略测试数据我用的是一个公开可下载的128维向量数据集格式是浮点向量加文本ID。考虑到现实业务中向量表往往还挂着标题、类目、摘要等字段我在建表时做了适当补充模拟一个带文本元信息的向量表CREATE TABLE items ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, content text, category text, embedding vector(128) );装载数据我用的是Python配合psycopg2驱动分批写入。这里有个容易被忽视的点不要让每条数据都走单次INSERT最好用execute_values或者COPY协议批量导入。原因很简单单条INSERT的事务开销、网络往返和WAL日志开销在百万级数据量面前会被无限放大。我实际导入500万条向量时批量写入的耗时大约只有逐条写入的零头。import psycopg2 from psycopg2.extras import execute_values conn psycopg2.connect(hostlocalhost dbnamevector_test userpostgres) cur conn.cursor() # 这里只是示意实际数据由向量文件解析而来 rows [] for i in range(1_000_000): vec [ ,.join([str(0.01 * (i % 100)) for _ in range(128)]) ] rows.append((i, fdoc-{i}, sample, vec)) execute_values( cur, INSERT INTO items (id, content, category, embedding) VALUES %s, rows, page_size5000 ) conn.commit() cur.close() conn.close()装载过程中还要顺手做两件事关闭查询计划中的全表扫描开关只是临时手段更重要的更新统计信息让优化器在后续查询中能正确估算代价。数据装载完成后执行了一次ANALYZE items;这一步虚拟数据量越大对稳定性的影响越明显。2.3 初始性能基线的记录在创建任何向量索引之前我先记录了一组“裸查询”基线数据。所谓裸查询就是直接在500万条记录上做暴力精确扫描。查询写法非常直接SELECT id, content FROM items ORDER BY embedding - [0.01,0.02,...] LIMIT 10;实测结果是单次暴力查询延迟在1.8到2.5秒之间波动P99能到3.2秒左右。这个数字是很丑陋的正常业务根本承受不了这样的延迟但它有两个价值第一它为我们后面计算召回率提供精确基准第二它让全表扫描代价变成一个可感知的标尺后续任何一分性能提升都能和这个基数做对比。可以说没有这组基线数据后面的调参和评测都是盲人摸象。3. 索引构建与查询细节实测3.1 IVFFlat和HNSW的构建差异在构建索引的对比测试中我分别创建了IVFFlat和HNSW索引。IVFFlat的关键参数是列表数量lists和查询探针数量probes。列表数越多每个桶越小构建阶段需要的聚类计算量越大探针数量越大扫描的候选桶越多召回率越高但延迟也会同步上升。创建IVFFlat索引的语句如下CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) WITH (lists 1000);HNSW索引的创建方式类似只是参数完全变了CREATE INDEX ON items USING hnsw (embedding vector_l2_ops) WITH (m 16, ef_construction 64);m参数控制图中每个节点的最大连接数ef_construction控制构建时动态候选集的规模。这两个参数直接决定了图的质量m越大图越稠密搜索路径越多召回率越高但索引文件更大、构建更慢ef_construction越大构建期候选探索更充分同样能提升质量代价也是资源和时间。实测下来HNSW在500万条数据上的构建耗时大约是IVFFlat构建耗时的1.6倍这个差距主要体现在图结构构建的逐点插入过程上。但IVFFlat的构建过程包含聚类训练如果数据分布和后续分布差异较大召回率会劣化明显这里就体现出了HNSW对数据分布不敏感的优势。3.2 查询语法和索引启用的验证向量查询的核心语法就是内建的距离算子。欧氏距离用的是-余弦距离用的是内积用的是#。距离算子的选择必须与索引创建时指定的ops一致否则索引根本用不上查询会掉回全表扫描。我做了个对比实验同一个查询分别用两种距离算子执行-- 走HNSW索引的查询 SET enable_seqscan off; EXPLAIN ANALYZE SELECT id FROM items ORDER BY embedding - [0.01,0.02,0.03,...] LIMIT 10; -- 走暴力精确扫描的查询 SET enable_seqscan on; EXPLAIN ANALYZE SELECT id FROM items ORDER BY embedding - [0.01,0.02,0.03,...] LIMIT 10;从EXPLAIN ANALYZE的输出可以明显看到当enable_seqscanoff时查询计划选择的是Index Scan using items_embedding_idx而当enable_seqscanon时计划退化为Seq Scan。这里有一个细节需要特别强调SET enable_seqscanoff只能作为诊断手段不能作为生产环境的常驻配置。你不可能让所有查询都强制走索引优化器的代价估算才是长期决策依据。3.3 新版本迭代索引扫描的实测新版本带来的一个明显变化是迭代索引扫描能力。简单说HNSW索引在传统模式下每次查询只返回一个批次的结果集合扫描过程中无法配合查询过滤条件做动态剪枝。但迭代索引扫描允许查询执行器更好地和索引协同在查询中如果带有对类目、内容等标量字段的过滤条件可以提前滤掉不匹配的节点从而显著减少无效距离计算。为了验证这个能力我在查询里加入了category条件SELECT id, content FROM items WHERE category sample ORDER BY embedding - [0.01,0.02,0.03,...] LIMIT 10;对比不带过滤条件的查询过滤后整体延迟大约下降了22%。这个数字本身不是重点重点是它给了我们一种新的可能性向量检索和标量过滤可以更紧密地协同工作而不是先查向量再过滤标量。如果你的业务里面有大量“按类目检索”的诉求这个特性值得重点关注。4. 性能数据与召回率对比4.1 索引构建耗时和资源占用我把500万条128维向量的构建结果整理成一张表便于直观对比。构建参数用的是我上一节提到的默认测试值没有做额外调优索引类型构建耗时索引大小内存峰值说明IVFFlat约18分钟约220MB中需要聚类训练耗时主要在训练阶段HNSW (m16, ef_construction64)约30分钟约410MB较高图构建阶段CPU占用明显内存峰值约1.4GBHNSW (m32, ef_construction128)约52分钟约780MB高参数翻倍后构建时间接近翻倍索引容量约扩大一倍从这个结果能看出HNSW的参数不能盲目加大。m从16加到32构建时间几乎翻倍索引体积也翻倍这对频繁更新数据和内存受限的环境是很大的代价。如果你只是想把召回率从95%推到97%不一定值得付出这个成本后面参数调优部分会具体说。4.2 查询延迟与QPS实测我用一个查询压测脚本模拟100个线程并发发送查询请求每个线程连续执行1000次查询查询向量随机从测试集里抽取。HNSW索引的ef_search我分别设置为10、40、80三档记录平均延迟、P99延迟和实际QPS。ef_search平均延迟(ms)P99延迟(ms)QPStop-10召回率106.816.2约47091.2%4012.528.4约26096.8%8021.347.6约15098.1%这组数据给了两个很明确的结论第一ef_search对延迟的影响几乎是线性的从10调到40延迟从6.8毫秒跳到12.5毫秒几乎翻倍第二召回率在ef_search40之后递增幅度明显放缓从40到80只涨了1.3个百分点但延迟几乎又翻了一倍。所以对这个数据集而言ef_search40是一个性价比很高的操作点。QPS数值和机器配置强相关我列出的数据只代表这个特定测试环境。但趋势可以复用你的机器越好、并发越高QPS值会整体上移但ef_search对延迟和召回率的影响曲线形状是类似的。4.3 新版本半精度与量化特性实测新版本在降低存储和内存成本上引入了几个新东西最直接的是半精度向量类型和标量量化支持。半精度向量把每个维度从4字节的float降到2字节内存占用直接减半标量量化则进一步把每个维度压到1字节的int8内存占用变成原来的四分之一。我在同样的500万数据上做了半精度存储测试方法很简单先建一个半精度向量列把原始float向量转换后写进去。构建HNSW索引后索引体积大约降到原来的55%左右查询延迟在低并发下基本持平召回率只下降了约0.8个百分点。这个结果很有吸引力尤其是在内存接近瓶颈的部署环境下。但必须提一句量化是有损压缩。如果你的业务对召回率极度敏感比如搜索精确率下跌0.5%都会影响收入那千万不要轻易在生产环境开启量化一定要在离线评测环境先验证损失幅度能否接受。5. 参数调优经验与避坑手册5.1 HNSW参数选择的实操建议HNSW的核心参数就三个构建期的m和ef_construction查询期的ef_search。m决定了索引图的节点度数常见的起点是16ef_construction决定了构建期候选队列大小常见起点是64。我的建议是不要一开始就上大参数先用小参数建立基线性能再逐步放大观察收益。调参的时候我犯过的一个错误是只盯着召回率忽略了构建效率。我一度把m调到48ef_construction调到256召回率确实冲上去了但索引构建时间从30分钟暴增到接近2小时这个代价在需要周期性重建索引的业务里是致命的。后来我调整了策略先固定ef_search40只在[16,48]区间内逐步增加m每增加一档记录构建耗时和召回率增量当召回率增量低于0.3个百分点时果断停手。5.2 查询性能和内存的避坑记录HNSW索引在查询时会出现突发内存占用尤其是高并发情况下。我在并发测试中把连接数调到200时数据库进程的内存占用一度接近机器上限导致部分查询被系统OOM Kill。这个问题的根源是每个查询会话都会维护一个距离计算用的候选堆并发一多堆内存乘以会话数压力就被放大。解决办法有两个方向。第一控制应用侧连接池的最大连接数比如限制在50到100之间第二减小work_mem参数让每个查询候选堆不要撑得太大但这个方法会影响复杂的排序操作需要谨慎调整。更稳妥的做法是给数据库单独配置内存上限同时监控共享内存和进程私有内存的总和。5.3 生产环境中容易被忽略的索引维护很多人建完索引就以为万事大吉实际上向量索引和普通B-tree索引一样需要维护。HNSW图结构在持续插入新数据后索引新鲜度会逐步下降删除和更新的数据可能留下残渣节点导致查询路径变长、召回率降低。建议建立定期维护计划数据量增长超过15%到20%就评估是否需要重建索引如果业务存在大量批量删除删除后紧接着跑一次REINDEX。我实测过在500万条数据里批量删除100万条后不重建索引查询延迟会增长约25%召回率下降1到2个百分点这个劣化程度视数据分布而定但方向是可以复现的。6. 常见问题与排查技巧实录6.1 查询不走索引的排查方法向量查询不走索引是最常见的坑而且很多时候不会报错就是默默走全表扫描然后业务方抱怨查询怎么越来越慢。遇到这类问题第一件事永远是看执行计划EXPLAIN (ANALYZE, BUFFERS) SELECT id FROM items ORDER BY embedding - [0.01,0.02,...] LIMIT 10;如果执行计划显示Seq Scan那基本可以确定三件事中至少有一件出了问题索引没有创建成功、查询用的距离算子和索引ops不匹配、或者优化器认为全表扫描反而更便宜。前两个问题从系统表和索引定义能查出来第三个问题则比较隐蔽一般发生在数据量很小或统计信息陈旧的时候。小数据量下全表扫描确实不慢索引的优势体现不出来是正常现象不必强行优化。6.2 索引构建过慢的常见原因HNSW构建慢的原因不外乎几个m和ef_construction参数设置过大、maintenance_work_mem不够导致频繁落盘、CPU线程没有充分利用、以及构建期间系统还在跑其他大查询。我遇到最典型的一次是构建期间一个分析型查询把CPU占满HNSW索引构建时间直接被拖长了1倍多。排查思路是先看系统监控确认CPU、磁盘IO是否正常再看构建会话是不是被其他查询阻塞。向量索引构建对资源属于“贪婪型”需求构建窗口最好安排在业务低峰期并且适当调大maintenance_work_mem让临时排序尽量在内存中完成。6.3 常见问题速查表问题现象可能原因处理建议查询走了Seq Scan距离算子与索引ops不匹配确认使用与索引创建时相同的距离算子查询走了Seq Scan统计信息陈旧执行ANALYZE更新统计信息HNSW构建时间异常长maintenance_work_mem过小临时调大该参数后重建索引查询延迟突增索引老化执行REINDEX重建向量索引高并发下内存溢出查询会话太多控制连接池大小或缩减ef_search召回率低于预期ef_search设太小逐步增大ef_search观察召回率曲线新版本安装后报错找不到文件扩展二进制路径旧重新编译并执行make install6.4 一个典型的召回率排查案例我这里说一个遇到的典型案例。某同事反馈他那边同一张表、同样的查询语句召回率从95%暴跌到80%排查了好久都找不到原因。我拿到执行计划一看查询里多了个WHERE created_at now() - interval 7 days条件这个条件在向量索引扫描时无法被迭代索引扫描合理剪枝优化器选择了先取候选集再过滤而候选集大小被ef_search限制住过滤后剩下的结果远不足10条很多真正的近邻在过滤阶段被丢弃了。这个案例的教训是召回率不是只由索引参数决定的候选集的规模和过滤条件的交互关系也会显著影响最终结果。如果你遇到“明明索引参数没动但召回率莫名其妙掉下去”的情况优先检查查询条件有没有新增过滤字段而不是一上来就重建索引。最后补一个实用技巧评测收尾的时候我再说一个很实用的小技巧。做召回率对比时我和基准的暴力扫描结果做了个EXCEPT集合差用来找出“被索引漏掉但实际应该召回”的点。但500万条数据上暴力扫描很慢每次都全表扫描不现实。我的做法是抽1000条查询向量作为固定评测集每次只在这1000条上做暴力扫描拿基准结果然后用同一批查询向量测HNSW召回率两次结果一对比指标就稳定可复现了。这个技巧帮我节省了大量时间也避免了评测过程中“每次查询基准确率都不一样”的尬尴情况。如果你也要给业务方出评测报告这个方法比临时抽几条查询看一眼准得多。评测就到这里希望这篇实测记录能帮你在用pgvector新版本做向量检索时少走几步弯路。