用 PostgreSQL + pgvector 搭建向量语义检索:一份可实操分享教程
> 原文:Building Vector Similarity Search in PostgreSQL with pgvector > 作者:Bala Priya C,Machine Learning Mastery,2026-05-18 > 原文链接:https://machinelearningmastery.com/building-vector-similarity-search-in-postgresql-with-pgvector/ > 本文来源:基于上次 /gsummary 缓存摘要与已保存的浏览器 DOM 全文重新生成。 > 缓存摘要:/home/lin/.hermes/projects/hermes-gsummary-workflow/runs/outputs/20260519-221821-Building-Vector-Similarity-Search-in-PostgreSQL-with-pgvector-2992498-415826240-summary.md > 全文缓存:/home/lin/Downloads/mll_pgvector_article.txt
改写后的分享主题
原始需求是“把文章总结为分享教程”。我将它改写成一个更适合团队分享和落地演示的问题:
如何在已有 PostgreSQL 体系内,用 pgvector 搭建一套最小可用、可验证、可扩展的向量语义检索闭环?
一句话结论
pgvector 让 PostgreSQL 可以直接存储 embedding,并用 SQL 距离操作符、HNSW/IVFFlat 索引完成语义相似检索;适合已经使用 PostgreSQL、希望降低新组件运维成本的中小规模语义搜索/RAG 场景。
你能从这篇教程得到什么
完成后你应该能讲清楚并动手验证:
- embedding 为什么要先选模型再建表。
vector(n)的维度为什么不能随便写。<->、<=>、<#>等距离操作符分别适合什么场景。- HNSW 和 IVFFlat 的取舍是什么。
- 为什么 operator class 不匹配会导致“SQL 正常但性能退化”。
- 如何用
EXPLAIN验证索引是否真的被使用。 - 如何把向量检索和普通 SQL 过滤条件组合起来。
适合场景与不适合场景
适合:
- 业务数据已经在 PostgreSQL 中。
- 向量规模处于从几千到百万级的验证/早期生产阶段。
- 需要把语义检索和
WHERE、JOIN、权限、租户、价格、状态等关系型条件一起使用。 - 团队希望先少引入一个独立向量数据库组件。
不适合直接套用:
- 已经是超大规模向量检索,例如千万级以上且延迟极敏感。
- 需要复杂分片、多副本、专门向量召回服务治理。
- 已经有成熟向量数据库平台和运维体系。
核心概念:什么是向量语义检索
关键词搜索依赖字面匹配。比如用户搜索:
something warm and breathable for high-altitude trekking如果商品描述里写的是“轻量中层保暖衣”“高海拔徒步装备”,传统关键词检索可能匹配不到。
向量语义检索的做法是:
1. 用 embedding 模型把商品描述、文档段落或 FAQ 转成一组浮点数。 2. 用同一个模型把用户查询也转成向量。 3. 在数据库中比较查询向量和已存向量的距离。 4. 距离越近,语义越相似。
最小流程是:
文本数据
→ embedding 模型
→ 向量写入 PostgreSQL
→ 查询文本转向量
→ SQL 按向量距离排序
→ 返回 Top-K 相似结果第 1 步:先选 embedding 模型,再设计表结构
pgvector 的一个硬约束是:向量维度写在列定义里。
例如:
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
category TEXT,
description TEXT,
price NUMERIC(8, 2),
embedding vector(1536)
);这里的 vector(1536) 必须和模型输出维度一致。
原文列出的模型选择包括:
- OpenAI
text-embedding-3-small/text-embedding-3-large:常见维度为 1536 / 3072。 - Cohere Embed v4:多语言、多模态,文本和图片可在共享向量空间中表示。
- EmbeddingGemma:Google 开源模型,308M 参数,768 维,支持 Matryoshka truncation 和 100+ 语言。
- BAAI/BGE-M3:开源、自托管,支持 1000+ 语言和较长文本序列。
- Sentence Transformers:轻量、本地可跑,适合开发和原型验证。
实践建议:
- Demo 阶段可以用小模型或手写向量理解流程。
- 生产前必须固定模型名称、版本、维度和文本切分策略。
- 换模型通常意味着全量重新 embedding 和重建索引,不要把它当成普通配置变更。
第 2 步:安装并启用 pgvector
pgvector 支持 PostgreSQL 13 及以上版本。
Debian / Ubuntu 示例:
sudo apt install postgresql-18-pgvector把 18 替换成你的 PostgreSQL 大版本。
源码安装示例:
cd /tmp
git clone --branch v0.8.2 https://github.com/pgvector/pgvector.git
cd pgvector
make
sudo make installmacOS:
brew install pgvector在目标数据库中启用扩展:
CREATE EXTENSION IF NOT EXISTS vector;验证:
SELECT extname, extversion
FROM pg_extension
WHERE extname = 'vector';预期:返回一行 vector 扩展记录。
第 3 步:创建一个 3 维 demo 表
真实系统通常是 768、1536、3072 维。为了分享时能看懂,我们先用 3 维向量模拟户外装备商品。
DROP TABLE IF EXISTS gear;
CREATE TABLE gear (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
category TEXT,
description TEXT,
embedding vector(3)
);写入样例数据:
INSERT INTO gear (name, category, description, embedding) VALUES
('Merrell Moab 3 GTX', 'Footwear', 'Waterproof hiking boot for all-day trail comfort', '[0.82, 0.15, 0.44]'),
('Salomon Speedcross 6', 'Footwear', 'Aggressive trail runner for muddy and technical terrain', '[0.79, 0.21, 0.38]'),
('Black Diamond Spot 400', 'Lighting', 'Rechargeable headlamp with 400 lumens and waterproofing', '[0.11, 0.88, 0.22]'),
('Petzl ACTIK CORE', 'Lighting', 'Lightweight headlamp for hiking and camping', '[0.09, 0.91, 0.19]'),
('Osprey Atmos AG 65', 'Backpacks', 'Anti-gravity backpack for multi-day backcountry trips', '[0.55, 0.30, 0.77]'),
('Gregory Baltoro 75', 'Backpacks', 'High-volume pack for extended wilderness expeditions', '[0.58, 0.28, 0.81]');这些手写向量只用于教学:
- Footwear 更靠近第一个维度。
- Lighting 更靠近第二个维度。
- Backpacks 更靠近第三个维度。
真实系统里,这些向量来自 embedding API 或本地模型。
第 4 步:执行第一条相似检索 SQL
假设用户查询“trail footwear for rough terrain”,应用层将它转成向量:
[0.80, 0.19, 0.40]用 L2 距离查询:
SELECT
name,
category,
description,
embedding <-> '[0.80, 0.19, 0.40]' AS distance
FROM gear
ORDER BY distance
LIMIT 3;预期结果形态:
name category distance
Salomon Speedcross 6 Footwear 0.0300
Merrell Moab 3 GTX Footwear 0.0600
Osprey Atmos AG 65 Backpacks 0.4599解读:
<->是 L2 / 欧几里得距离。- 距离越小,语义越接近。
- 两个 Footwear 商品排在最前,符合预期。
第 5 步:选择正确的距离度量
pgvector 常见操作符:
<->:L2 / 欧几里得距离。<=>:余弦距离。<#>:负内积,想得到实际 similarity 时要注意取反。<+>:L1 / 曼哈顿距离。<~>:Hamming distance,二进制向量。<%>:Jaccard distance,二进制向量。
对多数 LLM 文本 embedding,默认建议:
优先从余弦距离 <=> 开始。原因是文本 embedding 通常把语义编码在方向上,而不是向量长度上。
同一个查询换成余弦距离:
SELECT
name,
embedding <=> '[0.80, 0.19, 0.40]' AS cosine_distance
FROM gear
ORDER BY cosine_distance
LIMIT 3;验证标准:Top-K 的类别和人工直觉一致;如果业务有标注样本,应比较 Recall@K、MRR 或人工相关性评分。
第 6 步:为什么需要索引
没有索引时,PostgreSQL 会对每一行计算一次距离:
query vector vs row 1
query vector vs row 2
query vector vs row 3
...几千行问题不大;到百万行时,延迟会明显变差。
pgvector 主要提供两种近似最近邻索引:HNSW 和 IVFFlat。
第 7 步:HNSW 和 IVFFlat 怎么选
HNSW:默认优先考虑
HNSW 构建多层图结构,查询时从粗粒度层一路导航到相近节点。
优点:
- 召回率通常更好。
- 查询性能通常更好。
- 没有训练步骤,可以增量构建。
- 可以在空表上创建,再逐步写入数据。
代价:
- 构建更慢。
- 内存消耗更高。
创建 HNSW 索引:
CREATE INDEX gear_embedding_hnsw_cosine_idx
ON gear
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);参数说明:
m:图中每个节点最大连接数。ef_construction:构建索引时的候选列表大小。
查询时可以调:
SET hnsw.ef_search = 100;ef_search 越高,召回可能越好,但延迟也会上升。
IVFFlat:构建快、资源低,但要关注退化
IVFFlat 会在索引构建时把向量空间划分成若干簇,查询时只搜索最接近的簇。
优点:
- 构建较快。
- 内存占用较低。
代价:
- 通常召回不如 HNSW。
- 需要在已有代表性数据后再建索引。
- 后续新增数据如果分布变化,召回可能逐步下降。
创建 IVFFlat 索引:
CREATE INDEX gear_embedding_ivfflat_cosine_idx
ON gear
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);查询时可以调:
SET ivfflat.probes = 10;probes 越高,搜索的簇越多,召回更好但延迟更高。
第 8 步:operator class 必须和查询操作符匹配
这是本文最重要的生产坑。
如果查询用余弦距离:
ORDER BY embedding <=> '[0.80, 0.19, 0.40]'索引必须用:
USING hnsw (embedding vector_cosine_ops)对应关系:
<->对应vector_l2_ops<=>对应vector_cosine_ops<#>对应vector_ip_ops<+>对应vector_l1_ops<~>对应bit_hamming_ops<%>对应bit_jaccard_ops
如果不匹配,PostgreSQL 可能不会报错,而是退回顺序扫描。SQL 看起来没问题,线上性能却会出问题。
第 9 步:用 EXPLAIN 验证索引是否命中
不要只看“查询能不能跑”,要看执行计划。
EXPLAIN (ANALYZE, BUFFERS)
SELECT name, category
FROM gear
ORDER BY embedding <=> '[0.80, 0.19, 0.40]'
LIMIT 3;你需要确认:
- 是否出现对应索引扫描。
- 是否退化为
Seq Scan。 Buffers和实际耗时是否符合预期。
如果仍是顺序扫描,优先检查:
- 查询操作符和 operator class 是否匹配。
- 表数据量是否太小,优化器认为顺序扫描更便宜。
LIMIT、排序表达式是否写成了索引可用的形式。- HNSW/IVFFlat 索引是否真的存在。
第 10 步:和普通 SQL 过滤条件组合
pgvector 的工程优势在于它仍然是 PostgreSQL。
例如,只在 Footwear 分类中找相似商品:
SELECT
name,
category,
embedding <-> '[0.80, 0.19, 0.40]' AS distance
FROM gear
WHERE category = 'Footwear'
ORDER BY distance
LIMIT 2;预期结果:
name category distance
Salomon Speedcross 6 Footwear 0.0004
Merrell Moab 3 GTX Footwear 0.0016生产中可以继续加:
SELECT
p.name,
p.category,
p.price,
p.embedding <=> $1 AS distance
FROM products p
WHERE p.category = 'Footwear'
AND p.price BETWEEN 50 AND 200
AND p.status = 'active'
ORDER BY p.embedding <=> $1
LIMIT 10;这就是为什么 pgvector 对已有 PostgreSQL 团队很有吸引力:语义检索可以和租户、权限、价格、状态、时间范围、JOIN 等条件合在一起。
实操案例 1:最小 SQL 演示脚本
适合分享现场 5 分钟演示。
CREATE EXTENSION IF NOT EXISTS vector;
DROP TABLE IF EXISTS gear;
CREATE TABLE gear (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
category TEXT,
description TEXT,
embedding vector(3)
);
INSERT INTO gear (name, category, description, embedding) VALUES
('Merrell Moab 3 GTX', 'Footwear', 'Waterproof hiking boot for all-day trail comfort', '[0.82, 0.15, 0.44]'),
('Salomon Speedcross 6', 'Footwear', 'Aggressive trail runner for muddy and technical terrain', '[0.79, 0.21, 0.38]'),
('Black Diamond Spot 400', 'Lighting', 'Rechargeable headlamp with 400 lumens and waterproofing', '[0.11, 0.88, 0.22]'),
('Petzl ACTIK CORE', 'Lighting', 'Lightweight headlamp for hiking and camping', '[0.09, 0.91, 0.19]'),
('Osprey Atmos AG 65', 'Backpacks', 'Anti-gravity backpack for multi-day backcountry trips', '[0.55, 0.30, 0.77]'),
('Gregory Baltoro 75', 'Backpacks', 'High-volume pack for extended wilderness expeditions', '[0.58, 0.28, 0.81]');
SELECT
name,
category,
embedding <-> '[0.80, 0.19, 0.40]' AS distance
FROM gear
ORDER BY distance
LIMIT 3;
CREATE INDEX gear_embedding_hnsw_cosine_idx
ON gear
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
EXPLAIN (ANALYZE, BUFFERS)
SELECT name, category
FROM gear
ORDER BY embedding <=> '[0.80, 0.19, 0.40]'
LIMIT 3;验收标准:
- 表能创建。
- 数据能插入。
- Top-K 查询返回 Footwear 靠前的结果。
- HNSW 索引能创建。
EXPLAIN输出能用于判断是否命中索引。
失败处理:
type "vector" does not exist:没有执行CREATE EXTENSION vector;,或扩展没有安装。expected 3 dimensions, not ...:插入向量维度和vector(3)不一致。- 索引不命中:检查
<=>是否对应vector_cosine_ops,以及表数据是否太少导致优化器选择顺序扫描。
实操案例 2:应用层写入 embedding 的结构
真实系统不要手写向量,而是在应用层调用 embedding 模型。
伪代码:
def create_product(conn, product: Product) -> None:
text = f"{product.name}\n{product.description}"
embedding = embedding_client.embed(text)
conn.execute(
"""
INSERT INTO products (name, category, description, price, embedding)
VALUES (%s, %s, %s, %s, %s)
""",
(product.name, product.category, product.description, product.price, embedding),
)
def search_products(conn, query: str, limit: int = 10) -> list[ProductSearchResult]:
query_embedding = embedding_client.embed(query)
return conn.execute(
"""
SELECT
name,
category,
description,
price,
embedding <=> %s AS distance
FROM products
WHERE status = 'active'
ORDER BY embedding <=> %s
LIMIT %s
""",
(query_embedding, query_embedding, limit),
).fetchall()验收标准:
- 写入前记录模型名称和版本。
embedding长度等于数据库vector(n)的n。- 查询和写入使用同一个模型。
- embedding API 失败时不要写入半成品数据。
失败处理:
- embedding 服务超时:应用层重试,超过阈值进入失败队列。
- 模型版本切换:新增列或新表灰度,不要直接覆盖旧 embedding。
- 维度不匹配:在应用层写入前先校验长度,避免数据库报错才发现。
实操案例 3:模型切换和回滚方案
不要直接把旧列覆盖掉。更安全的做法是并行保留新旧 embedding。
ALTER TABLE products
ADD COLUMN embedding_v2 vector(3072);迁移流程:
1. 新增 embedding_v2 列。 2. 后台批量生成 v2 embedding。 3. 为 embedding_v2 创建新索引。 4. 用相同查询集对比 v1/v2 的 Top-K 质量和延迟。 5. 灰度切流量。 6. 保留旧列一段时间,确认无问题后再清理。
验收标准:
- v2 覆盖率达到目标,例如 99.9%。
- v2 查询延迟不高于可接受阈值。
- v2 Top-K 质量不低于 v1。
- 有明确回滚开关。
失败处理:
- v2 召回变差:停止切流,继续使用 v1。
- 索引构建耗时过长:低峰构建,必要时分表或分批。
- 存储膨胀明显:评估 half-precision、归档旧 embedding 或拆分冷热数据。
生产检查清单
上线前至少检查:
- 模型:名称、版本、维度已记录。
- Schema:
vector(n)和模型维度一致。 - 写入:embedding 生成失败不会产生脏数据。
- 查询:距离操作符符合模型特性。
- 索引:operator class 和查询操作符一致。
- 执行计划:
EXPLAIN (ANALYZE, BUFFERS)已验证。 - 质量:有人工样本或评测集评估 Top-K 结果。
- 延迟:有 p50 / p95 / p99 压测数据。
- 内存:HNSW 索引内存消耗已评估。
- 演进:换模型、重建索引、回滚方案已准备。
常见坑
坑 1:先建表,后选模型
vector(1536) 不是随便写的。模型一换,维度可能就变了,后面要全量重算。
坑 2:操作符和索引类型不匹配
查询用 <=>,索引用 vector_l2_ops,可能不报错,但可能退化为顺序扫描。
坑 3:只看延迟,不看召回
HNSW 的 ef_search、IVFFlat 的 probes 都是在延迟和召回之间取舍。不能只看查询快不快。
坑 4:IVFFlat 建在空表或小样本上
IVFFlat 依赖构建时的数据分布。数据不代表真实分布,后续召回可能很差。
坑 5:把 pgvector 当成万能向量数据库
pgvector 的优势是简单、低运维成本、和关系数据结合自然。但超大规模向量检索仍可能需要专门向量数据库。
5 分钟分享讲稿
可以按这个顺序讲:
1. 传统关键词搜索为什么不够:自然语言意图和商品描述不一定同词。 2. embedding 把语义转成向量:相似文本在向量空间里距离更近。 3. pgvector 的价值:向量列、距离操作符、HNSW/IVFFlat 索引直接在 PostgreSQL 内完成。 4. 最小 SQL demo:建表、插入 3 维样例、Top-K 查询。 5. 生产坑:模型维度、距离度量、operator class、EXPLAIN 验证。
结尾总结
pgvector 不是“更高级的 PostgreSQL 插件”这么简单,它是把语义检索拉回到已有关系数据库体系的一种工程折中。真正要做好的不是安装扩展,而是管住四件事:模型维度、距离度量、索引匹配、执行计划验证。
如果团队已经有 PostgreSQL,而且只是要先落地语义搜索或 RAG 检索原型,pgvector 是一个简单够用、运维成本低的起点。