ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用pgvector在PostgreSQL中实现向量检索与语义搜索的实战指南

用pgvector在PostgreSQL中实现向量检索与语义搜索的实战指南 如果只允许我在最近的项目中选出一个“高性价比”的 PostgreSQL 插件我会毫不犹豫地投给 pgvector。做知识库、语义搜索、或者给老系统加一个“以文找文”的功能时很多人的第一反应是引入 Milvus、Chroma 这类专用向量数据库。但实际用下来在数据量没有到千万级、又不想额外运维一套分布式系统时直接在现有的 Postgres 里扩展一个向量字段往往是最舒服的选择。这篇文章我会把自己从安装到上线的全过程拆开讲包括踩过的坑、地址段的选择、索引调参、以及如何和 embedding 模型接成一条可用的链路。1. 为什么数组项目我最终选择了 pgvector 而不是独立的向量数据库先说结论pgvector 不是万能的但在绝大多数中小型项目里它带来的收益远大于放弃它的损失。我的知识库项目起初准备用专门的向量数据库理由是“大家都说向量数据库更快”。后来仔细权衡后发现项目的数据量满打满算也就三十多万条而且这些数据本身就在 PostgreSQL 里事务、权限、备份都要和现有系统联动。再引入一套独立向量库意味着数据同步成了必须处理的难题还要学习一套新 API运维成本直接翻倍。1.1 在现有 Postgres 里增加“语义检索”有多自然pgvector 提供了原生的vector数据类型可以在普通的表里直接加一列例如ALTER TABLE articles ADD COLUMN embedding vector(1024);这一列和其他字段一样参与 ACID 事务、支持ORDER BY、WHERE过滤也能被备份工具扫描到。对比一下独立向量数据库的方案你需要把文章正文和向量分别存放在两套系统里然后自己处理两侧的写入顺序、失败回滚、数据迁移。如果源数据还在 Postgres这种埋点式同步迟早会变成坑。另一个容易忽略的点是权限和生态。PostgreSQL 的 GRANT、行级安全策略可以直接作用到向量列上对称加密、备份恢复、流复制这些成熟的机制也能沿用。这对于企业项目意味着不用为了一个“向量搜索”功能而引入大量新的安全审计项。1.2 pgvector、Milvus、Chroma、Qdrant 的真实差异用一个表格来展示我在选型时最关心的维度维度pgvectorMilvusChromaQdrant部署复杂度极低作为插件装进 Postgres需独立服务 etcd/对象存储等相对简单默认单机文件模式需独立服务数据一致性强事务一致需要额外配置确认较弱较强大数据量下的性能上限千万级以内表现足够适合亿级规模百万级以下百万/千万级与现有业务数据关联查询天然 JOIN需要通过 ID 手动关联手动关联手动关联运维成本Postgres 现有运维体系额外监控/扩容/升级较轻中等功能完备度基本的 TopN 相似度检索近两年开始支持 HNSW、窗口函数标量过滤、混合搜索、分区丰富简易 API快速原型过滤器和 payload 设计优秀当时我们的核心诉求是30 万文档、每晚更新一次、在线检索延迟 300ms。pgvector 加上 HNSW 索引完全接得住这个量级如果在 Milvus 里跑等于杀鸡用牛刀。1.3 什么时候应该避开 pgvector如果你想避坑建议先想清楚自己的数据量级。如果数据量已经达到千万级以上、单条向量维度很高而且请求模式是高频并发 TopN 检索那 pgvector 很可能不是最佳选择。独立向量库在稀疏向量、混合检索、超大规模 ANN 场景下做得更极致。另一个极端是——如果你只有几百条数据连索引都不需要建也没必要专门搭一个 Milvus直接在 pgvector 里用顺序扫描就能在毫秒级返回结果。我的个人经验是入门级项目优先上 pgvector等真的遇到 QPS 瓶颈了再迁移到专业向量数据库也不迟。因为 pgvector 本身就是匿名地做 ANN 搜索它不会迫使你把业务数据全塞进一个封闭体系后续迁移成本反而低。2. 从零安装PostgreSQL 与 pgvector 的版本搭配这一步看起来简单却是我见过翻车最多的地方。pgvector 以扩展插件形式安装意味着你的 PostgreSQL 版本必须在它的支持范围内。建议直接使用 PostgreSQL 13、14、15、16、17 这类稳定版本并下载和系统匹配的安装包。2.1 先确认底子PostgreSQL 版本与操作系统安装前先执行psql --version我遇到过生产库还是 PostgreSQL 10 的情况那是 pgvector 比较难直接支持的版本。如果你的主库版本偏老建议先升级 PostgreSQL 或让数据库管理员帮忙确认扩展兼容性。操作系统中Windows、LinuxUbuntu/Debian、CentOS/Red Hat、macOS 都有对应的安装方式但细节差别不小。Windows 用户优先考虑官方的 EDB 安装包。下载页面会提供postgresql-16.x-windows-x64.exe安装时勾选 “Stack Builder”里面通常能顺带选择 pgvector。但更稳妥的做法是手动下载对应版本的pgvector-0.7.x-windows-x64.zip解压后将vector.dll和vector.control等文件复制到 PostgreSQL 的lib和share/extension目录下。2.2 在 Linux 上的几种标准安装方式Ubuntu/Debian 上如果确认已经加入了官方 PostgreSQL apt 仓库可以这样来sudo apt update sudo apt install postgresql-16-pgvectorCentOS/RHEL 使用 PostgreSQL 官方 yum 仓库则通常是sudo yum install pgvector_16注意包名的后缀要与大版本对应如果系统同时存在多个 PostgreSQL 大版本安装时要尤其小心。我身边就有一位同事因为装了postgresql-15-pgvector却连接的是 14 的实例白白浪费了半小时排查。也可以源码编译安装。对想跟着源码更新的朋友这是我实际跑过的流程git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git cd pgvector make sudo make install源码编译前需要确保 PostgreSQL 的开发头文件已安装例如 Ubuntu 下是postgresql-server-dev-16。编译报错时最常见的提示是PostgreSQL headers not found先补这个包再继续。2.3 macOS 用户的安装细节macOS 如果使用 Homebrew会比较省心brew install pgvector但这里有个前提你的 PostgreSQL 可能也是通过 Homebrew 装的。如果 PostgreSQL 是通过 Postgres.app 安装的路径布局不一样brew install pgvector不会生效。这种情况下最简单的办法是切换到 Postgres.app 所在的安装路径然后把pgvector源码编译产物复制到对应目录。建议直接创建vector测试库来验证pg_config --pkglibdir pg_config --sharedir输出会告诉你插件该复制到哪两个目录。2.4 确认安装成功的标志安装后进入数据库执行CREATE EXTENSION vector; SELECT vector [1, 2, 3] - vector [1, 2, 4];如果返回一个数值距离说明扩展已经生效。如果提示could not open extension control file问题基本出在插件文件和 PostgreSQL 安装目录的版本不对应。检查CREATE EXTENSION时用的连接指向哪个 PostgreSQL 实例尤其是本地存在多个 PostgreSQL 服务时这种错误最容易出现。3. 核心 API 点向量类型、运算符和距离函数pgvector 提供了一套非常克制的 API大面上就三种运算符对应三种距离度量。理解它们的区别才能在上线后避免“召回结果看起来不对”的问题。3.1 三种距离函数什么时候用-欧几里得距离强调向量在空间中的绝对远近适合体型差异明显的特征向量。余弦相似度只关注方向一致性忽略模长。文本 embedding 中非常常用。#内积往往用于向量方向与模长都有意义的情况比如用户偏好向量与物品向量做点积得到的就是一个“匹配分”。以我的知识库为例用的是 OpenAItext-embedding-ada-002生成的 1536 维向量。文本之间的强弱关系更接近方向上的相似而不是空间远近所以我默认用。如果某些场景期望模型能区分“文章长度带来的语义强差异”才考虑换成-。3.2 建表、写入、查询的基本节奏创建表时直接注明向量维度这个维度必须和 embedding 模型输出对齐CREATE TABLE documents ( id BIGSERIAL PRIMARY KEY, title TEXT, content TEXT, embedding vector(1536) );写入一条记录时不需要做任何特殊转换把模型输出的数组直接作为字符串传进来就可以INSERT INTO documents (title, content, embedding) VALUES (PostgreSQL 安装指南, 每一步都该注意什么..., [0.012, -0.023, ...]);查询时用ORDER BY配合距离运算符加LIMIT控制返回条数SELECT id, title, embedding [0.01, -0.02, ...] AS distance FROM documents ORDER BY embedding [0.01, -0.02, ...] LIMIT 10;有人在网上会纠结要不要把查询向量单独提出来避免重复写一长串。其实可以直接用一个参数PREPARE search_articles(vector(1536)) AS SELECT id, title FROM documents ORDER BY embedding $1 LIMIT 10; EXECUTE search_articles([0.01, -0.02, ...]);3.3 过滤条件不要只记得距离排序需要强调的是向量距离排序通常不应该脱离业务过滤条件单独使用。比如只搜索某个分类下的文章可以在WHERE里加条件SELECT id, title FROM documents WHERE category_id 7 ORDER BY embedding [0.01, -0.02, ...] LIMIT 10;索引在这种情况下能否生效要分两看pgvector 的近似索引本身不做条件过滤PostgreSQL 的优化器会在同类条件下评估是先按向量索引取 TopN 再过滤还是先过滤再算距离。数据量变大后这类场景要单独做压测不能想当然。我还习惯在结果里把距离值一起返回用来校准阈值。比如排查“为什么召回的内容没到预期”时看一眼 distance 分布就大概知道模型有没有收敛。4. 从语义搜索到知识库与 Embedding 模型配合的完整链路安装好 pgvector 只是拿到了一个远程的容器真正让项目“活”起来的是把它接到 embedding 模型上。这个环节我在不同模型之间切换过踩过不少坑聊点经验。4.1 选 Embedding 模型OpenAI 和 Ollama 本地方案如果你面向的是中文知识库OpenAI 的text-embedding-ada-002依然是一个很坚实的基线只有 1536 维质量稳定生态成熟。代码也很简单import openai openai.api_key your-key resp openai.Embedding.create( modeltext-embedding-ada-002, input[你的文本内容] ) embedding resp[data][0][embedding]如果想完全在本地部署、避免外部接口依赖可以用 Ollama 拉一个支持 embedding 的模型比如nomic-embed-text或者常见的中文模型。启动后调用本地接口curl http://localhost:11434/api/embeddings -d { model: nomic-embed-text, prompt: 你的文本内容 }返回的embedding数组可以直接存入 pgvector。建议选模型之前先统一维度。不同模型输出维度差异很大例如all-MiniLM-L6-v2是 384 维bge-large-zh是 1024 维text-embedding-ada-002是 1536 维。维度一旦建在表字段里之后再换模型就涉及表结构变更和全量重算相当费劲。所以务实验证模型稳定性后再定下维度。4.2 在 Python 后端里把查询串起来我常用 FastAPI 封装查询接口。核心步骤包括读取请求里的 query 文本、调用 embedding 接口获得向量、然后拼接 SQL 到 pgvector 查询。下面是一个最小可行的示例import psycopg2 from fastapi import FastAPI import openai app FastAPI() def embed_text(text: str) - list: resp openai.Embedding.create( modeltext-embedding-ada-002, input[text] ) return resp[data][0][embedding] def search(query: str, top_k: int 5): vec embed_text(query) conn psycopg2.connect(dbnamepostgres userpostgres passwordxxx) cur conn.cursor() cur.execute( SELECT title, content, embedding %s::vector AS distance FROM documents ORDER BY embedding %s::vector LIMIT %s , (vec, vec, top_k) ) rows cur.fetchall() cur.close() conn.close() return rows有个小细节%s::vector里的类型转换很关键。psycopg2 默认不会把 Python 列表自动转成 pgvector 的vector类型如果少了显式转换数据库可能报operator does not exist: vector character varying。这也是接 pgvector 最常见的错误之一。4.3 文档分块策略和元数据过滤这才是真核心向量检索的效果上限很大程度上由文档分块决定。当时为了开发效率我先尝试把一整篇文章切成 2000 字一块跑出来的效果很一般。后来改成按段落切分每块控制在 300 到 500 字并保留段落标题作为元数据召回准确性明显上升。核心原因是 embedding 模型对短文本的语义捕捉更稳定。块太大时关键信息被长篇幅稀释距离计算会把“整体主题”和“关键细节”混为一谈。分块时我会顺便把来源文档 ID、章节路径、发布时间等存成普通列查询时用元数据过滤显著减少无关内容干扰。例如WHERE doc_id 12 OR published_at 2024-01-01这种过滤成本低但对用户体验的提升很直观。5. 索引选择不是小事ivfflat 和 HNSW 实测踩坑pgvector 的精髓在索引。初期数据量小全表扫描也能跑但当表到了几十万行以后还傻乎乎地每次扫全表延迟会从毫秒级涨到秒级。我在这部分花了最多时间调参也踩过最深的坑。5.1 两种索引的底层逻辑pgvector 先后提供了两种索引算法IVFFlat倒排文件平面索引首先把向量聚类成lists个桶查询时只扫描附近的桶每个桶内做精确计算。速度较快但有个明显问题——它需要先有足够数据来执行ivfflat的聚类训练否则索引质量很差。HNSW分层小世界图构建一张多层的近邻图搜索时从高层粗粒度跳到低层精粒度。不需要训练过程插入新数据后增量更新整体查询性能和召回率通常优于 IVFFlat在越大的数据集上优势越明显。你可以在建索引时通过USING hnsw (embedding vector_cosine_ops)或USING ivfflat (embedding vector_cosine_ops)来明确指定。5.2 索引参数的经验值我的经验建议如果你的数据可以一次性大批量导入用 IVFFlat 也够但如果数据持续增长、线上写入不断HNSW 是更省心的选择。HNSW 有两个关键参数m节点的最大连接数默认 16。越大召回率越高同时内存占用和查询耗时也会涨。我常用m 32。ef_construction建索引时动态候选列表大小默认 64。提高它能提升索引质量但会明显增加建索引时间。我是先在这个参数上设了 128构建时间可以接受再测试了大小时才决定最终参数。CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops) WITH (m 32, ef_construction 128);IVFFlat 的lists参数通常取sqrt(行数)的近似值。比如 30 万行数据lists可以取 600 到 1000。太小时每个桶的数据过多查询变慢太大时训练和查询都可能不稳定。5.3 维度不匹配和「列大小」报错处理我在使用时经常遇到下面这种报错ERROR: vector column does not have correct dimensions原因往往是写入的向量维度和列定义的维度不一致。比如列定义是vector(1536)却传了一个长度 1024 的数组。这个错误通常发生在模型切换或文本预处理阶段排查时直接检查数组长度。另一个 I 经常见到的坑是搜索时当成exact false却依然期望 100% 召回。HNSW 和 IVFFlat 都是近似最近邻搜索不是精确计算偶尔漏掉真实最近邻是正常的。业务如果真要求精确排序可以去掉索引强制顺序扫描或者把LIMIT从 10 扩大到 100再在应用层做二次精排。5.4 日常维护与更新注意事项向量索引会占用额外的存储和内存。HNSW 索引把图结构常驻内存消耗比较大但性能可观。在 PostgreSQL 配置里如果发现内存吃紧可以通过降低m和ef_construction来控制体积。批量更新数据时我建议先删索引再灌数据灌完重建索引。这是很多从 MySQL 转过来的同学容易忽略的操作。一次导入更新到有索引的表里由于每次都触发图更新耗时会成倍增加还可能导致索引碎片化。重建索引命令也很简单DROP INDEX documents_embedding_idx; -- 执行大批量更新 CREATE INDEX documents_embedding_idx ON documents USING hnsw (embedding vector_cosine_ops);写完这批数据后跑一个最基本的耗时测试看看EXPLAIN ANALYZE里的实际执行计划是不是走了索引EXPLAIN ANALYZE SELECT id FROM documents ORDER BY embedding [0.01, ...] LIMIT 10;如果看到Index Scan using documents_embedding_idx基本就稳了。如果出现Seq Scan需要检查是不是查询条件里的类型转换出了问题或者表数据量太小导致优化器认为全表扫描更快。6. 最后两年的实战笔记如果让我给准备用 pgvector 的人一条最主要的建议那就是先在明确业务过滤条件下测通一小批数据再考虑放大到全量。很多人在演示阶段拿几百条数据跑得很开心但一旦加上十几种元数据过滤条件、写并发、更新频率后索引设计就会变复杂。此时先想清楚“索引是给谁用的”比堆配置更重要。另外PostgreSQL 12 以前版本的兼容性是很大的限制建议尽早升级到 16 或 17。pgvector 的优势随着新版本越来越明显比如对更多运算符的支持、更完善的查询优化。如果团队本来就在用 Postgres那么通过 pgvector 添加向量检索能力几乎是最平滑的演进路线。刚开始时留好足够的字段扩展空间和模型选择时间后面真的能省掉很多返工成本。
返回列表