跳到正文

PgSQL 与 AI:向量不离开业务库 ​

一句话:把向量当成普通列,让「元数据过滤 + 向量召回 + 业务写入」在同一个库、同一个事务里完成;代价是单机内存和索引构建。

适用 PostgreSQL 16/17、pgvector 0.8.x。pgvector 0.8 起支持迭代索引扫描;更早版本没有 hnsw.iterative_scan。

为什么需要 ​

在 RAG 链路里,PostgreSQL 承担的是数据层,不是编排层:

  • 存切片内容、文档元数据、租户、权限、状态。
  • 存 embedding,并提供按距离的 topK 召回。
  • 存 tsvector,提供关键词召回,和向量召回做融合。
  • 用事务保证「切片写入 + 文档状态更新 + 计数」一致。

它不负责调用 LLM、不负责对话状态、不负责原始文件存储。会话状态放 Redis,原始文件放对象存储,事件放消息队列。这样分工的目的是:让需要事务一致的那部分状态留在一个库里,而不是把整个 AI 栈塞进一个数据库。

AI 带来的不是「多了一个 vector 类型」,而是几种访问模式同时出现:

  • 写入模式:文档入库时批量切片、批量 embedding、批量 INSERT;一次可能几千到几万行,事务比典型 OLTP 长。
  • 读取模式:topK 召回,带租户/权限/状态过滤;延迟敏感,p99 比平均更重要。
  • 混合检索:向量召回 + 关键词召回,再按排名融合;两路召回的候选集和排序逻辑不同。
  • 重建负载:换 embedding 模型后,整表回填新向量;期间旧查询不能停。
  • 隔离要求:多租户共享表时,每条查询都必须带 tenant_id;漏一条就是越权。

这些负载和「按主键增删改查」不同:它们更依赖索引内存、ANN 参数、过滤选择和批量写入的 WAL 量。

核心机制 ​

能力与参数 ​

能力版本/默认值说明
vector(n)pgvector 0.8.xfloat32,4 字节/维
halfvec(n)pgvector 0.7+float16,内存约减半
bit(n) / sparsevec(n)pgvector 0.7+二值量化、稀疏向量
HNSWm=16、ef_construction=64、ef_search=40默认值;ef_construction=200 是常见调优值
IVFFlatlists=100、probes=1需要表里先有数据才能训练
迭代索引扫描pgvector 0.8+hnsw.iterative_scan=relaxed_order,缓解过滤后召回不足
JSONB + GINPostgreSQL 内置存元数据、标签、权限快照
tsvector + GINPostgreSQL 内置关键词召回;中文需 zhparser/pg_jieba
pgai / pgvectorscale / pg_cron / pg_partman第三方扩展按需引入,版本与兼容性以各自文档为准

参数口径要和 06 篇一致:ef_construction 的 64 是 pgvector 默认值,200 是常用调优值;ef_search 的 40 是默认值,100 是常见调优值。不要把「默认」和「调优」混写。

索引与召回路径 ​

HNSW 是分层图,查询快、召回高,但索引常驻内存、构建慢;IVFFlat 先聚类再倒排,构建快、省内存,但必须先有数据训练。两者都只加速 ORDER BY embedding <距离运算符> ... LIMIT K 这类查询,距离运算符必须和索引 opclass 对上。

真正的难点是过滤:先 ANN 取 topK 再按 WHERE 过滤,条件一严可能一条不剩。pgvector 0.8 的迭代扫描会继续扫索引直到凑够 K 条,代价是延迟上升;另一种做法是给高选择性过滤列建 B-tree,让优化器先过滤再算距离,实际走哪条路径要用 EXPLAIN 确认。

混合检索不要直接把距离和 ts_rank 相加,量纲不同;用 RRF 按排名融合,见 09 篇的可执行 SQL。向量索引的更新不属于业务事务:事务提交后新数据还要等索引追上才进入检索路径。

最小上手 ​

sql
-- PostgreSQL 16/17 + pgvector 0.8.x
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE doc (
  id          bigint PRIMARY KEY,
  chunk_count int NOT NULL DEFAULT 0
);

