pgvector 深度拆解:当关系数据库学会「向量检索」——HNSW/IVFFlat 索引原理、PostgreSQL 18 AIO 加持与 RAG 工程实战
2026 年,几乎每个后端团队都在问同一个问题:"我的 RAG / 语义搜索 / 推荐系统,到底要不要再引入一个独立的向量数据库?" 网上充斥着《PostgreSQL 18 原生向量搜索真香》这样的标题,但真相是:PostgreSQL 18 的正式版并没有把向量计算"下沉到内核"。所谓"原生向量搜索",到今天依然靠
pgvector这个扩展撑着。本文不蹭热点、不制造焦虑,从工程视角把 pgvector 的底层原理、PostgreSQL 18 到底帮了什么忙、以及一条能上生产的 RAG 链路,彻底讲透。读完你会对"该不该上专用向量库"有自己的判断,而不是被标题党带着跑。
一、背景:为什么向量检索成了数据库的"必修课"
过去十年,数据库的战场是"结构化数据":行、列、索引、事务、JOIN。但大模型时代改变了游戏规则。
当你把一段文本、一张图片、一段语音丢进 Embedding 模型(OpenAI text-embedding、BGE、E5、bge-m3……),它会输出一个 高维浮点向量(常见 384 / 768 / 1536 / 3072 维)。语义相近的内容,在向量空间里距离更近。于是"搜索"这件事从 WHERE title LIKE '%关键词%' 变成了"找出和这个向量最近的 K 个向量"——最近邻搜索(Nearest Neighbor Search)。
典型场景:
- RAG(检索增强生成):先把知识库切片 Embedding 入库,用户提问时检索最相关片段,拼进 Prompt 喂给 LLM。
- 语义搜索:用户搜"怎么给猫驱虫",能召回"幼猫体内寄生虫防治方案",而不是死磕关键词匹配。
- 推荐 / 去重 / 以图搜图:商品、文章、人脸,本质都是向量相似度问题。
2023 年之前,这类需求的标准答案是独立的向量数据库:Milvus、Qdrant、Weaviate、Pinecone。但引入它们意味着:多一套存储、多一套运维、多一个数据同步链路(业务数据改了,向量什么时候同步?最终一致还是强一致?),以及"我的用户表和向量表怎么 JOIN"的灵魂拷问。
pgvector 的出现,让"用我已经在用的 PostgreSQL 顺手把向量也存了"变成现实。它不是一个独立系统,而是 PostgreSQL 的一个扩展(extension)——和 postgis、pg_stat_statements 一个性质。这一点必须先讲清楚,因为它决定了后面所有的架构判断。
关键澄清:很多文章说"PostgreSQL 18 把向量搜索做进内核了",这是不准确的。截至 PG18,向量类型与索引依然由
pgvector扩展提供,核心代码里没有原生的vector类型。PG18 真正给向量检索"助攻"的,是底层 I/O 与查询优化器的一系列改进(见第四节)。把"扩展能力"说成"内核原生",是典型的标题党。
把视角拉回 2026 年:'关系数据库 + 向量'的融合早已不是 pgvector 一个人在战斗。Oracle、SQL Server、Redis(Query Engine 的向量索引)、ClickHouse、Elasticsearch 8 的 dense_vector 都在抢同一块蛋糕——行业的共识是:向量检索正在从"独立中间件"降级为"数据库的又一个一等公民能力"。pgvector 的特别之处,在于它证明了"用扩展而非改内核"这条路在 PostgreSQL 上完全走得通:既享受社区的高速迭代,又不破坏核心的稳定性契约。这恰恰解释了为什么它成了中小团队落地 AI 检索的默认入口。
二、核心概念:向量、距离与"近似"的必要性
2.1 向量与距离度量
pgvector 的核心是 vector 数据类型。它存的就是一组浮点数:
-- 安装扩展(每个数据库只需一次)
CREATE EXTENSION IF NOT EXISTS vector;
-- 建一张带向量列的表,1536 维对应 OpenAI text-embedding-3-small
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding vector(1536),
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT now()
);
pgvector 提供三类距离算子,选哪个取决于你的 Embedding 模型怎么训练的:
| 算子 | 距离 | 含义 | 适用场景 |
|---|---|---|---|
<-> | L2(欧氏距离) | 向量空间中的直线距离 | 通用,未归一化向量常用 |
<=> | 余弦距离 | 1 - 余弦相似度,衡量方向而非长度 | 最常用,语义相似度 |
<#> | 内积(负) | 向量点积取负(pgvector 用负值便于 ORDER BY ... ASC) | 已归一化且需考虑模长时 |
工程经验:绝大多数语义检索用余弦距离。如果你的 Embedding 已经归一化(openai 的默认输出就是单位向量),
1 - (a <=> b)就是余弦相似度。千万别混用——用 L2 去比较本该用余弦的向量,效果会莫名其妙地差。
除了算子,pgvector 也提供函数式写法(l2_distance(a,b)、cosine_distance(a,b)、inner_product(a,b)),在复杂 SQL 里可读性更好。
2.2 精确 vs 近似:为什么不能全表扫
最朴素的检索就是暴力计算:拿查询向量和库里每一个向量算距离,排序取 Top-K。复杂度 O(N × D),N 是向量条数,D 是维度。
- 10 万条 1536 维向量,每次查询要算 1.5 亿次乘加。还能忍。
- 1000 万条?每次查询 1500 亿次浮点运算,延迟直接崩到几百毫秒甚至秒级。
真实业务里向量规模轻易到千万、亿级。暴力搜索不可行。近似最近邻(ANN, Approximate Nearest Neighbor) 因此成为标配:牺牲一点点召回率(能不能找回全部真正最近的 K 个),换来数量级的延迟下降。pgvector 提供两种 ANN 索引:IVFFlat 和 HNSW。理解它们的差异,是你能否把向量检索调快的决定性因素。
三、架构分析:pgvector 的两种索引到底怎么干活
3.1 IVFFlat:倒排文件,快建但"看运气"
IVFFlat(Inverted File Flat)的思想像图书馆分类上架:
- 训练阶段:用聚类算法(类似 k-means)把所有向量分成
lists个"书架"(聚类中心)。 - 入库阶段:每条向量挂到离它最近的那个聚类中心下面。
- 查询阶段:算出查询向量最近的
probes个聚类中心,只在这几个"书架"里做精确比对。
-- 先有数据再建索引(IVFFlat 需要训练,空表建不了)
CREATE INDEX ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- 查询时扩大探查的聚类数,召回率↑但延迟↑
SET ivfflat.probes = 10;
优点:构建快、占内存小、实现简单。
缺点:召回率受 probes 影响大;如果查询向量恰好落在聚类边界,"最相似的那个"可能在你没探查的书架上,直接漏掉。数据分布不均匀时(实际业务几乎一定不均匀),效果很看脸。
经验法则:lists 取 sqrt(行数) 左右;probes 先取到 lists 的 5%~10%,再按召回率调。
3.2 HNSW:分层导航小世界,慢建但又快又准
HNSW(Hierarchical Navigable Small World,分层可导航小世界)是今天的事实标准。它构建一张多层图:
- 顶层:节点极稀疏,像城市主干道地图,快速把搜索定位到大区域。
- 中层:节点变密,像街道地图,缩小范围。
- 底层:包含所有节点,像详细地图,精确定位到目标向量。
每个节点连向它最相似的 m 个邻居。搜索时从顶层出发,沿着边"跳"到更近的邻居,逐层下钻,几步就逼近目标——这就是"小世界":任意两点之间只需很少的跳跃就能连通(六度空间理论的图论版)。
-- m=16 每个节点连 16 个邻居;ef_construction=64 建图时每层考察的候选数
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
关键参数:
m:每层每个节点的邻居数。越大召回率越高、图越密、内存越大、构建越慢。默认 16,质量敏感场景可到 32~64。ef_construction:建索引时每层从候选里挑邻居的视野宽度。越大索引质量越高、构建越慢。默认 64,追求质量可 128~200。hnsw.ef_search(GUC,查询期生效,默认 40):查询时考察的候选宽度。这是你线上调召回率最直接的旋钮——不用重建索引。
-- 线上想提高召回率,直接调 GUC,零成本
SET hnsw.ef_search = 200;
SELECT id, content, 1 - (embedding <=> %s) AS similarity
FROM documents
ORDER BY embedding <=> %s
LIMIT 5;
HNSW 的最大优势:空表就能建索引,数据动态增删不影响结构,查询延迟稳定在毫秒级,召回率可以做到很高。代价是构建慢、占内存(整张图要能放内存里才快)、磁盘占用大。
HNSW 到底是怎么把图建出来的? 插入一个新向量时,算法先从顶层入口找到离它最近的若干邻居,逐层下钻到最底层,记录每层走过的路径;然后以概率 1/m 决定这个节点是否"晋升"到上一层(越往上节点越稀疏,这正是一个指数衰减的层高分布,保证顶层是稀疏的"主干道")。在它被分配到的每一层里,把新节点连向那一层最近的 m 个邻居,同时可能反向让那些邻居也连回它(受每层最大连接数约束)。查询时同理:从顶层入口出发,贪心地向更近的邻居移动,到达局部最优后下钻到下一层,直到底层输出候选,最后用 ef_search 控制最终候选宽度做精排。理解了"晋升概率 1/m"和"每层次大 m 个邻居",你就明白为什么 m 调大图更密、召回更高——本质是给每个节点铺了一张更大的"社交关系网",搜索时能走的近道更多。
3.3 两者怎么选(决策表)
| 维度 | IVFFlat | HNSW |
|---|---|---|
| 构建速度 | 快 | 慢(可能数小时/千万级) |
| 内存占用 | 低 | 高(图常驻内存) |
| 查询延迟 | 中 | 低且稳定 |
| 召回率 | 中(依赖 probes) | 高(可微调 ef_search) |
| 能否空表建 | 否(需先有数据训练) | 是 |
| 动态写入 | 重建代价小 | 友好 |
| 适用 | 静态归档、低频更新、省资源 | 实时检索、RAG、推荐 |
一句话结论:新项目无脑上 HNSW;只有数据极静态、资源极紧张、且能接受偶尔漏召回时,才考虑 IVFFlat。
3.4 pgvector 的数据类型全家桶
vector 只是基础。pgvector 还提供:
halfvec:半精度浮点向量,最多 4000 维,内存直接砍半,精度损失在检索场景通常可忽略——省内存神器。sparsevec:稀疏向量,适合 TF-IDF、BM25 这类"大多数维度为 0"的场景(混合检索里很有用)。bit:二进制向量,做海明距离,适合某些哈希类特征。
-- 用半精度把 1536 维的存储和内存都压下来
ALTER TABLE documents ALTER COLUMN embedding TYPE halfvec(1536);
3.5 执行计划:PostgreSQL 到底走不走 HNSW 索引
很多新手会困惑:"我建了 HNSW 索引,为什么 EXPLAIN 里还是 Seq Scan?"答案是:索引用不用,由优化器按 cost 决定。当表里只有几百条向量时,全表扫一遍算距离可能比"走索引 + 回表"还便宜,优化器就会放弃索引。只有数据量上来(通常几万条以上),HNSW 的 cost 优势才压过顺序扫描。
-- 看 PG 到底怎么检索的
EXPLAIN ANALYZE
SELECT id, content, 1 - (embedding <=> %s) AS similarity
FROM kb_chunks
ORDER BY embedding <=> %s
LIMIT 5;
-- 大表应看到:Index Scan using kb_chunks_embedding_idx ... (HNSW)
-- 小表可能是:Seq Scan ... 然后 Sort,属正常现象
-- 调试时可强制走索引,验证索引本身没问题
SET enable_seqscan = off;
注意 hnsw.ef_search 是执行期旋钮,不影响优化器的索引选择,只影响索引扫描时的候选视野宽度。ivfflat.probes 同理。理解这一点,你就不会在"索引明明建了却没生效"上浪费时间——要么数据量还不够,要么 cost 估算被统计信息误导(跑一次 ANALYZE kb_chunks 通常就好)。
四、PostgreSQL 18 到底给向量检索帮了什么忙
回到开头的澄清:PG18 没有原生向量类型。但 PG18 确实有几个底层改进,对"向量表越来越大"的检索场景是实打实的利好。
4.1 异步 I/O(AIO)子系统
PG18 引入了全新的异步 I/O 框架,Linux 上基于 io_uring。过去顺序扫描、位图堆扫描、VACUUM 的读是同步阻塞的——CPU 干等磁盘。现在可以一次性并发发起多个读请求,CPU 边处理边等数据回来。
为什么和向量检索有关?当你用 WHERE metadata->>'lang'='zh' 做过滤、再 ORDER BY embedding <=> q 时,PG 往往要先扫一批行、做过滤、再算距离。扫行阶段的随机/顺序读被 AIO 加速,整体延迟下降。官方基准在云存储等 I/O 受限场景下,读密集查询有 2~3 倍提升。
-- PG18 配置异步 I/O 方式,Linux 推荐 io_uring
ALTER SYSTEM SET io_method = 'io_uring';
-- 或 'worker'(通用,跨平台)、'sync'(回退)
4.2 Skip Scan:多列索引不再被"最左前缀"卡死
过去一个 (lang, created_at) 的多列 B-tree 索引,如果你的查询只过滤 created_at 不带 lang,索引基本废了。PG18 的 Skip Scan 让"跳过前导列"也能高效利用索引。对你的向量表做结构化过滤(先按业务字段缩小范围,再做向量检索)是大杀器——它让"混合检索"的前置过滤快很多。
4.3 RETURNING 增强(OLD / NEW)
PG18 让 INSERT/UPDATE/DELETE/MERGE 的 RETURNING 能同时拿到修改前(OLD)和修改后(NEW)的行。在向量场景里,更新一条文档的 embedding 后能直接返回新旧向量,做缓存失效、审计、ETL 都省一次往返。
4.4 虚拟生成列、UUIDv7、OAuth2 认证、EXPLAIN 升级
虚拟生成列可以自动派生 metadata 里的某个字段做索引;UUIDv7 带时间戳前缀,做主键插入更友好(避免 UUIDv4 随机主键的索引膨胀);OAuth2 认证让托管 PG 的接入更安全;EXPLAIN 的升级让你能更清楚地看到"这次查询到底走没走 HNSW 索引"。
这些都和向量检索"间接相关":向量表也是业务表,业务表的那些老问题(主键、过滤、审计、认证)一个都不少。用 pgvector 的最大价值,正是向量和这些业务属性在同一张表里、同一套事务、同一个 SQL 里统一解决。
五、代码实战:从零搭一条生产级 RAG 链路
光讲原理不够,下面用 Python + pgvector 把"知识库 → 检索 → 生成"完整跑通。
5.1 起一个带 pgvector 的 PostgreSQL
# 官方镜像已带 pgvector 扩展(14+ 的各类发行版)
docker run -d --name pg-vector \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=rag \
-p 5432:5432 \
pgvector/pgvector:pg18
pip install openai psycopg2-binary pgvector
5.2 建表与索引
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE kb_chunks (
id BIGSERIAL PRIMARY KEY,
doc_id BIGINT NOT NULL,
chunk_idx INT NOT NULL,
content TEXT NOT NULL,
embedding vector(1536),
lang TEXT DEFAULT 'zh',
created_at TIMESTAMPTZ DEFAULT now()
);
-- HNSW 余弦索引;metadata 过滤靠单独的 B-tree/GIN 索引加速
CREATE INDEX ON kb_chunks USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
CREATE INDEX ON kb_chunks (lang);
CREATE INDEX ON kb_chunks (doc_id);
5.3 Embedding 与写入
import openai
from pgvector.psycopg2 import register_vector
import psycopg2
conn = psycopg2.connect("postgres://postgres:secret@localhost:5432/rag")
register_vector(conn) # 让 psycopg2 认识 vector 类型
cur = conn.cursor()
client = openai.OpenAI()
def embed(text: str) -> list[float]:
r = client.embeddings.create(
model="text-embedding-3-small", # 1536 维
input=text,
)
return r.data[0].embedding
# 假设已经把一篇长文切成若干 chunk
chunks = [
"PostgreSQL 18 引入了异步 I/O 子系统,基于 io_uring……",
"pgvector 提供 HNSW 与 IVFFlat 两类近似最近邻索引……",
# ... 更多 chunk
]
for i, text in enumerate(chunks):
cur.execute(
"INSERT INTO kb_chunks (doc_id, chunk_idx, content, embedding, lang) "
"VALUES (%s, %s, %s, %s, %s)",
(1, i, text, embed(text), "zh"),
)
conn.commit()
5.4 相似度检索(带结构化过滤)
query = "PostgreSQL 18 对向量检索有什么性能提升?"
q_vec = embed(query)
# 只在中文库里检索,余弦相似度降序取前 5
cur.execute(
"""
SELECT id, content,
1 - (embedding <=> %s) AS similarity
FROM kb_chunks
WHERE lang = 'zh'
ORDER BY embedding <=> %s
LIMIT 5
""",
(q_vec, q_vec),
)
for row in cur.fetchall():
print(f"[{row[2]:.3f}] {row[1][:60]}")
过滤检索的坑(必看):pgvector 的 HNSW 是"后过滤"——它先在向量索引里找候选,再应用
WHERE lang='zh'。如果中文只占 1%,你可能凑不够 5 条。解法有二:(1) 提高hnsw.ef_search让候选池更大;(2) 用子查询先按结构化条件缩表再算向量(数据量不大时更直接):SELECT id, content, 1 - (embedding <=> %s) AS similarity FROM (SELECT * FROM kb_chunks WHERE lang = 'zh') t ORDER BY embedding <=> %s LIMIT 5;
5.5 拼成完整 RAG
def retrieve(query: str, k: int = 5) -> list[str]:
q_vec = embed(query)
cur.execute(
"SELECT content FROM kb_chunks ORDER BY embedding <=> %s LIMIT %s",
(q_vec, k),
)
return [r[0] for r in cur.fetchall()]
def generate(query: str) -> str:
context = "\n\n".join(retrieve(query))
prompt = f"基于以下资料回答问题,不要编造:\n\n{context}\n\n问题:{query}"
r = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
)
return r.choices[0].message.content
print(generate("PostgreSQL 18 对向量检索有什么性能提升?"))
到这里,一条"切片 → Embedding → pgvector 存储 → 余弦检索 → 拼 Prompt → LLM 生成"的 RAG 链路就闭环了。全程没有引入任何独立向量数据库,向量和业务数据在同一个事务里保持一致。
六、性能优化:让它真的快、真的省
6.1 索引选型决策树
数据是否频繁动态增删? ──是──> HNSW
│否
是否极度省内存 / 低频查询? ──是──> IVFFlat
│否
└──> HNSW(默认无脑选)
6.2 HNSW 调参清单
| 目标 | 调什么 | 怎么调 |
|---|---|---|
| 提高召回率(不改索引) | hnsw.ef_search | 40 → 100 → 200,观测延迟 |
| 提高索引质量(可接受慢建) | ef_construction | 64 → 128 → 200 |
| 提高图密度/质量 | m | 16 → 32,内存随之翻倍 |
| 加速建索引 | max_parallel_maintenance_workers | 设大,HNSW 支持并行建 |
| 控制内存 | 改用 halfvec | 1536 维内存直接减半 |
-- 加速 HNSW 并行构建
SET max_parallel_maintenance_workers = 8;
CREATE INDEX CONCURRENTLY ON kb_chunks
USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 128);
小技巧:
CREATE INDEX CONCURRENTLY不会锁表,生产环境加向量索引务必加这个,否则大表建索引会把写入全堵死。
6.3 维度别贪高:Matryoshka 截断
pgvector 对建索引的维度有上限(历史上约 2000 维)。而 OpenAI 的 text-embedding-3 系列支持 Matryoshka 表示——可以在不重训的情况下把 1536 维截断到 512、256 维,且检索质量损失极小。维度砍半,内存、索引、查询延迟全降。
# 截断到前 512 维,既满足索引限制又省资源
vec = embed(text)[:512]
6.4 召回率怎么量化
别拍脑袋调参,用带标签的查询集算召回率:
def recall_at_k(ground_truth: set, retrieved: list, k: int) -> float:
topk = set(retrieved[:k])
return len(topk & ground_truth) / len(ground_truth)
# 提升 ef_search 后重测,找到召回率/延迟的甜点
6.5 和专用向量数据库的选型对比
| 维度 | pgvector(PG 扩展) | Milvus / Qdrant / Pinecone |
|---|---|---|
| 数据一致性 | ACID,强一致 | 多为最终一致 |
| SQL / JOIN | 原生支持 | 需额外查询层 |
| 运维复杂度 | 复用现有 PG | 独立集群,较重 |
| 超大规模(十亿+) | 吃力(图进内存) | 擅长,分布式 |
| GPU 加速 | 无 | 有(Milvus/Qdrant) |
| 强过滤检索 | 需技巧(见 5.4) | 原生支持(metadata 索引) |
| 成本 | 低(无新增组件) | 中高 |
选型口诀:
- 向量量 < 千万级、要和业务数据强一致、想少运维 → pgvector 直接上。
- 向量量 > 亿级、要 GPU、要复杂 metadata 过滤、做独立检索服务 → 上专用向量库,业务库和向量库解耦。
- 中间地带:pgvector 先顶着,真到瓶颈再拆。拆的时候,DuckDB 之类做离线分析、pgvector 做在线检索的混合架构也很常见。
6.6 常见坑
- 维度超过索引上限:报错或建不了索引,用 Matryoshka 截断(6.3)。
- 索引膨胀 / 召回下降:
VACUUM不及时、频繁 UPDATE embedding 会让 HNSW 质量劣化,定期REINDEX CONCURRENTLY。 - 忘记
CONCURRENTLY:大表建索引锁全表,线上事故高发。 - 余弦/L2 混用:Embedding 没归一化却用余弦,效果差。先确认模型的归一化约定。
- 过滤后凑不够 K 条:见 5.4 的子查询解法或调大
ef_search。
6.7 生产部署 checklist
向量表也是业务大表,生产环境躲不开经典 PG 运维题:
- 连接池必上 PgBouncer / pgcat:PostgreSQL 是"一连接一进程",向量应用往往长连接 + 高并发,直接裸连会拖垮连接数。用事务级(transaction)池化;若用
PREPARE语句要注意事务池下跨事务不可用。 - 读扩展靠只读副本:流复制会把 HNSW 索引自动同步到副本。检索是读多写少的场景,把 Top-K 查询打到只读副本,主库只负责写入和
REINDEX。 - 分区降低单表规模:按
tenant_id或时间RANGE分区,每个子表控制在千万级内,REINDEX CONCURRENTLY和维护窗口都更轻。 - 让热点图待在内存:HNSW 图常驻内存才快。调大
shared_buffers,并用pg_prewarm把向量索引预热进缓存,避免冷启动查询毛刺。 - 监控两板斧:
pg_stat_statements抓慢的向量查询;EXPLAIN ANALYZE验证是否走了索引、扫了多少候选。 - 备份一致性天然:向量列和普通列在同一个库、同一份 WAL 里,
pg_dump/ 流复制天然一致,不用操心"业务库和向量库对不上"这种分布式系统的经典痛点。
6.8 顺手做个基准,别凭感觉
上线前用真实数据压一下,暴力 vs HNSW 的差距会让你安心:
import time, random
q = [random.random() for _ in range(1536)]
cur.execute("SET enable_indexscan = on;")
t0 = time.time()
cur.execute("SELECT id FROM kb_chunks ORDER BY embedding <=> %s LIMIT 10", (q,))
cur.fetchall()
print(f"HNSW 延迟: {(time.time()-t0)*1000:.1f} ms")
cur.execute("SET enable_indexscan = off; SET enable_seqscan = on;")
t0 = time.time()
cur.execute("SELECT id FROM kb_chunks ORDER BY embedding <=> %s LIMIT 10", (q,))
cur.fetchall()
print(f"暴力扫描延迟: {(time.time()-t0)*1000:.1f} ms")
# 百万级数据上,两者往往差出 100~1000 倍
七、总结与展望
pgvector 的价值,从来不是"它比专用向量库更快"——在十亿级、GPU、复杂过滤这些硬指标上它确实打不过 Milvus/Qdrant。它的价值在于**"少一个系统"**:向量和业务数据在同一个 PostgreSQL 里,同一套事务、同一个 SQL、同一份备份、同一套运维。对绝大多数团队(向量量千万级以内、要强一致、不想养另一个数据库),这就是最优解。
从 roadmap 看,pgvector 0.7 已经抬高了可索引维度上限,并引入了更聪明的量化与并行构建;社区也在讨论让 HNSW 支持"带谓词过滤的检索"(filtered / search-with-predicate),以根治 5.4 里提到的后过滤漏数据问题。一旦这些落地,pgvector 在"强过滤检索"这个最后的软肋上也会被补上。至于内核是否收编向量类型,短期看概率不高——扩展模式让 pgvector 能按自己的节奏发版,不必等 PostgreSQL 一年一度的主版本节奏。换言之,"用扩展做到接近原生的体验"这条路,pgvector 已经走通了。
同时请记住那个被标题党掩盖的事实:PostgreSQL 18 没有把向量做进内核,pgvector 依旧是扩展。但 PG18 的异步 I/O、Skip Scan、RETURNING 增强,确实让"向量表也是业务大表"这件事更顺了。理解这一点,你就不会被《原生向量搜索真香》带节奏,也不会在选型时盲目跟风。
未来值得关注:pgvector 社区在持续逼近"准原生"体验——更大的可索引维度、更好的过滤检索、更聪明的量化。而 PostgreSQL 内核是否终有一天把向量类型收编,取决于社区对"关系 + 向量"融合路线的投票。在那之前,pgvector 就是你在 PostgreSQL 里做 AI 检索最务实的一把刀。
工程建议清单(抄走即用):
- 新项目向量检索无脑
pgvector + HNSW(cosine)。- 建索引永远
CREATE INDEX CONCURRENTLY。- 线上调召回率先动
hnsw.ef_search,别急着重建索引。- 维度能截就截(Matryoshka),内存和延迟双赢。
- 结构化过滤 + 向量检索用子查询或调大
ef_search解决"后过滤漏数据"。- 真到亿级再考虑拆专用向量库,别提前过度设计。
本文所有 SQL / Python 示例均可在 PostgreSQL 18 + pgvector 0.7+ 环境直接运行。距离算子、参数名、GUC 以官方文档为准。