pgvector 实战:在 PostgreSQL 里做向量检索

做 RAG 或语义搜索,绕不开「给一段文本找出语义最相近的 K 段候选」这件事。站内此前的《向量数据库选型指南:从暴力搜索到 HNSW》讲过选型逻辑:十万级以下、低并发的场景,引入专用向量库往往是过度设计。而现实中大量团队的知识库、工单、内容数据本来就在 PostgreSQL 里——pgvector 给 Postgres 加一个 vector 类型和两套向量索引,让你在同一个库里把结构化数据和向量一起存、一起查、一起备份,少养一套独立组件。还有一个常被低估的好处:事务一致性。文档删除或更新时,向量作为普通列随行增删改,不会出现「主库已删、向量库还在」的双写不一致;权限、备份、监控也全部复用现有体系。

那什么时候该换专用库?把选型指南的分层逻辑沿用下来即可:向量规模千万级以下、QPS 不极端、过滤条件想直接用 SQL 的 JOIN 和 WHERE 表达,pgvector 是默认答案;到了数亿级向量、单机内存装不下索引、需要水平分片或多租户强隔离时,再迁移 Qdrant/Milvus 这类专用系统——向量只是业务库的一列,迁移成本可控。这是本文作者基于截至 2026-10-07 产品格局的一般性建议,具体以自家压测为准。

安装与建模

pgvector 是 PostgreSQL 扩展,要求 Postgres 13+。各家托管数据库(RDS、Supabase 等)与官方 Docker 镜像都预装了它,自建也可以编译或 apt/yum 安装;老库升级扩展用 ALTER EXTENSION vector UPDATE;。本文所有 SQL 均在官方镜像 pgvector/pgvector:pg18(PostgreSQL 18.6 + pgvector 0.8.7,2026-10-01 发布,截至 2026-10-07 的当前版本)上实测通过。

CREATE EXTENSION vector;

CREATE TABLE docs (
  id        bigserial PRIMARY KEY,
  title     text NOT NULL,
  embedding vector(4)   -- 维度与 embedding 模型输出对齐,常见 768/1024/1536
);

维度写死在列上有两个好处:插错维度立刻报错;上限明确——vector 类型最多 16000 维(实测 vector(16001) 直接报 dimensions for type vector cannot exceed 16000)。768 维 float4 存储是 4×768+8 字节 ≈ 3KB,估算内存账后再决定要不要用半精度的 halfvec(2 字节/维)。

接着建索引。pgvector 提供两种近似索引,索引方法里的操作符类(operator class)决定按哪种度量排序:

-- HNSW:图索引,空表就能建,查询快、召回稳,建得慢、更占空间
CREATE INDEX idx_docs_hnsw ON docs
  USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);

-- IVFFlat:倒排聚类索引,必须先有数据再建(要做 k-means),省内存,召回受 probes 影响大
CREATE INDEX idx_docs_ivf ON docs
  USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

默认参数即可起步:HNSW 的 m=16、ef_construction=64 是官方默认值;IVFFlat 的 lists 官方建议按「行数/1000(百万行以内)、√行数(超过百万)」取。

查询语义:操作符与函数的对应

距离度量 操作符 距离函数 索引操作符类
L2 欧氏距离 <-> l2_distance vector_l2_ops
内积 <#>(负内积) inner_product vector_ip_ops
余弦距离 <=> cosine_distance vector_cosine_ops
L1 曼哈顿距离 <+> l1_distance vector_l1_ops

实测 <-> 与 l2_distance、<=> 与 cosine_distance 结果相等,操作符就是函数的索引友好写法。从示例值也能看出度量的脾气:cosine_distance('[1,0,0,0]', '[1,0,0,0]') 为 0(方向相同),l2_distance('[1,0,0,0]', '[0,1,0,0]') 为 √2 ≈ 1.41,<#> 对正交向量返回 -0。<#> 返回负内积是因为 Postgres 的索引扫描只支持升序——内积越大越相似,取负号才能让「升序 = 最相似在前」,用它排序后记得把结果负回来。文本语义检索最常用的组合是 <=> 配 vector_cosine_ops。

近邻查询的标准写法是「ORDER BY 距离操作符 + LIMIT」:

SELECT id, title, embedding <=> $1 AS distance
FROM docs
ORDER BY embedding <=> $1
LIMIT 5;

索引生效有三个硬条件:ORDER BY 的是距离操作符本身(不能包在表达式或函数里)、升序、带 LIMIT——官方 Troubleshooting 原话是要有 ORDER BY 与 LIMIT,且 ORDER BY 必须是距离操作符的结果。缺一个就退化为顺序扫描。

实战:从建表到 EXPLAIN

一份可直接复制的完整脚本(示例用 4 维手写向量,换真实 embedding 后流程不变;本节全部语句实测跑通):

CREATE TABLE docs (
  id        bigserial PRIMARY KEY,
  title     text NOT NULL,
  embedding vector(4)
);

INSERT INTO docs (title, embedding) VALUES
  ('数据库内核原理',   '[1,0,0,0]'),
  ('分布式一致性协议', '[0.2,1,0,0]'),
  ('向量检索入门',     '[0.9,0.1,0,0]'),
  ('React 组件设计',   '[0,0,1,0]'),
  ('Vue3 响应式原理',  '[0,0,0.9,0.1]');

CREATE INDEX idx_docs_hnsw ON docs
  USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);

SELECT id, title, embedding <=> '[1,0.2,0,0]' AS dist
FROM docs ORDER BY embedding <=> '[1,0.2,0,0]' LIMIT 3;

