为什么说 pgvector 是中小规模 RAG 项目的首选?从选型到最小可用环境实战
前言
在前面的 RAG 系列里,我们已经聊过文档解析切块、混合检索与 Rerank。但回退到最底层,所有这些检索优化都建立在一个前提上:向量得有一个地方存,而且存得能快速查。
于是每个做 RAG 的开发者都会撞上同一个架构决策:
到底该上专门的向量数据库(Milvus、Qdrant、Weaviate),还是在现有数据库上加一个插件就够了?
很多人下意识觉得"专门的肯定更强",于是第一天就引入了 Milvus,配上 etcd、MinIO、消息队列一整套依赖。团队花了三天搭环境,半个月踩运维坑,最后知识库里只有两万条向量。
对于大多数中小规模项目(向量数据量在百万级以下)来说,PostgreSQL + pgvector 的组合往往是工程性价比最高的选择。这篇文章就来讲清楚:为什么 pgvector 是中小项目的"真香"选择,它和专用向量库到底差在哪,以及如何用最小步骤把它跑起来。
一、背景:暴力搜索为什么撑不到上线
很多团队在 Demo 阶段图省事,直接用"暴力搜索(Flat Search)"——也就是遍历全表,对每条向量计算相似度,再排序取 Top-K。
这种做法在数据量几千条时毫无感知,但一旦到了几十万条,问题就来了:
| 数据量 | 暴力搜索典型延迟 | 体感 |
|---|---|---|
| 1 万条 | < 50ms | 秒回 |
| 10 万条 | 200-500ms | 略卡 |
| 50 万条 | 1-3s | 明显卡顿 |
| 100 万条 | 5s+ | 基本不可用 |

根因很简单:暴力搜索的时间复杂度是 O(N×D),N 是向量数量,D 是维度。线性增长的数据量换来线性增长的延迟,没有任何"近道"可走。
生产环境的必然要求是引入向量索引(ANN,近似最近邻),用少量精度损失换取数量级的延迟下降。这正是 pgvector、Milvus 这类工具存在的意义。那为什么中小项目更该选 pgvector?
二、核心思路:pgvector 为什么"真香"
pgvector 是 PostgreSQL 的一个开源扩展,给 PG 加上了向量类型和向量索引能力。它的核心价值不是"比专用向量库更快",而是**“让你不用引入专用向量库”**。
具体来说有四大优势。
1. 技术栈统一:少一套系统,少一类故障
专用向量库是独立进程,要单独部署、单独监控、单独做备份和高可用。而 pgvector 就长在你已有的 PostgreSQL 里:
- 不需要新机器、不需要新容器编排。
- 备份策略沿用 PG 的
pg_dump/ 物理备份 / 流复制,运维同学已经会了。 - 连接池、慢查询日志、监控告警全部复用现有基建。
对一个中小团队来说,少引入一套中间件,就少一类线上故障。这条优势的工程价值往往被低估。
2. 事务强一致性:向量与业务数据同表同事务
这是 pgvector 最被低估、却最硬核的优势。
在 RAG 系统中,向量从来不是孤立存在的——每条向量都关联着业务元数据:文档标题、来源 URL、章节编号、权限标签、版本号等。如果用专用向量库,向量存在 Milvus、元数据存在 MySQL,两者之间靠一个 id 关联,就会产生经典的双写一致性问题:
- 写入成功但向量库写入失败 → 数据"有业务记录但查不到"。
- 删除时一方成功一方失败 → 出现"幽灵向量",检索出已删除的内容。
- 更新时两方版本不一致 → 召回的是旧版本文档。

而 pgvector 把向量和元数据放在同一张表、同一个事务里:
-- 向量和业务字段在同一行,同一个事务保证原子性
BEGIN;
INSERT INTO documents (id, title, category, version, content, embedding)
VALUES (101, 'K8s 扩容指南', '运维', 'v2', '...', '[0.1, 0.2, ...]');
UPDATE documents
SET content = '更新后的内容', embedding = '[0.3, 0.4, ...]'
WHERE id = 42;
DELETE FROM documents WHERE id = 7;
COMMIT; -- 三条语句要么全部成功,要么全部回滚
要么全成功,要么全回滚——不需要分布式事务,不需要最终一致性的补偿任务。对需要精确控制"哪些文档能被检索到"的 RAG 场景,这一点至关重要。
3. SQL 表达力:元数据过滤与向量搜索天然组合
pgvector 让你用熟悉的 SQL 把业务逻辑和语义检索合二为一。比如"查找 Java 领域、版本 ≥ v2 的相关文档",一条 SQL 搞定:
SELECT id, title, 1 - (embedding <=> :query_vector) AS similarity
FROM documents
WHERE category = 'Java' AND version >= 'v2'
ORDER BY embedding <=> :query_vector
LIMIT 5;
这种"WHERE 过滤 + ORDER BY 向量距离"的组合,是专用向量库要么不支持、要么需要额外配置才能实现的。而且你还能轻松做 JOIN——比如只检索某个用户有权限访问的文档:
SELECT d.id, d.title, 1 - (d.embedding <=> :query_vector) AS similarity
FROM documents d
JOIN document_acl a ON d.id = a.doc_id
WHERE a.user_id = :current_user
ORDER BY d.embedding <=> :query_vector
LIMIT 5;
向量检索 + 权限过滤 + JOIN,一条 SQL,一次往返。换成双系统方案,你得先查权限库拿到 doc_id 列表,再用这批 id 去过滤向量库的检索结果——多一次网络往返,还可能撞上"id 列表过长"的性能问题。
4. 生态扩展性:一套数据库打天下
PostgreSQL 的插件生态是其杀手锏。一套 PG 实例,可以同时承载:
- 向量检索:pgvector(ANN 索引 + 余弦/欧氏距离)
- 全文搜索:pg_bm25 / pg_trgm(关键词检索、模糊匹配)
- 地理信息:PostGIS(空间检索)
- JSON 处理:原生 JSONB(灵活的半结构化元数据)

