用现有 Postgres 搭建 RAG 向量检索
教程介绍通过 pgvector 在 Postgres 内存储嵌入并执行相似度检索,将文档、用户与权限条件放入同一条 SQL。内容包含扩展安装、分块表结构及向量维度配置。
AI 深度解读
pgvector 给 RAG 提供了一条务实路线:如果文档、用户和权限已经放在 Postgres,就把向量检索也放进去。文本、向量和访问条件可以由同一条 SQL 处理,继续使用现有的事务、备份、JOIN 和时间点恢复能力,少维护一套需要同步的数据库。
原文真正有用的地方,是讲清了这条路线的代价:精确检索适合起步,近似索引用召回率换速度,而权限过滤可能让 LIMIT 5 连 5 条都拿不回来。选型之前,这些细节比「支持向量搜索」更值得看。
先把检索跑通,再考虑向量索引
RAG 的检索环节可以拆成三件事:保存文本块及其嵌入向量(embedding),找到与问题最接近的内容,再限制用户能看到哪些内容。pgvector 是开源 Postgres 扩展,把向量类型和距离运算带进现有数据库。
它支持 Postgres 13+,可通过 Docker、Homebrew、APT、Yum、PGXN、conda-forge 安装,一些托管服务已经预装。每个需要使用它的数据库执行一次 CREATE EXTENSION vector;。
原文的 doc_chunks 表包含 id、doc_id、team_id、doc_type、content 和 embedding。其中 team_id 承载访问范围,doc_type 区分 faq、runbook 等类型。OpenAI 的 text-embedding-3-small 默认输出 1536 维,因此向量列使用 vector(1536);已有表也可以执行 ALTER TABLE doc_chunks ADD COLUMN embedding vector(1536);。
下面这条查询按余弦距离取前 5 条,同时返回余弦相似度:
SELECT id, content, 1 - (embedding <=> $1) AS cosine_similarity
FROM doc_chunks
ORDER BY embedding <=> $1
LIMIT 5;
要看的是 ORDER BY:它按距离从小到大排序;展示分数时,再用 1 - 距离 转成相似度。
三个运算符别混用:<=> 表示余弦距离,<-> 表示 L2 距离,<#> 返回负内积。负号来自 Postgres 对运算符索引扫描升序的要求,需要真实内积时再乘 -1。
没有向量索引时,这属于精确最近邻搜索,按所选距离度量具有完整召回。但距离最近不保证业务上最相关,切块方式和 embedding 模型仍然影响结果。
索引加速了什么,又牺牲了什么
数据量和延迟还没形成压力时,可以先用精确搜索。它也能充当后续衡量近似检索的参照。
pgvector 提供 HNSW 和 IVFFlat 两种近似索引。查询运算符必须与索引操作符类匹配:<=> 对应 vector_cosine_ops,<-> 对应 vector_l2_ops,<#> 对应 vector_ip_ops。
这段代码创建余弦距离的 HNSW 索引:
CREATE INDEX ON doc_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64); -- 这些是默认值
最后一行控制建索引阶段:m = 16 是每层最大连接数,ef_construction = 64 控制构建候选范围。提高后者通常改善召回,但增加构建时间,并拖慢插入。
HNSW 可以在空表上创建,速度与召回率的权衡较好,代价是构建较慢、内存需求较高。查询时的 hnsw.ef_search 默认是 40,提高它会增加候选数量,通常提升召回并增加耗时。原文用 BEGIN; SET LOCAL hnsw.ef_search = 100; SELECT ...; COMMIT; 展示事务内调整,设置只对当前事务生效。
IVFFlat 构建更快、内存更省,但速度与召回率的权衡较弱,应在表里有数据后创建。README 建议:不超过 1M 行时,从 lists = rows / 1000 起步;超过后用 sqrt(rows);查询参数从 probes = sqrt(lists) 起步。示例是 lists = 100、SET ivfflat.probes = 10;,后者默认值为 1。
加上近似索引后,同一条 SQL 可能返回不同结果。
这是性能取舍,不能只看耗时下降就认定优化完成。
为什么权限过滤会让结果凑不够
原文最容易踩坑的细节是:使用近似索引时,过滤发生在索引扫描之后。
假设 team_id 条件只匹配全表 10% 的行,HNSW 使用默认 hnsw.ef_search = 40,平均只有 4 行符合过滤条件。即使 SQL 写了 LIMIT 5,也可能拿不到 5 个文本块。这里的问题是检索数量和召回,不是 WHERE 自动失去权限约束。
pgvector 0.8.0+ 提供迭代扫描,可以继续扫描直到找到足够结果,或达到扫描上限。开启方式是 SET hnsw.iterative_scan = strict_order;,也可选择召回表现更好的 relaxed_order。HNSW 的 hnsw.max_scan_tuples 默认是 20,000;IVFFlat 对应 ivfflat.max_probes。
其他选择取决于数据分布:过滤后只剩很少行,可以给 team_id 建 B-tree 索引,再进行精确距离计算;只围绕少数固定值查询,可以为 doc_type = 'faq' 建部分向量索引;多租户还可以考虑按租户列表分区。原文提醒,共享近似索引时,一个租户的向量会影响其他租户的召回和速度。
Python 示例使用 psycopg 3 和 pgvector-python,安装命令是 pip install "psycopg[binary]" pgvector openai。每个连接需要 register_vector(conn) 注册向量类型,连接池应通过配置函数处理,异步连接使用 register_vector_async。
示例调用 client.embeddings.create(model="text-embedding-3-small", input=text) 生成向量,再用带 WHERE team_id = ... 的参数化 SQL 检索。换成 Node 接入时,也应保留这个边界:查询向量、权限条件和返回数量由服务端组织,租户身份需要来自可信的认证上下文。
我的判断
把已有权限数据和向量放在一起,确实减少了同步链路。对已经运行 Postgres、检索负载还不重的团队,这个起点很合理。
但原文没有给出实测规模、延迟、硬件配置,也没有与专用向量数据库做对照。它的 SaaS 支持文档流程明确是假设场景。因此,「值得先试」有依据,「一定更快、更省」则没有数据支撑。
还要拆开看「同一事务更新文档与向量」这条建议。数据库事务可以让两者一起提交,但远程 embedding API 不受 Postgres 事务控制。原文没有说明生成失败、请求超时和并发更新如何处理。实际接入时,需要明确先生成再提交,还是异步生成并管理版本。
错误码、SKU 等精确词也未必适合只靠向量搜索。原文建议结合 Postgres 全文检索,再用 Reciprocal Rank Fusion 或 cross-encoder 合并、重排;这条路线合理,但它没有提供效果对照。
当向量搜索开始争抢事务业务的 CPU 和内存,或现有部署缺少所需能力,再评估专用向量数据库。继续留在 Postgres 也有扩展选项:halfvec 的索引支持最高 4,000 维,vector 为 2,000 维,另有二进制量化和子向量索引。这些是能力选项,不能替代容量测试。
能拿来做什么
- 给已有 Postgres 的内部文档助手先做精确检索,准备一小组带相关文档标注的问题,同时记录答案相关性和查询耗时,作为加索引后的比较基线。
- 在 Node 或 Python 服务端把租户权限写进检索 SQL,重点测试窄权限范围下是否返回足够结果,再决定调高
ef_search、开启迭代扫描或采用过滤列索引。 - 为错误码、产品编号等问题加入全文检索对照,确认纯向量检索漏在哪里,再决定是否引入融合与重排。
- 给文档更新设计明确的向量版本和失败处理流程,同时观察检索与事务业务的资源竞争,再判断是否需要拆库。