用 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 场景。

你能从这篇教程得到什么

完成后你应该能讲清楚并动手验证:

适合场景与不适合场景

适合:

不适合直接套用:

核心概念:什么是向量语义检索

关键词搜索依赖字面匹配。比如用户搜索:

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) 必须和模型输出维度一致。

原文列出的模型选择包括:

实践建议:

第 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 install

macOS:

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]');

这些手写向量只用于教学:

真实系统里,这些向量来自 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

解读:

第 5 步:选择正确的距离度量

pgvector 常见操作符:

对多数 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);

参数说明:

查询时可以调:

SET hnsw.ef_search = 100;

ef_search 越高,召回可能越好,但延迟也会上升。

IVFFlat:构建快、资源低,但要关注退化

IVFFlat 会在索引构建时把向量空间划分成若干簇,查询时只搜索最接近的簇。

优点:

代价:

创建 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)

对应关系:

如果不匹配,PostgreSQL 可能不会报错,而是退回顺序扫描。SQL 看起来没问题,线上性能却会出问题。

第 9 步:用 EXPLAIN 验证索引是否命中

不要只看“查询能不能跑”,要看执行计划。

EXPLAIN (ANALYZE, BUFFERS)
SELECT name, category
FROM gear
ORDER BY embedding <=> '[0.80, 0.19, 0.40]'
LIMIT 3;

你需要确认:

如果仍是顺序扫描,优先检查:

第 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;

验收标准:

失败处理:

实操案例 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()

验收标准:

失败处理:

实操案例 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. 保留旧列一段时间,确认无问题后再清理。

验收标准:

失败处理:

生产检查清单

上线前至少检查:

常见坑

坑 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 是一个简单够用、运维成本低的起点。