这意味着前面系列文章讲的"混合检索(BM25 + 向量 + Rerank)",在 pgvector 方案里,BM25 和向量检索可以跑在同一个数据库、甚至同一条 SQL 里,不需要额外引入 Elasticsearch。
三、选型矩阵:pgvector vs Milvus vs Qdrant
说了这么多优势,也要客观看待边界。下面这张矩阵把三者放在同一张表里对比:
| 维度 | pgvector | Milvus | Qdrant |
|---|---|---|---|
| 定位 | PG 扩展,数据库插件 | 专用分布式向量库 | 专用向量库(Rust) |
| 事务一致性 | 原生 ACID,同表同事务 | 无关系型事务,需外部协调 | 无关系型事务 |
| 元数据过滤 | SQL WHERE / JOIN,极强 | 标量字段过滤 | Payload 过滤 |
| 分布式分片 | 依赖 PG(较弱) | 原生分布式,成熟 | 支持分片 |
| 百万级以下性能 | 优秀 | 优秀 | 优秀 |
| 千万~亿级性能 | 开始吃力 | 强项 | 强项 |
| 部署复杂度 | 低(一条命令装扩展) | 高(etcd + MinIO + 多组件) | 中(单二进制较友好) |
| 运维成本 | 低(复用 PG 运维) | 高 | 中 |
| 学习成本 | 低(会 SQL 就会) | 中(需学 SDK + 架构概念) | 中 |
| 适合团队 | 已有 PG 技术栈的中小团队 | 大规模、高 QPS、专业算法团队 | 追求高性能单机的团队 |
一句话总结:在百万级以下,三者的检索性能差异远没有它们在运维复杂度上的差异大。
四、实现步骤:最小可用 pgvector 环境
光说理论不够,下面用最少的步骤把 pgvector 跑起来。
步骤 1:用 Docker 起一个带 pgvector 的 PostgreSQL
官方 pgvector 仓库提供了打好包的 Docker 镜像,开箱即用:
# docker-compose.yml
services:
postgres:
image: pgvector/pgvector:pg16 # 基于 PostgreSQL 16,内置 pgvector
environment:
POSTGRES_USER: rag
POSTGRES_PASSWORD: rag123
POSTGRES_DB: ragdb
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
启动:
docker compose up -d
步骤 2:启用扩展并建表
-- 启用 pgvector 扩展(每个数据库执行一次)
CREATE EXTENSION IF NOT EXISTS vector;
-- 建表:业务字段 + 向量字段同表
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
category TEXT,
version TEXT DEFAULT 'v1',
content TEXT,
embedding vector(1536), -- 维度与你的 Embedding 模型一致
created_at TIMESTAMPTZ DEFAULT now()
);
注意:
vector(1536)的维度必须和 Embedding 模型输出维度完全一致。比如 OpenAItext-embedding-3-small是 1536 维,text-embedding-3-large是 3072 维,写错维度写入会直接报错。
步骤 3:写入与查询
-- 写入:向量用字符串字面量表示
INSERT INTO documents (title, category, version, content, embedding)
VALUES (
'K8s 集群扩容指南', '运维', 'v2',
'当节点资源不足时,可通过 cluster-autoscaler 自动扩容...',
'[0.012, -0.034, 0.078, ...]' -- 实际由 Embedding 模型生成
);
-- 查询:用余弦距离算子 <=> 找最相似的 5 条
SELECT id, title, category,
1 - (embedding <=> '[0.015, -0.030, 0.080, ...]') AS similarity
FROM documents
ORDER BY embedding <=> '[0.015, -0.030, 0.080, ...]'
LIMIT 5;
<=> 是余弦距离,值越小越相似,所以用 1 - (embedding <=> query) 可以得到 0~1 的相似度分数,方便业务展示。
步骤 4:创建向量索引(生产必备)
数据量超过几万条后,暴力搜索就会变慢,必须建 ANN 索引。这里先给一个最常用的 HNSW 索引(索引调优的细节留给下一篇展开):
-- 创建 HNSW 索引,使用余弦距离算子类
CREATE INDEX idx_documents_embedding
ON documents USING hnsw (embedding vector_cosine_ops);
建完后,同样的查询会自动走索引,延迟从几百毫秒降到个位数毫秒。
步骤 5:用 Python 串起完整链路
实际 RAG 系统中,向量是由 Embedding 模型在线生成的,不会手写 SQL 字符串。下面是用 Python + psycopg 把"生成向量 → 写入 → 检索"串起来的最小链路:
# pip install psycopg[binary] pgvector openai
import psycopg
from pgvector.psycopg import register_vector
from openai import OpenAI
client = OpenAI()
# 连接数据库并注册 vector 类型
conn = psycopg.connect("host=localhost dbname=ragdb user=rag password=rag123")
register_vector(conn)
def embed(text: str):
"""调用 Embedding 模型生成向量"""
resp = client.embeddings.create(
model="text-embedding-3-small", input=text
)
return resp.data[0].embedding
# 1. 写入
doc_embedding = embed("K8s 集群扩容指南:通过 cluster-autoscaler 自动扩容节点")
with conn.cursor() as cur:
cur.execute(
"INSERT INTO documents (title, category, version, content, embedding) "
"VALUES (%s, %s, %s, %s, %s)",
("K8s 扩容", "运维", "v2", "自动扩容内容...", doc_embedding),
)
conn.commit()
# 2. 检索
query_embedding = embed("k8s 怎么扩容")
with conn.cursor() as cur:
cur.execute(
"SELECT id, title, 1 - (embedding <=> %s) AS similarity "
"FROM documents "
"WHERE category = '运维' "
"ORDER BY embedding <=> %s LIMIT 5",
(query_embedding, query_embedding),
)
for row in cur.fetchall():
print(f"[{row[2]:.4f}] {row[1]}")
运行后,你会看到按相似度排序的检索结果。至此,一个最小可用的 pgvector RAG 存储层就跑通了。
五、运行结果与效果说明
在上面这个最小环境中,你可以观察到:
- 建索引前后延迟对比明显:10 万条数据无索引时查询约 300ms,建 HNSW 索引后降到 5-15ms。
- 元数据过滤生效:
WHERE category = '运维'只返回运维类文档,且仍走向量索引排序。 - 事务一致性可验证:在一个事务中插入文档和向量,回滚后两者都不存在,不会出现"幽灵向量"。
- 维度错误会被拦截:如果 Embedding 维度和建表维度不一致,写入直接报
vector dimension mismatch错误,不会静默截断。
六、常见问题与避坑
1. 数据量到底多大才该考虑专用向量库?
经验阈值:百万级(100 万)以下,pgvector 绰绰有余;千万级(1000 万)以上,认真考虑 Milvus/Qdrant;亿级以上,基本必须上专用分布式向量库。 但这个阈值不是绝对的,还取决于 QPS 要求、向量维度和延迟 SLA。
2. pgvector 的性能瓶颈在哪?
主要在两点:一是单机内存——HNSW 索引比较吃内存;二是单机扩展性——PG 不像 Milvus 那样原生支持水平分片。如果你的瓶颈是"单机扛不住数据量",就该考虑升级了。
3. 已经在用 MySQL,要为了 pgvector 换 PG 吗?
如果你已经有 PostgreSQL,直接加扩展即可。如果只有 MySQL 且团队完全没有 PG 经验,引入 PG 会增加学习成本——这时候要权衡"引入 PG"和"引入专用向量库"哪个对团队更友好。很多 Java 团队选择"MySQL 存业务 + Milvus 存向量"的双系统方案,代价是要自己解决双写一致性。
4. 维度写错了怎么办?
建表时 vector(1536) 是硬约束。写入一个 1024 维的向量会直接报错,不会截断或补零。这是好事——它在开发期就帮你拦截了错误,而不是让线上静默返回错误结果。
七、总结
记住一句话:RAG 的上限由数据质量决定,而向量数据库负责在低延迟下兑现这个上限。 在项目初期,pgvector 提供的"简单与稳定"往往比"高性能黑盒"更有价值。
选型决策可以浓缩成三条:
- 首选 pgvector:数据量 < 100 万,追求架构简单、事务一致性,已有 PG 技术栈。
- 考虑 Qdrant:追求单机高性能、部署简单,但不需要关系型事务。
- 考虑 Milvus:数据量达到千万或亿级,对 QPS 和延迟有极端要求,需要成熟的分布式分片。
下一篇我们会深入 pgvector 的索引内部,对比 HNSW 与 IVFFLAT 两大算法的原理,并实战调优 ef_search 等核心参数,把延迟和召回率调到最优。
更多推荐




所有评论(0)