EXPLAIN (COSTS OFF)
SELECT id FROM docs ORDER BY embedding <=> '[1,0.2,0,0]' LIMIT 3;

实测有个反直觉的结果:表里只有 5 行时,EXPLAIN 显示的是 Seq Scan + Sort,HNSW 索引根本没被用——规划器认为这么小的表全表扫描更便宜,这是正常行为而非配置错误。往表里灌到三百多行再看同一个 EXPLAIN:

Limit
  -> Index Scan using idx_docs_hnsw on docs
       Order By: (embedding <=> '[1,0,0.5,0.2]'::vector)
       Index Searches: 1

所以「EXPLAIN 里看不到索引」先别急着排查参数,先看行数是否值得走索引。日常诊断用 EXPLAIN ANALYZE 能看到 Buffers 和真实耗时;PostgreSQL 18 的输出里还多了 Index Searches 一行。

关键调参与坑

HNSW:建索引时 m 控制每个节点的连接数(越大越准越占内存),ef_construction 控制建图候选队列(越大建得越慢、图质量越好);查询时 SET hnsw.ef_search = 100;(默认 40)抬高查询候选队列,召回率上升、延迟也上升。这类会话级参数也可以只对单个事务生效:BEGIN; SET LOCAL hnsw.ef_search = 100; ...; COMMIT;,提交后自动还原,避免污染连接池里的其他查询。0.8.0 起支持迭代扫描:SET hnsw.iterative_scan = strict_order;(或用 relaxed_order 换更高召回),配合 hnsw.max_scan_tuples(默认 20000)在带过滤条件的查询结果不足时自动多扫一段索引。它是缓解而非根治——过滤性太强的查询,正确姿势还是先做结构化过滤再算距离。

IVFFlat:SET ivfflat.probes = 10;(默认 1,官方建议起点是 √lists)控制一次查多少个聚类桶。「lists 调大、probes 忘了调」是召回率暴跌的常见原因。另外它按建索引时的数据聚类,数据分布大变(比如翻倍)后要重建索引。

建索引提速:SET maintenance_work_mem = '8GB'; SET max_parallel_maintenance_workers = 7; 让图尽量在内存里并行构建;生产环境用 CREATE INDEX CONCURRENTLY 避免阻塞写入。

维度上限(以下错误信息均为 0.8.7 实测原文):

  • 列维度:vector 与 halfvec 都是 16000 封顶;
  • HNSW 索引:vector 类型最多 2000 维,超了报 column cannot have more than 2000 dimensions for hnsw index;halfvec 最多 4000 维;
  • 列超过 2000 维又想走 HNSW,改用 halfvec 表达式索引——实测 vector(3000) 列上可建、且查询走 Index Scan:
CREATE INDEX ON docs USING hnsw ((embedding::halfvec(3000)) halfvec_cosine_ops);

维护:HNSW 的 VACUUM 出名地慢,官方建议先 REINDEX INDEX CONCURRENTLY idx_docs_hnsw; 再 VACUUM docs;(两条语法均实测跑通)。版本尽量新:0.8.3/0.8.4 连修了 HNSW vacuum 期间的索引损坏问题,最新的 0.8.7 又修了一个 IVFFlat 建索引的缓冲区溢出。

混合检索:向量 + 全文检索

纯向量检索抓不住型号、缩写这类精确词。pgvector 不用出库就能配 PostgreSQL 原生全文检索:加一列 tsvector 建 GIN 索引,查询时语义一路 <=> 排序、关键词一路 ts_rank 排序,两份列表用 RRF(倒数排名融合,1/(k+rank),k 常取 60)合并,取 Top-N 交给 rerank 精排。「粗召回管别漏、重排管别乱」的三段式成本账,站内《混合检索与重排序:BM25、向量与 Rerank 的三段式》已经算过,这里只给融合骨架(实测跑通的最小版本):

WITH semantic AS (
  SELECT id, row_number() OVER (ORDER BY embedding <=> $1) AS rank
  FROM docs LIMIT 50
),
keyword AS (
  SELECT id, row_number() OVER (ORDER BY ts_rank(content, $2) DESC) AS rank
  FROM docs WHERE content @@ $2 LIMIT 50
)
SELECT COALESCE(s.id, k.id) AS id,
       COALESCE(1.0/(60+s.rank), 0) + COALESCE(1.0/(60+k.rank), 0) AS score
FROM semantic s FULL JOIN keyword k ON s.id = k.id
ORDER BY score DESC LIMIT 10;

一个中文特有的坑,实测确认:to_tsvector('simple', '向量检索实战') 会把整句当成一个词——内置分词按空格切,中文必须用 zhparser / pg_jieba 这类分词扩展,或应用侧分好词再用空格拼进 tsvector,否则关键词这一路基本空转。

小结

pgvector 的定位很清楚:不是要打赢专用向量库的极限性能榜,而是让「向量检索」降级成一个普通的数据列——有事务、有 SQL、有备份、有权限,团队已有的 Postgres 运维经验全部复用。百万级以下的知识库与语义搜索,先把这一列用明白,是性价比最高的路线。上线前再拿真实模型输出的 embedding、按生产规模压一轮召回率(拿暴力扫描的精确结果当基准),比任何经验参数都可靠。

参考资料

← 返回资讯列表

读者留言

COMMENTS 暂无
仅本站原创文章开放留言 · 请勿留下手机号、邮箱等个人信息

还没有留言,来说第一句?