最近在给公司的商品库做语义搜索方案,前期调研比选了一圈,最后反而把目光落回了自家那套PostgreSQL上。原因很简单:不想再维护一套独立的向量数据库集群。用pgvector直接把向量搜索塞进现有数据库,生产环境少一套组件,监控、备份、权限体系全都复用已有的,这个性价比对我这种运维精力有限的小团队来说太有吸引力了。
pgvector是PostgreSQL的一个开源扩展,专门解决"存向量、算相似度、做检索"这组需求。它支持float向量存储、三种距离计算(L2、内积、余弦)、以及IVFFlat和HNSW两类索引。不需要额外起服务,不需要学新的查询语法,用标准SQL就能完成向量检索。适合正在做AI应用(RAG知识库、图片搜索、推荐系统)但目前不想引入独立向量数据库的团队,也适合那些已经跑着PostgreSQL、想先用低成本方式验证向量搜索效果的个人开发者。
这篇就结合我自己的实操过程,从选型理由、环境准备、安装细节,到建表、写入、查询、索引调优,再到踩过的坑,完整过一遍pgvector的落地流程。
1. 选型之前,先想清楚为什么是pgvector而不是别的
选型这件事,最怕跟风。2024、2025年向量数据库概念很火,但仔细拆解需求,很多时候并没有到"必须上独立向量库"的程度。pgvector的核心价值在于:它让PostgreSQL同时具备关系型查询和向量相似度检索两种能力,数据不用在两套系统之间来回搬运。
具体对比几个常见选项:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| pgvector | 复用PG生态,运维成本低,支持SQL联合查询,事务一致性强 | 索引性能在亿级数据下不如专用库,功能相对精简 | 千万级以下数据量,已有PG基础设施的团队 |
| Milvus / Qdrant 等专用向量库 | 海量数据下性能强,索引类型更丰富,支持多租户等高级能力 | 需要独立部署与运维,需处理数据同步问题,技术栈增加复杂度 | 纯向量检索为主、数据量大、对检索性能有极致要求 |
| 其他数据库内向量能力(如Elasticsearch的dense_vector) | 复用已有ES集群,支持全文+向量混合检索 | 内存开销大,向量索引类型相对受限 | 已有ES且需要语义词法混合搜索 |
我的选型判断标准是这样的:
- 数据量:向量记录在千万条以内,单表几十GB以内,pgvector完全能扛住。
- 业务复杂度:如果业务需要在同一个查询里既做向量召回,又要过滤结构化条件(价格区间、品类、状态标志),pgvector可以一条SQL搞定。专用向量库也能做filter,但写起来和调优都更麻烦。
- 一致性要求:商品、订单这类数据不能容忍检索库和业务库不同步。pgvector和业务数据在同一个事务里,写入即查询,天然一致。
- 团队技能栈:团队成员都会SQL,但未必会用Milvus的SDK和索引概念。
如果这条Project在满足上述条件下,我会直接选pgvector,而不是多引入一套系统。多数情况下,选型过度比选型不足更可怕。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手安装前,必须先搞清楚版本匹配问题
pgvector的安装本身不算复杂,麻烦的是版本匹配。PostgreSQL的扩展机制要求扩展编译时使用的PG版本号和运行时完全一致,否则加载时会出现类似could not open extension control file或者incompatible library的报错。
重要的事情说三遍:先确认PostgreSQL版本,再决定下载哪个pgvector版本。 我用的是PostgreSQL 16,pgvector选0.7.x完全够用。如果你在生产环境还没升级PG,建议优先考虑已经广泛使用的稳定组合(如PG 15/16 + pgvector 0.6/0.7),而不是追最新版。
2.1 Linux下源码编译安装
Linux环境我推荐源码编译,虽然步骤多一点,但可控性最强。下面贴一份我完整跑通的流程(CentOS 7 + PostgreSQL 16 + pgvector 0.7.4):
bash复制# 1. 安装编译依赖
yum install -y gcc make git postgresql16-devel
# 2. 下载源码
git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git
cd pgvector
# 3. 编译和安装
make
make install
如果make install提示找不到pg_config,多半是PATH没包含PG的bin目录,手动指定一下:
bash复制export PATH=/usr/pgsql-16/bin:$PATH
make clean && make && make install
2.2 Windows下的DLL方案
Windows环境比较特殊,因为官方仓库不直接提供编译好的二进制文件,需要自己找合适的DLL或者用Docker。网上热词里提到"pgvector windows dll download postgresql 16",说明有不少人在这上面折腾。
我的建议是Windows下优先走Docker路线,真的比找DLL省心。官方镜像已经内置pgvector:
dockerfile复制# docker-compose.yml 示例
services:
pgvector:
image: ankane/pgvector:v0.7.4-pg16
environment:
POSTGRES_PASSWORD: yourpassword
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
ankane/pgvector这个镜像系列覆盖了多个PG版本和pgvector版本的组合,直接按tag拉取就行,省去本地编译的一切麻烦。
如果你确实需要在Windows裸环境(非Docker)下装,搜索"pgvector windows dll"能找到社区编译好的包。但务必检查两点:DLL对应的PostgreSQL major version是否完全一致;pgvector版本是否保守选择已被广泛使用的。我之前帮人排查过一个案例,就是DLL版本不匹配导致CREATE EXTENSION成功后查向量时报invalid memory alloc request size,重下正确版本后解决。
2.3 验证安装是否成功
安装完成后,进入数据库执行:
sql复制CREATE EXTENSION IF NOT EXISTS vector;
看到CREATE EXTENSION即代表成功。如果想进一步确认版本和可用函数,执行:
sql复制SELECT extversion FROM pg_extension WHERE extname = 'vector';
-- 期望输出类似 0.7.4
SELECT typname FROM pg_type WHERE typname = 'vector';
-- 期望输出 vector
3. 跑通第一个向量检索,理解pgvector的最小闭环
安装好之后,很多人会急着建索引、上生产,其实最稳妥的路径是先手动操作一遍最小闭环:建表、插入向量、查询相似度。我对这个环节的评价是——它是后续一切调优的地基,哪怕操作很简单,也值得完整走一遍。
3.1 创建带向量字段的表
pgvector的核心数据结构是vector类型。创建表时用vector(n)指定维度,n必须和你要存的嵌入向量维度保持一致。比如用768维的嵌入模型,就建vector(768)。
sql复制CREATE TABLE item_embeddings (
id bigserial PRIMARY KEY,
item_id bigint NOT NULL,
embedding vector(768),
created_at timestamptz NOT NULL DEFAULT now()
);
维度不一致时,插入或查询会直接报错,这个约束反而能帮你在早期发现嵌入模型的变更。
3.2 插入向量数据
插入方式很简单,把向量以字符串形式传进去:
sql复制INSERT INTO item_embeddings (item_id, embedding)
VALUES
(1001, '[0.013, 0.082, ...]'),
(1002, '[0.021, -0.015, ...]');
这里有个小技巧:从Python等语言写入时,向量先转成逗号分隔的字符串,外面再套上中括号,比如'[' + ','.join(map(str, vec)) + ']'。如果向量来自其他系统,可以用编程语言格式化好再传给SQL。
3.3 执行相似度查询
pgvector支持三种距离符号,分别对应三种相似度计算方式:
sql复制-- L2距离(欧几里得距离),越小越相似
SELECT item_id, embedding <-> '[0.012, 0.081, ...]' AS distance
FROM item_embeddings
ORDER BY distance
LIMIT 10;
-- 内积,越大越相似
SELECT item_id, embedding <#> '[0.012, 0.081, ...]' AS inner_product
FROM item_embeddings
ORDER BY inner_product DESC
LIMIT 10;
-- 余弦距离,越小越相似
SELECT item_id, embedding <=> '[0.012, 0.081, ...]' AS cosine_distance
FROM item_embeddings
ORDER BY cosine_distance
LIMIT 10;
等一等,内积的排序方向为什么是DESC?这是因为<#>返回的是负内积,负的越大(绝对值越小)代表内积越大。所以实际使用中需要ORDER BY inner_product DESC,或者直接不取别名、用表达式排序。这一点特别容易踩坑,我在第6节会专门展开。
最小闭环跑通的标志是:返回的结果列表符合直觉——和查询向量越相似的item排得越靠前。
4. 理解距离算法与索引机制,检索才能又准又快
小数据量不用索引直接扫描没问题,但数据量过了几十万条,全表扫描的速度就无法接受了。这时候需要索引。但在调索引之前,先搞清楚距离算法,因为索引的参数和查询效果都跟它相关。
4.1 三种距离算法到底怎么选
| 距离类型 | SQL运算符 | 返回含义 | 推荐场景 |
|---|---|---|---|
| L2(欧氏距离) | <-> |
数值越小越相似 | 向量已经经过L2归一化,或需要关注长度差异的场景 |
| 内积 | <#> |
返回负内积,排序需DESC | 向量未归一化、希望保留向量模长信息的场景 |
| 余弦距离 | <=> |
数值越小越相似 | 语义相似度场景(推荐系统、文本匹配)的默认选择 |
我的默认建议是:大多数文本语义搜索直接选余弦距离。原因是文本嵌入模型在计算相似度时,余弦相似度的语义解释最直观——两个文本语义是否接近,和它们的向量长度无关。而L2或内积会把向量模长差异也计算进去。
但如果嵌入模型本身已经对向量做了L2归一化(常见于很多新模型),那么三种距离的结果排序是一样的。这种情况下我优先用余弦距离,代码可读性最好。
4.2 HNSW索引和IVFFlat索引,别搞混了
pgvector提供了两种索引:IVFFlat和HNSW。很多人上来就问"哪个快",但其实这两个索引的工作逻辑完全不同,适用场景也不同。
IVFFlat(倒排文件 + 扁平存储):先对所有向量做聚类,把向量分到若干"桶"里。查询时只扫描和query最近的几个桶。它的核心参数是lists——桶的数量。查询时还有probes参数控制扫描几个桶。类比来说,就像图书馆先按大类分区,找书的时候只去几个可能的区翻,而不是全馆搜。
- 优点:内存占用小,构建快,适合较大规模数据。
- 缺点:聚类需要足够数据支撑,否则搜索质量和速度都打折;数据插入后需要重建索引才能体现新数据聚类分布。
HNSW(分层小世界图):在向量之间构建一张多层图,高层快速粗定位,底层精细遍历。提供m(每层最大连接数)和ef_construction(构建时动态候选集大小)两个参数。类比来说,就像一个社交网络——先认识几个关键联系人,再由他们带着找到目标圈子。
- 优点:查询精度高,无需训练聚类,插入新数据不需要重建。
- 缺点:索引体积更大,内存占用更高,构建时间更长。
我的选型经验:
- 数据量小于100万,优先HNSW。精度高,省心,不用考虑什么时候重建索引。
- 数据量几百万以上,且服务器内存有限,IVFFlat可能更合适。但要接受一个事实:
lists和probes需要调参,且数据量大变动后需要REINDEX。 - 写多读少且插入实时性要求高,也优先HNSW,因为IVFFlat对增量插入不友好。
4.3 参数调优的实操建议
如果你选了HNSW,这是我在0.7.x版本上验证过的起点参数:
sql复制CREATE INDEX ON item_embeddings USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
m:默认16。数据特征越复杂,值可以调大(如24/32),但索引体积和构建时间也随之上涨。ef_construction:默认64。值越大,索引质量越好,但构建越慢。质量要够高的话可以设128,超过256的收益就很不明显了。
查询时还可以动态调ef_search(查询时扫描的候选集大小),这个参数不建在索引上,而是每次查询前设置:
sql复制SET hnsw.ef_search = 100;
ef_search越大,召回质量越好,但单次查询越慢。建议从40开始,逐步增大到100,观察召回率变化。线上没有明显变慢的前提下,调大一些更稳妥,毕竟召回率翻了倍查询才慢30%,多数场景都划算。
如果你选了IVFFlat,建索引示例:
sql复制CREATE INDEX ON item_embeddings USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
lists建议根据行数来:行数/1000,但上限控制在5000左右,过大没意义。查询时用SET ivfflat.probes = 5;调整。别忘了,IVFFlat需要先有数据再建索引,空表或数据量极小时建出来的索引聚类意义不大,这也是很多人说IVFFlat不好用的主要原因之一。
5. 实战:把一张普通业务表升级成可语义搜索
前面讲的是基础用法,这一节我整体演示一遍如何把一个已有的商品表改造为支持语义搜索的完整流程。这个思路可以复用到文档库、图片库、用户画像等几乎所有"非结构化内容检索"场景。
假设现在有一张商品表:
sql复制CREATE TABLE products (
id bigserial PRIMARY KEY,
title text NOT NULL,
category text,
price numeric(10,2),
is_active boolean DEFAULT true
);
目标:让用户输入"适合雨天户外穿的轻便鞋子"这类自然语言,系统能召回到语义相近的商品,同时仍然支持category='运动鞋' AND price < 500这样的结构化筛选。
5.1 新增向量字段并回填数据
sql复制ALTER TABLE products ADD COLUMN embedding vector(768);
回填数据时,需要先把已有商品逐条送入嵌入模型生成向量。比如用Python脚本从数据库读出title+category,调用模型接口得到768维向量,再UPDATE写回:
python复制# 伪代码,示意回填逻辑
for batch in select_products():
texts = [f"{row['title']} {row['category']}" for row in batch]
vectors = embedding_model.encode(texts)
for row, vec in zip(batch, vectors):
update_embedding(row['id'], vec)
提示:回填是一个耗时操作,务必先小批量跑通,再全量执行。同时记得
UPDATE会触发行锁和WAL日志增长,高峰期不要硬跑。
5.2 创建HNSW索引
sql复制CREATE INDEX ON products USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
5.3 一条SQL完成语义搜索+结构化过滤
用户输入一句话后,先调用同样的嵌入模型把它转成向量,然后执行:
sql复制SELECT id, title, price,
1 - (embedding <=> '[0.012, ...]') AS similarity
FROM products
WHERE is_active = true
AND category = '运动鞋'
AND price < 500
ORDER BY embedding <=> '[0.012, ...]'
LIMIT 20;
1 - 余弦距离就是把距离转成相似度分数,方便业务侧展示"匹配度87%"这类效果。
这条SQL能跑得动,核心在于:postgres会先利用is_active、category、price上的BTREE索引过滤出候选集,再对候选集应用向量排序,还是直接走HNSW索引再过滤,取决于优化器的选择。 这里有个很实用的调优思路:如果组合查询很慢,可以通过EXPLAIN ANALYZE看执行计划,确认向量索引是否真的被用上了。
sql复制EXPLAIN ANALYZE
SELECT id, title
FROM products
WHERE is_active = true
ORDER BY embedding <=> '[0.012, ...]'
LIMIT 20;
如果执行计划里没有出现Index Scan using products_embedding_idx,大概率是查询条件里LIMIT和过滤条件的组合让优化器觉得全表扫描更快。这种情况可以尝试调整enable_seqscan等参数,或者把过滤条件换成更明确的形式。
5.4 生产环境要考虑的:和现有系统怎么配合
在一个已经跑着的系统里接pgvector,最现实的问题往往不是SQL怎么写,而是数据同步。搜索引擎里的热搜词也出现了"mysql/sqlserver/postgresql数据库同步软件"、"excel通过odbc连接postgresql",说明不少人实际的诉求是把散在多个地方的数据汇总到PG里做统一语义检索。
我之前在一个文档检索项目里的做法是:关系型主库继续负责业务,把需要做语义搜索的数据通过定时任务(基于updated_at增量同步)写入PG的向量表。同步任务用Python的APScheduler挂一个定时器,每5分钟拉一次增量,调用嵌入模型生成向量,再批量UPSERT到pgvector。存储过程、触发器或者专门的同步工具都行,关键点是把嵌入生成和向量写入放在异步链路上,不要让业务主流程阻塞在模型推理上。
另外,如果你的团队成员习惯用Excel分析数据,pgvector同样可以通过ODBC连接,直接在Excel里查询向量字段(不推荐但可行)。这至少说明pgvector的通用性比专用向量库好很多——标准PG生态的工具链它全都继承下来了。
6. 踩坑实录:这些细节不处理,迟早要返工
最后这部分是我最想说的。pgvector网上的教程很多,但真正让人抓狂的往往不是冷门难题,而是一些看起来不起眼、却会反复踩的坑。我按自己的踩坑顺序整理如下:
6.1 内积排序方向,老手也会翻车
用<#>算内积时,pgvector返回的是负内积。设计意图是让"越小越相似"在所有距离函数中保持一致,但不熟悉的人很容易直接用ORDER BY inner_product升序,结果得到的是最不相似的数据。
检查方法很简单:查一条和query完全一样的向量,看返回的距离是不是接近0或负数。如果发现结果反了,把ORDER BY改成DESC即可。
6.2 CREATE EXTENSION成功,但查询报错
典型报错是function xxx does not exist或者type "vector" does not exist。原因通常是:建表/查询时用了错误的database或schema。
pgvector的扩展是按数据库安装的,不是按实例安装的。你在A数据库执行了CREATE EXTENSION vector;,换到B数据库照样查不到vector类型。每个需要用到向量字段的数据库都要单独执行一次CREATE EXTENSION IF NOT EXISTS vector;。
6.3 索引建了,但查询不走
我在5.3节提到过这个问题。这里再补充一个常见原因:LIMIT条数和查询条件组合不佳时,优化器会高估全表扫描的成本。
我常用的处理方式有两种:
- 手动
SET enable_seqscan = off;测试是否走索引,如果走索引后明显更快,说明统计信息没跟上,执行ANALYZE products;刷新统计信息。 - 如果业务上必须频繁做组合过滤,考虑把高频过滤字段(比如
is_active)放进部分索引的WHERE条件里:
sql复制CREATE INDEX ON products USING hnsw (embedding vector_cosine_ops)
WHERE is_active = true;
这样查询里带is_active = true时,优化器更倾向于选择这个部分索引,候选集天然缩小。但要注意,部分索引的查询条件是固定的,业务过滤条件变化太灵活时不适用。
6.4 Windows下的DLL地狱
前面提过Windows环境建议Docker或WSL,这里再说具体一点。很多人图省事下载了某个博客/论坛分享的DLL文件,结果要么版本不对,要么依赖的MSVC运行库缺失。我自己在Windows上实际上也是最终回到Docker才彻底解决问题。其实部署在线上的生产环境很少用Windows跑PG,所以如果只是本地开发,Windows就用Docker跑个带pgvector的PG容器;如果是生产,直接上Linux裸机或者容器编排,别在Windows上硬扛。
6.5 数据量到了百万级,为什么变慢了
很多人实现完功能后,会在数据涨到百万行后突然发现查询开始变慢。排查思路:
- 首先确认索引类型是HNSW还是IVFFlat。IVFFlat在数据量翻了几倍后,原有聚类可能不再代表当前分布,需要
REINDEX INDEX ...重建。 - 其次检查
shared_buffers配置。向量索引的随机访问很吃内存,shared_buffers设得太小会导致频繁磁盘IO。PG默认128MB远不够,建议调整为物理内存的25%左右。 - 最后看查询是否真的用到了索引,执行
EXPLAIN确认。
6.6 中文语义搜索的效果问题
纯技术层面能跑通之后,还有一个容易忽略的问题:中文语义搜索的效果,取决于嵌入模型,而不取决于pgvector。pgvector只是存储和检索的工具,召回结果好不好,关键看谁生成的向量。用开源中文嵌入模型(如BGE系列、M3E)通常比用通用英文模型处理中文效果更可靠。而具体部署模型的时候,也要注意输入文本的预处理——中文没有天然空格分词,有的模型需要自己控制token长度,把title、category拼接时也要注意长度截断,避免信息丢失或超过模型限制。
7. 我现在的使用体会
pgvector这个项目我从0.5.x版本一路用到0.7.x,最大的感受是:它解决的不是"最强检索"的问题,而是"工程上最省心"的问题。 如果你的业务核心就是海量向量检索且对延迟极其敏感,专用向量库仍是更专业的选择;但更多场景下,把搜索能力以扩展形式装进PostgreSQL,换来的是架构极简、事务一致、运维省事。这一点对我来说,价值非常大。
最后分享一个小技巧:如果你打算把HNSW索引用在生产环境,建索引之前先手动设置maintenance_work_mem开大点。比如:
sql复制SET maintenance_work_mem = '2GB';
HNSW索引构建非常吃内存,默认值下大表建索引慢得让人怀疑人生,调大这个参数后构建速度能快一个数量级。这是我踩过最值回票价的坑,希望你不用重蹈覆辙。