CREATE TABLE doc_chunk (
  id        bigserial PRIMARY KEY,
  tenant_id bigint NOT NULL,
  doc_id    bigint NOT NULL REFERENCES doc(id),
  status    text   NOT NULL,
  content   text   NOT NULL,
  embedding vector(3) NOT NULL          -- 演示用 3 维,生产按模型改
);

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

INSERT INTO doc (id, chunk_count) VALUES (100, 0);

BEGIN;
INSERT INTO doc_chunk (tenant_id, doc_id, status, content, embedding)
VALUES (1, 100, 'published', '退款政策', '[0.1,0.2,0.3]'::vector);
UPDATE doc SET chunk_count = chunk_count + 1 WHERE id = 100;
COMMIT;

SET hnsw.ef_search = 100;   -- 默认 40;>= LIMIT

SELECT id, content, embedding <=> '[0.1,0.2,0.3]'::vector AS distance
FROM doc_chunk
WHERE tenant_id = 1 AND status = 'published'
ORDER BY embedding <=> '[0.1,0.2,0.3]'::vector
LIMIT 5;

EXPLAIN (ANALYZE, BUFFERS)
SELECT id FROM doc_chunk
WHERE tenant_id = 1 AND status = 'published'
ORDER BY embedding <=> '[0.1,0.2,0.3]'::vector
LIMIT 5;

期望看到 Index Scan using idx_chunk_embedding。如果过滤条件选择性很高,优化器也可能先走 tenant_id/status 的 B-tree 再排序;用 EXPLAIN 确认实际路径,不要假设一定走 HNSW。

生产动作 ​

  • 建索引:HNSW 构建慢且吃 maintenance_work_mem(默认 64MB);在线表用 CREATE INDEX CONCURRENTLY,先在测试环境估算时间和内存。
  • 过滤与召回:给高选择性过滤列建 B-tree;过滤后召回不足时再开迭代扫描,并用 recall@K 验证,不要只看单条查询。
  • 写入一致性:向量写入、业务状态更新、计数放在同一事务;批量写入控制单批行数,避免长事务拖住 Vacuum 和 DDL。
  • 备份恢复:向量列随表备份;恢复后校验索引和模型版本。换模型时加新列/新表,不要原地覆盖。
  • 扩展升级:pgvector、zhparser、pg_partman 都要跟 PostgreSQL 大版本升级;升级前验证索引兼容性。
  • 容量与监控:1536 维 float32 约 6KB/条;盯索引大小、内存余量、p99 和 recall@K,内存不足时 HNSW 会断崖式变慢。
  • 多租户:每条查询强制带 tenant_id;用行级安全或统一数据访问层兜底,不能只靠调用方自觉。

边界 ​

规模/约束选择理由
百万级以内、已有 PostgreSQL、要事务/JOINpgvector一条 SQL 完成过滤 + 召回,运维最省事
百万到千万、单机内存足够、过滤复杂pgvector + 分区/只读副本先压测 p99 和 recall@K,确认内存余量
千万以上到亿级、高 QPS、多租户隔离专用向量库(Milvus 等)分布式、弹性扩缩、专用索引
需要跨地域、GPU 索引、复杂多路召回专用向量库 + 业务库接受双写和回表,做一致性补偿

这些是社区常见经验分界,不是硬指标;同样 1000 万条,1536 维和 256 维的内存差好几倍。判断标准可以反过来问:删掉「AI」两个字,这套架构是否仍然成立?如果只是普通业务表加一个向量列,pgvector 的价值主要在少一套系统;如果负载的核心是亿级 ANN、弹性扩缩和多租户隔离,专用向量库才是主线。

讲清楚 ​

AI 应用的数据层不是「把向量塞进数据库」这么简单。切片、向量、租户、权限、状态天然要一起写、一起查,分散到向量库和业务库就要处理双写、回表和一致性补偿;放在 PostgreSQL 里,这些字段在同一张表,写入是一个事务,召回是一条带过滤的 SQL。代价也很明确:HNSW 索引吃内存、构建慢,单机扩展性有限,过滤条件一严还会把 topK 筛空。所以选型看规模:百万到千万级、过滤复杂、要求事务一致,pgvector 最省事;上亿条、高 QPS、要弹性扩缩,交给 Milvus 这类专用库,并用 recall@K、p99 和真实压测而不是感觉做决定。

把一门技术讲透到既懂也会用