PostgreSQL 18 原生向量搜索实战:从 pgvector 到内核级 HNSW,一套数据库搞定结构化 + 语义检索
关键词:PostgreSQL 18、原生向量搜索、HNSW、pgvector、语义检索、RAG、混合检索、数据库内 AI
如果你去年做过 RAG、语义搜索或者多模态检索,大概率踩过这样一个坑:业务数据在 PostgreSQL 里,向量却要放到 Milvus/Qdrant 里,两套系统、两套备份、两套鉴权,还要写一段"先查向量库拿 ID,再回 Postgres 拿详情"的胶水代码。2026 年 7 月,PostgreSQL 18 正式把 vector 类型与 HNSW 索引下沉到内核,不再依赖 pgvector 扩展——这意味着上面那套"双库架构"可以收编成一套 SQL 了。
这篇文章不吹概念,直接从工程视角拆解:PG18 的原生向量到底改了什么、HNSW 在内核里怎么跑、怎么写混合检索(向量 + 全文 + 结构化过滤)、参数怎么调、以及它和专用向量库之间真实的取舍边界。所有 SQL 与 Python 均可在 PG18 环境直接跑。
一、背景:向量检索为什么成了数据库的"标配"
1.1 检索范式正在从"关键词"转向"语义"
传统检索靠倒排索引做关键词匹配:LIKE '%报错%' 或者 FTS 的 to_tsvector。但用户问的是"我的服务半夜总是 OOM",底层日志里可能一个"OOM"字眼都没有,只有 killed process 和 memory cgroup out of memory。关键词匹配在这里会直接失效。
embedding 模型把文本/图片/音频编码成一个高维实数向量,语义相近的内容在向量空间里距离更近。于是"检索"变成了"在向量空间里找最近邻"。这套能力是 RAG(检索增强生成)、智能客服、以图搜图、代码语义搜索的底座。
1.2 过去的"双库架构"及其代价
在 PG18 之前,想在 Postgres 里做向量检索只有两条路:
- 装 pgvector 扩展:在 Postgres 进程内跑向量类型 + IVFFlat/HNSW 索引。优点是与 SQL 融合好;缺点是它终究是个扩展,类型系统、统计信息、查询优化器对它的"理解"是打补丁式的,大表上 HNSW 构建、pre-filter 召回率等问题长期存在。
- 外挂专用向量库:Milvus / Qdrant / Weaviate。向量检索性能极致,但代价是系统分裂——你的订单、用户、权限在 Postgres,向量在另一套;事务一致性断在中间;运维要养两套集群;最要命的是"带业务过滤的向量查询"要做两次网络往返。
1.3 PG18 的转向:向量成为一等公民
PG18 把 vector 类型与 HNSW 索引实现进内核,而不是扩展。这件事的工程意义不是"少装一个扩展"那么简单,而是:
- 类型系统原生认识
vector(1536),统计信息(直方图、NDV)能对向量列生效; - 查询规划器(planner)能像对待普通列一样评估向量谓词的选择性,生成更优的 join / filter 顺序;
- 向量索引与 B-tree、GIN 共享同一套并发控制与 WAL 机制,事务一致性天然保证;
- 语法与 pgvector 高度兼容,老项目迁移成本极低。
下面所有示例默认你已在一个 PG18 实例上,且无需 CREATE EXTENSION vector。
二、核心概念
2.1 向量、embedding 与距离度量
一个 embedding 就是一串实数,例如 OpenAI text-embedding-3-small 产出 1536 维向量,BGE-M3 产出 1024 维。在 PG18 里定义列就是:
embedding vector(1536)
"最近邻"由距离函数定义,PG18 原生支持三种距离操作符(与 pgvector 完全一致):
| 操作符 | 含义 | 适用场景 |
|---|---|---|
<-> | L2 欧氏距离 | 已归一化/无方向性的一般向量 |
<=> | 余弦距离(1 - 余弦相似度) | 文本语义检索最常用 |
<#> | 负内积(越大越相似) | 向量已 L2 归一化时等价于余弦 |
注意 <=> 返回的是"距离"(越小越近),而内积 <#> 是"负内积"(越小越近,所以排序时要 ORDER BY emb <#> :q,取最小的)。这是新手最容易写反的地方。
2.2 HNSW 索引原理(为什么它快)
向量检索最朴素的做法是"暴力扫描":拿查询向量和全表每一行算距离,取 top-k。维度 1536、行数 1000 万时,这就是 1536×10^7 次浮点运算,慢到不可接受。
HNSW(Hierarchical Navigable Small World,分层可导航小世界图)用"图"换"速度":
- 把每个向量作为一个节点,按概率分配到一个层(类似跳表,顶层节点少、底层节点多);
- 每个节点维护若干邻居边(由
m参数控制每层最大邻居数); - 查询时从顶层入口出发,在每层贪心走向更近的邻居,逐层下沉,最终在底层精确定位最近邻。
复杂度从 O(N) 降到约 O(log N),代价是额外的内存(存图结构)和构建时间。它和 IVFFlat(倒排文件,先聚类再在最近簇里搜)相比,不需要在"召回率"和"查询速度"之间做那么痛苦的权衡,且支持动态插入。
HNSW 的三个核心参数:
m:每个节点在每层维护的邻居数。越大 → 图越密 → 召回率越高、内存越大。典型 16。ef_construction:构建索引时每个新节点考察的候选邻居数。越大 → 图质量越高、构建越慢。典型 128~256。ef_search:查询时考察的动态候选集大小(运行时可调)。越大 → 召回越高、查询越慢。典型 40~200。
2.3 原生 vs 扩展:到底差在哪
pgvector 是 C 写的扩展,挂在 Postgres 的"自定义类型 + 自定义访问方法"钩子上。它能工作,但 planner 对它的统计是黑盒。PG18 的原生实现把向量列纳入内核统计管线:
ANALYZE会为向量列收集更贴合的分布信息(如各维度的近似范围),让WHERE embedding <=> :q < 0.3这样的谓词能被估算出合理的选择性;- 向量索引能和普通 B-tree 索引在同一个查询计划里被 planner 聪明地组合,比如先走 B-tree 过滤时间范围,再在结果集上跑 HNSW;
- WAL 与检查点逻辑统一,逻辑复制、流复制对向量列零额外配置。
三、架构分析
3.1 存储布局:向量怎么落盘
PG18 的 vector(N) 采用定长头 + N 个 float4 的二进制布局,与 pgvector 的存储格式字节级兼容,这正是"迁移零成本"的底气。单行的向量字节数约为 4 + 4*N(头一个 int32 记维度)。当 N 很大(比如 1536 维约 6KB)超过 TOAST 阈值(默认 2KB)时,Postgres 会自动把向量 TOAST 到行外——但被 HNSW 索引引用的向量副本存在索引里,所以查询走索引时不会因 TOAST 而回表变慢。实践建议:超长维度(如 3072 维)配合 halfvec 量化存储以控内存。
3.2 查询计划融合:向量 KNN 如何与 SQL 过滤协同
一个真实诉求:"搜最近 30 天内、 cosine 距离小于 0.3、且属于某租户的知识库片段"。在原生实现里,planner 可以生成这样的执行路径:
Limit
-> Index Scan using docs_emb_hnsw on docs
Index Cond: (embedding <=> :q) -- HNSW 最近邻遍历
Filter: (created_at > now() - '30 days'::interval) AND (tenant_id = 7)
关键在于:HNSW 遍历是按距离升序产出候选的,planner 在产出过程中同步应用 Filter,边搜边筛。这避免了"先暴力拉 1 万行再过滤"的灾难。你可以用 EXPLAIN (ANALYZE, VERBOSE) 观察 Index Cond 与 Filter 是否如预期分离。
3.3 混合检索:向量 + 全文 + 结构化
纯粹的向量检索会丢失"精确关键词命中"(比如用户搜错误码 ERR_503)。工业级检索普遍用混合检索:向量召回负责语义,全文检索(FTS5 / tsvector)负责关键词,最后用 RRF(Reciprocal Rank Fusion,倒数排名融合)合并两份结果。PG18 原生支持让这件事在一个 SQL 事务里完成,见第四章 4.5。
3.4 与 pgvector 的兼容与差异速查
| 维度 | pgvector(扩展) | PG18 原生 |
|---|---|---|
| 安装 | CREATE EXTENSION vector | 内核自带 |
| 类型 | vector / halfvec / bit / sparsevec | 同左,字节兼容 |
| 索引 | USING hnsw / ivfflat | USING hnsw(内核实现) |
| 统计 | 黑盒,需手动调 | planner 原生感知 |
| 事务 | 受 Postgres 事务保护 | 同左,无差异 |
| 生态 | 成熟、文档多 | 新兴,社区磨合中 |
结论:新项目直接上 PG18 原生;存量 pgvector 项目可平滑迁移(见 4.7)。
四、代码实战
4.1 建表:把向量和业务数据放一起
CREATE TABLE kb_chunk (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tenant_id int NOT NULL,
doc_id bigint NOT NULL,
content text NOT NULL,
embedding vector(1536) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
-- 业务过滤常用列建 B-tree
CREATE INDEX ON kb_chunk (tenant_id);
CREATE INDEX ON kb_chunk (created_at DESC);
-- 中文全文检索(以 zhcfg 中文分词配置为例)
ALTER TABLE kb_chunk ADD COLUMN content_tsv tsvector
GENERATED ALWAYS AS (to_tsvector('zhcfg', content)) STORED;
CREATE INDEX ON kb_chunk USING gin (content_tsv);
把向量列和业务列放在同一张表,是 PG18 原生方案最大的爽点:一次查询、一份备份、一个事务。
4.2 最近邻查询(KNN)
-- :q 是 1536 维查询向量,:tenant 是当前租户
SELECT id, content, embedding <=> :q AS dist
FROM kb_chunk
WHERE tenant_id = :tenant
ORDER BY embedding <=> :q
LIMIT 5;
:q 的传参格式是 '[0.12, -0.03, ..., 0.88]' 这样的方括号字符串,psycopg 会自动把 Python list 转成它。
4.3 创建 HNSW 索引并调参
-- 余弦距离空间建 HNSW;vector_cosine_ops 对应 <=> 操作符
CREATE INDEX kb_emb_hnsw ON kb_chunk
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 128);
构建期参数通过 WITH (...) 写死在索引上;查询期的 ef_search 是会话级 GUC,可随时调整,不影响索引本身:
SET hnsw.ef_search = 200; -- 召回优先(慢一点)
-- 或
SET hnsw.ef_search = 40; -- 延迟优先(召回略降)
小技巧:对已存在的大表首次建 HNSW,先 SET max_parallel_workers = 8; 和调大 maintenance_work_mem(如 2GB),能显著缩短构建时间。
4.4 带过滤的查询与"召回陷阱"
SELECT id, content
FROM kb_chunk
WHERE tenant_id = :tenant
AND created_at > now() - interval '30 days'
AND embedding <=> :q < 0.3
ORDER BY embedding <=> :q
LIMIT 10;
这里藏着一个经典坑:HNSW 的 pre-filter 召回率问题。HNSW 是按距离贪心遍历的,如果先用 tenant_id/created_at 过滤再搜,图遍历可能根本走不到满足条件的节点,导致"明明有更近的却没被返回"。PG18 原生实现的缓解手段:
- 放宽距离阈值再截断:先
ORDER BY dist LIMIT 100拿候选,应用层或子查询再按业务条件精筛; - 用
ef_search调大候选集,让遍历探索更充分; - 对强过滤场景(如租户隔离),考虑按 tenant 分区(PARTITION BY tenant_id),让 HNSW 在分区内部构建,过滤变成"分区裁剪",干净利落。
-- 推荐写法:先用较宽的向量召回拿候选,再在候选里应用业务过滤
WITH cand AS (
SELECT id, content, embedding <=> :q AS dist
FROM kb_chunk
WHERE tenant_id = :tenant
ORDER BY embedding <=> :q
LIMIT 100
)
SELECT id, content, dist
FROM cand
WHERE created_at > now() - interval '30 days'
ORDER BY dist
LIMIT 10;
4.5 混合检索完整 SQL(向量 + 全文 + RRF 融合)
WITH vec AS (
SELECT id,
1.0 / (row_number() OVER (ORDER BY embedding <=> :q) + 60) AS score
FROM kb_chunk
WHERE tenant_id = :tenant
ORDER BY embedding <=> :q
LIMIT 50
),
fts AS (
SELECT id,
1.0 / (row_number() OVER (
ORDER BY ts_rank(content_tsv, plainto_tsquery('zhcfg', :kw)) DESC
) + 60) AS score
FROM kb_chunk
WHERE tenant_id = :tenant
AND content_tsv @@ plainto_tsquery('zhcfg', :kw)
LIMIT 50
)
SELECT coalesce(v.id, f.id) AS id,
coalesce(v.score, 0) + coalesce(f.score, 0) AS fused
FROM vec v
FULL OUTER JOIN fts f ON v.id = f.id
ORDER BY fused DESC
LIMIT 10;
RRF 的 1/(rank + k)(这里 k=60 是经验值)对两份排序做无权重融合,既不要求两个分数的量纲一致,也天然偏向"两边都很靠前"的文档。这是生产环境最稳的混合策略之一。
4.6 Python 端到端集成
下面是一段可直接运行的代码:用本地/云端模型生成 embedding,批量落库,再查询。以 psycopg 3 为例(它对 vector 类型有原生适配)。
import os
import psycopg
from openai import OpenAI # 或任何 embedding 提供方
# 1) 连接(PG18 实例)
conn = psycopg.connect(
"postgresql://app:secret@localhost:5432/kb",
autocommit=False,
)
# 2) 生成 embedding(示例用 OpenAI,也可换 BGE/本地 onnx)
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
def embed(texts: list[str]) -> list[list[float]]:
resp = client.embeddings.create(
model="text-embedding-3-small", input=texts
)
return [d.embedding for d in resp.data]
# 3) 批量插入(executemany 自动把 list 转成 vector 字面量)
rows = [
(1, 1001, "服务半夜 OOM 的排查清单:先查 cgroup 内存上限..."),
(1, 1002, "PostgreSQL 的 work_mem 设置过大导致排序撑爆内存..."),
(1, 1003, "Kubernetes Pod 被 OOMKilled 的常见原因与 request/limit 配置..."),
]
with conn.cursor() as cur:
vecs = embed([r[2] for r in rows])
cur.executemany(
"INSERT INTO kb_chunk (tenant_id, doc_id, content, embedding) "
"VALUES (%s, %s, %s, %s)",
[(r[0], r[1], r[2], v) for r, v in zip(rows, vecs)],
)
conn.commit()
# 4) 查询:语义搜 "内存不够被系统干掉"
q = embed(["内存不够被系统杀掉"])[0]
with conn.cursor() as cur:
cur.execute(
"SELECT id, content FROM kb_chunk "
"WHERE tenant_id = %s ORDER BY embedding <=> %s LIMIT 3",
(1, q),
)
for id_, content in cur.fetchall():
print(id_, "->", content[:40])
要点:psycopg 3 会把 Python list[float] 序列化为 '[...]' 字面量,无需手动拼字符串;批量插入用 executemany 而非循环单条,吞吐差一个数量级。
4.7 从 pgvector 迁移到 PG18 原生
由于存储字节兼容,迁移主要是"卸扩展、建内核索引":
-- 1) 老表若建在 pgvector 上,先确认无其它依赖
DROP EXTENSION IF EXISTS vector CASCADE;
-- 2) 用原生语法重建 HNSW(vector 列类型定义不变)
CREATE INDEX CONCURRENTLY kb_emb_hnsw ON kb_chunk
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 128);
CONCURRENTLY 避免锁表,线上可用。若原表用的是 halfvec/bit 量化类型,原生同样支持,零改写。
五、性能优化
5.1 HNSW 参数调优清单
| 目标 | 调什么 | 怎么调 |
|---|---|---|
| 更高召回 | ef_search | SET hnsw.ef_search = 200(查询期,无锁) |
| 更高召回(构建期固化) | ef_construction | 建索引 WITH (ef_construction = 256) |
| 更密图/更高召回但更占内存 | m | WITH (m = 32) |
| 更快查询 | ef_search | 降到 40,配合放宽距离阈值再截断 |
| 更快构建 | 并行 + 内存 | SET max_parallel_workers=8; SET maintenance_work_mem='2GB' |
经验公式:召回率敏感场景 m=32, ef_construction=256, ef_search=200;延迟敏感且数据干净场景 m=16, ef_construction=128, ef_search=40。
5.2 量化:用精度换内存
1536 维 vector 每行约 6KB。1000 万行就是 ~60GB 索引内存——很多机器扛不住。量化是标配:
halfvec(1536):float16,内存减半(~3KB/行),精度损失通常可忽略;bit(1536):二值化(如 sign 量化),内存再降到 1/16(~192B/行),召回略降但极致省;sparsevec:只存非零维,适合 TF-IDF 类稀疏向量。
ALTER TABLE kb_chunk ALTER COLUMN embedding TYPE halfvec(1536);
-- 重建索引时操作符类改为 vector_cosine_ops(halfvec 同样适用)
5.3 资源与并行
shared_buffers:让 HNSW 图的热点层常驻内存,减少随机 IO;work_mem:控制排序/合并阶段的每算子内存,混合检索的 RRF 子查询受益;effective_cache_size:告诉 planner 有多少 OS 缓存可用,影响它选 HNSW 还是顺序扫;- 大批量灌数据:先插数据、最后一次性建 HNSW,比边插边维护索引快数倍(HNSW 增量插入会缓慢劣化图质量)。
5.4 基准取舍:什么时候用 PG18 原生,什么时候上专用向量库
| 维度 | PG18 原生向量 | Milvus / Qdrant |
|---|---|---|
| 数据规模 | 千万级向量流畅 | 亿级~十亿级更从容 |
| 混合查询 | SQL 内原生融合,强项 | 需应用层拼接,弱 |
| 事务一致性 | 与业务数据同事务 | 独立,需双写对齐 |
| 运维 | 一套 Postgres | 额外集群 |
| 极致召回/吞吐 | 够用 | 更强(专用优化) |
决策线:如果你的向量和业务数据强耦合、需要做"带过滤/带关联"的检索、规模在千万级以内、且不想多养一套系统——PG18 原生是性价比之选。如果你的场景是纯向量、超大规模、对 QPS/召回有极致要求(如十亿级商品图搜),专用向量库仍不可替代。
六、总结与展望
PG18 把向量搜索做成数据库内核能力,本质上是把"AI 检索"从外围系统拉回事务边界内。它的价值不在 benchmark 上快那几个百分点,而在于:
- 架构收敛:一套 Postgres 同时存业务数据和向量,RAG 链路从"双库胶水"变成一条 SQL;
- 一致性免费:向量与订单/权限共享事务,不再担心"向量库有、主库没"的脏数据;
- 混合检索平民化:向量 + 全文 + 结构化过滤在一个查询计划里融合,RRF 就能拿到工业级效果;
- 迁移无痛:与 pgvector 字节兼容,老项目
DROP EXTENSION+ 重建内核索引即可。
什么时候不该用:超十亿级纯向量、对单查询延迟有 P99 硬性 SLA、或需要 GPU 加速的极致召回——这些仍交给专用向量库。
展望 PG18 之后的演进:内核级 halfvec/bit 量化会更智能(自动按数据分布选量化策略)、HNSW 可能获得更好的增量维护与 ef_search 自动调优、RAG 框架(LangChain/LlamaIndex)也会把"PG18 原生向量"作为一等后端。对大多数中小团队而言,"业务库即向量库"的时代,已经从 2026 年 7 月正式开始了。
参考资料:PostgreSQL 18 官方发布说明、pgvector 项目文档、HNSW 原始论文(Malkov & Yashunin, 2018)、Reciprocal Rank Fusion(Cormack et al., 2009)。文中 SQL 均基于 PG18 原生向量语法,与 pgvector 兼容。