
最近在搭知识库的时候纠结了很久文本切片做完 embedding 之后向量到底放哪儿。单独起一个 Milvus 或者 Qdrant又要同步业务库的数据还要处理两套系统之间的一致性全塞进 PostgreSQL 用 pgvector反而省心不少。pgvector 是 PostgreSQL 生态里最主流的向量检索扩展它给关系数据库新增了 vector 类型和三种距离算子让你能用普通 SQL 直接写按相似度排序的查询而且过滤条件、关联查询、聚合统计可以全部揉进同一条 SQL。这篇文章从安装、建表、索引、联合查询到常见坑一条龙讲完适合正在做 RAG 知识库、推荐系统或者任何需要语义检索 业务过滤场景的开发者参考。1. 为什么说向量检索进数据库是个真需求1.1 从 RAG 知识库的架构说起做知识库问答的朋友对这套流程应该不陌生先把文档切成片段每个片段用 embedding 模型转成向量查询的时候把用户问题也转成向量然后在向量库里找出最相似的几个片段拼进 Prompt 扔给大模型。这里面的关键是找最相似片段这一步业界做法就是向量检索。向量检索本身不复杂复杂的是业务逻辑。比如一个企业知识库文档可能带部门、权限、类型、状态这些属性。你不能只做相似度召回你还得过滤掉当前用户没权限看的文档还要按发布时间倒序还要把召回结果和业务表 JOIN 才能拿到完整的文档信息。如果向量存在独立的向量数据库里这些过滤和关联就变成了跨系统操作先用向量库做近似最近邻召回拿到一批 doc_id再拿着 doc_id 去业务库查元数据、做过滤最后再在内存里排序。代码绕不说两边的数据还会经常对不上文档删了向量还留着权限改了检索结果还是旧的。1.2 单独部署向量库的隐形成本很多人一提到向量检索就想到 Milvus、Qdrant、Weaviate、Pinecone。这些产品本身没问题功能强、性能高但对大多数中小项目来说是杀鸡用牛刀。部署一套独立的向量数据库意味着你要多维护一个服务要考虑它的高可用、备份、监控还要写一套同步逻辑把业务数据灌进去。数据同步是最容易翻车的地方。业务表里一行文档更新了向量库里的向量也得跟着变文档删了向量也得删。要么用消息队列做异步同步要么写定时任务全量同步两种方案都有延迟窗口而且出问题的时候很难排查到底是哪一步没同步上。如果你的数据量只有几十万条、几百万条性能完全不是瓶颈稳定性和开发效率才是。与其维护两套存储不如把向量直接放在业务数据库旁边让数据库自己保证一致性。这就是 pgvector香的地方。1.3 pgvector 的定位够用、省事、SQL 友好pgvector 是 PostgreSQL 的扩展不是独立产品它只干一件事让 PostgreSQL 支持向量类型和向量检索。它的设计哲学很朴素——不追求极致的检索性能追求的是你能在一个地方搞定所有事情。表现在实际使用中就是向量可以存在普通表里向量列旁边就是普通的文本列、数值列、JSON 列建索引用 CREATE INDEX查相似度用 ORDER BY过滤用 WHERE关联用 JOIN。所有你熟悉的 SQL 能力都可以和向量检索组合使用而且是在同一个事务里ACID 保证删了就是删了。当然也要说清楚它的边界pgvector 的检索性能和专用的向量数据库有差距尤其是千万级别以上的数据规模和高并发场景。但如果你面对的是百万级以内的知识库、中小规模推荐系统pgvector 完全够用而且省下的运维成本非常可观。2. 环境准备从安装到建出第一个向量表2.1 安装 pgvectorLinux、Windows 与 Dockerpgvector 的安装分两个层面先装扩展文件再在数据库里执行 CREATE EXTENSION。Linux 上比较简单Debian/Ubuntu 可以用 apt 直接装对应 PostgreSQL 版本的包# Ubuntu/Debian以 PostgreSQL 16 为例 sudo apt install postgresql-16-pgvector # 或者编译安装 git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git cd pgvector make sudo make installWindows 上稍微麻烦一点官方没有直接给 exe 安装包需要到 GitHub 的 Releases 页面找对应 PostgreSQL 版本和架构的 zip 包解压后把文件复制到 PostgreSQL 的 lib 和 share/extension 目录。这一步容易出错我建议直接看官方安装文档或者更省事的方式是用 Dockerdocker run -d --name pgvector-demo \ -e POSTGRES_PASSWORDpostgres \ -p 5432:5432 \ pgvector/pgvector:pg16Docker 镜像是官方维护的里面已经装好扩展拉下来直接用我建议新手一律走这条路省去很多版本对不上的烦恼。装完之后在数据库里执行CREATE EXTENSION IF NOT EXISTS vector;执行成功就能用了验证一下SELECT [1,2,3]::vector;注意pgvector 的版本要和 PostgreSQL 内核版本匹配尤其 Windows 手动复制文件时zip 包的命名里会标明支持的 PG 主版本比如 pg16别下错了。执行 CREATE EXTENSION 报 could not open extension control file 的话基本就是文件放错目录或者版本不匹配。2.2 vector 类型与三种距离算子pgvector 给 PostgreSQL 增加了一个基础类型 vector。声明列的时候可以指定维度CREATE TABLE embeddings ( id bigserial PRIMARY KEY, document_id bigint NOT NULL, content text NOT NULL, embedding vector(1536) );这里的 1536 表示每个向量有 1536 个维度。维度必须在建表时声明而且每条记录插入的向量维度必须和声明一致否则报错。表建好之后检索的核心就是三个距离算子算子含义说明-L2 欧氏距离越小越相似最常用#负内积值越大表示内积越大注意存的是负数余弦距离越小越相似适合文本 embedding很多初学者会在这里犯迷糊我多解释两句。L2 距离和余弦距离都是越小越相似ORDER BY embedding - [...] ASC 然后 LIMIT 10就能取到最相似的 10 条。内积算子#返回的是负的内积所以写法是 ORDER BY embedding # [...] ASC值越接近 0 说明内积越大、越相似一般配合归一化向量使用。另外注意一个坑pgvector 里没有直接的余弦相似度它提供的是余弦距离1 - cosine_similarity。如果你在别的文章里看到有人用排序然后又拿排序值当相似度注意这个值不是 0~1 的相似度是 0~2 的距离。想要相似度用1 - (embedding $1)转换一下。2.3 程序侧写入ORM 和原生 SQL写入向量跟写入普通数据没什么区别INSERT 就行INSERT INTO embeddings (document_id, content, embedding) VALUES (1, pgvector 是 PostgreSQL 的向量检索扩展, [0.001, -0.002, ...]);程序侧稍微要注意的是 embedding 参数的类型。pgvector 的官方 Python 库是 pgvector-python配合 SQLAlchemy 或 psycopg 都有对应的辅助方法。在 Node.js 生态里很多人用 Prisma但 Prisma 目前没有内置的 vector 类型支持你需要用 $queryRaw 写原生 SQL我实测下来这样最稳定const result await prisma.$queryRaw SELECT id, document_id, content, 1 - (embedding ${vector}::vector) AS similarity FROM embeddings ORDER BY embedding ${vector}::vector LIMIT 10 ;这里$queryRaw的参数会自动做参数化处理不要自己拼字符串。不管是普通 SQL 还是带向量的查询用户输入一律走参数绑定这是底线别给自己挖坑。3. 索引选型决定查询快慢的核心3.1 没有索引的向量检索有多慢先做一个坏示范如果 embeddings 表里有 100 万条数据直接跑 ORDER BY embedding $1 LIMIT 10PostgreSQL 会对全表逐条计算距离然后排序这叫暴力搜索。100 万条其实很快能算完可能一两秒但数据到 1000 万、上亿的时候这个时间就完全不可接受了。更重要的是向量检索的近似最近邻不是加一个普通 B-tree 索引就能解决的它需要专门的索引结构。pgvector 提供了两种IVFFlat 和 HNSW下面分别说。3.2 IVFFlat聚类 倒排的思想IVFFlat 的全称是 Inverted File with Flat storage思路是把向量先聚类查询的时候只在最近的几个簇里搜索而不是全表扫。建索引的语法CREATE INDEX ON embeddings USING ivfflat (embedding vector_cosine_ops) WITH (lists 100);关键参数有两个lists 控制聚类中心数量查询时的 probes 控制搜索多少个最近的簇。lists 越大每个簇里的向量越少搜索越快但召回率会下降建议 lists 取max(100, 表行数 / 1000)比如 100 万行取 1000。probes 是查询时扫描的簇数量默认值是 1这代表只搜最近的一个簇召回率会低得吓人。一般要把它调大比如 probes 设成 lists 的 5%~10%召回率才够看。IVFFlat 有一个特别烦人的特点建索引的时候要求表里已经有数据否则聚类没有意义。而且如果后面数据量翻倍了原来的聚类可能严重失衡你需要重建索引。所以很多团队的实践是先导数据再建索引数据涨到一定程度就重建一次。3.3 HNSW图结构的高性能方案HNSWHierarchical Navigable Small World是 pgvector 0.5.0 之后加入的索引类型思路是把向量组织成多层图高层稀疏、低层稠密查询时从高层往下层走类似跳表的导航。它和专用向量数据库常用的 HNSW 算法是同一个东西所以性能表现更接近专用数据库。建索引语法CREATE INDEX ON embeddings USING hnsw (embedding vector_cosine_ops) WITH (m 16, ef_construction 64);关键参数m每个节点的最大连接数默认 16越大精度越高、内存占用越大。ef_construction构建索引时考虑的候选集大小默认 64越大索引质量越高、建索引越慢。ef_search查询时的候选集大小默认 40这是查询参数用语句级的 SET 设置不是建索引时设的。SET hnsw.ef_search 100;HNSW 相比 IVFFlat 的优势是不需要提前有数据支持随时插入数据增长也不至于让索引失效召回率通常更高查询延迟更稳。代价就是内存占用大毕竟图结构比聚类结构费内存。3.4 索引选型的实战建议对比维度IVFFlatHNSW建索引速度快慢查询速度快但受 probes 影响更快更稳定召回率需要仔细调参默认参数就很稳内存占用低高动态插入不友好数据翻倍要重建友好适合场景数据量大、内存紧张、静态数据中小数据量、频繁更新我的建议很简单新项目直接上 HNSW省心。除非你的数据量巨大且内存吃紧才考虑 IVFFlat。而且不管用哪种索引在数据量很小比如几万条的时候顺序扫描反而可能比索引更快不用过早优化。4. 核心实战一条 SQL 完成检索 过滤 关联4.1 表结构设计业务表与向量表直接拿知识库场景举例。假设你有一个文档表 documents存文档的业务信息一个切片表 chunks存文档切片后的文本同时把向量也放在这一张表里。CREATE TABLE documents ( id bigserial PRIMARY KEY, title text NOT NULL, category text NOT NULL, owner_id bigint NOT NULL, status int NOT NULL DEFAULT 1, created_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE chunks ( id bigserial PRIMARY KEY, document_id bigint NOT NULL REFERENCES documents(id), content text NOT NULL, chunk_index int NOT NULL, embedding vector(768) );这里我把 embedding 放在 chunks 表里一个切片一行和文本一起存。你也可以单独建一张 embeddings 表用 document_id 关联两种方式都行。我建议放同一张表少一次 JOIN写入也简单后面做逐行更新也方便。4.2 联合查询示例过滤 最近邻 关联用户提问公司今年的报销政策是什么先转成向量 v然后一条 SQLSELECT d.id AS document_id, d.title, c.content, 1 - (c.embedding $1) AS similarity FROM chunks c JOIN documents d ON d.id c.document_id WHERE d.status 1 AND d.category finance AND d.owner_id 10086 AND (c.embedding $1) 0.4 ORDER BY c.embedding $1 LIMIT 10;这条 SQL 和普通 SQL 唯一的区别就是 ORDER BY 和 WHERE 里多了向量运算符其它全是标准 SQL 能力。它做的事是只搜索 finance 分类、状态正常、属于当前用户的文档切片找到相似度距离小于 0.4 的候选按相似度排序返回前 10 条并且 JOIN 出文档标题。这才是 pgvector 最大的价值——你不用在向量库里先检索一批 ID再拿 ID 去业务库里二次过滤一切都发生在数据库内部一次查询搞定。代码量、调试成本、出错的概率全部降一个量级。4.3 过滤条件的执行顺序与性能注意组合过滤和向量检索的时候有件事必须清楚PostgreSQL 的查询规划器会决定是先走向量索引还是先用 WHERE 过滤。常见的情况是向量检索的代价高如果 WHERE 能把数据筛到很小范围规划器可能选择先过滤再算距离甚至直接顺序扫描。这里有一个实践规律如果过滤条件能筛掉大部分数据比如筛选后只剩几千条顺序扫描算距离反而比走 HNSW 索引更快如果过滤后还有几十万条那走向量索引更划算。你可以在 EXPLAIN ANALYZE 里看到实际执行计划别凭感觉猜。另外pgvector 从 0.7.0 开始支持带过滤条件的索引和迭代式索引扫描允许你在索引扫描过程中同时应用 WHERE 过滤减少了先召回再过滤的开销但这对 PostgreSQL 版本和 pgvector 版本都有要求生产环境要确认版本够新再上。4.4 更进一步的玩法混合检索与二次排序单纯向量检索有个天然问题它只懂语义不太懂精确的关键词。比如用户搜身份证复印件如果语义向量没有把身份证和复印件这两个关键 token 编码得足够近召回可能不理想。常见的补救方案是混合检索向量召回一路关键词召回一路用 PostgreSQL 自带的全文本搜索 tsvector两边结果合并再去重排序。SELECT d.id, d.title, c.content, 1 - (c.embedding $1) AS vector_score, ts_rank(to_tsvector(simple, c.content), plainto_tsquery(simple, $2)) AS keyword_score FROM chunks c JOIN documents d ON d.id c.document_id WHERE ... ORDER BY vector_score * 0.7 keyword_score * 0.3 DESC LIMIT 10;权重可以按业务调这种混合方案在知识库场景里非常实用。整个过程仍然是一句 SQL这是把向量检索放进关系数据库带来的直接红利。你还可以在这个基础上做 Rerank先用 SQL 粗召回 50 条再交给一个重排模型精排依然不用换存储。5. 常见问题与排查技巧5.1 维度不一致报错向量维度必须和你声明的一致。OpenAI 的 text-embedding-3-small 是 1536 维BGE、m3e 这些常见开源模型一般是 768 维。如果你建表写 vector(1536)插入 768 维的向量PostgreSQL 会直接报错报错信息类似 different vector dimensions 1536 and 768。这类问题通常不是突发的而是你换过 embedding 模型。我建议在表里加一列 model_name记录每批数据用的模型避免新旧模型数据混在一起。不同模型产出的向量空间完全不同混着用相似度没有任何意义检索结果会莫名其妙地差。5.2 索引看起来没生效一个常见困惑建了 HNSW 索引跑 EXPLAIN 却发现没有走索引。原因通常有三个第一数据量太小规划器认为顺序扫描更快这种情况不用管数据多了自然走索引。第二你用的算子或操作符类和索引不匹配。建索引时用的 vector_cosine_ops 只对应算子如果你用-查询索引就用不上需要额外建 vector_l2_ops 的索引。第三ORDER BY 的方向或 LIMIT 缺失HNSW 索引需要排序加 LIMIT 才能走索引。排查方法很简单EXPLAIN 看一眼EXPLAIN ANALYZE SELECT * FROM chunks ORDER BY embedding [0.1, 0.2, ...] LIMIT 10;看到 Index Scan using ... 说明走索引了看到 Seq Scan就按上面三个原因逐个排除。5.3 召回率差、相似度值看不懂这是新手最容易懵的问题查出来的相似度全是 1或者排名很怪。多半是数据的向量没做归一化或者你混淆了距离和相似度。之前提过余弦距离的范围是 0~2余弦相似度是 1 - 距离范围是 -1~1。如果你算出来的相似度都接近 1说明这批向量质量高、文本本身高度相似这不一定有问题。但如果所有结果排序都不对建议检查 embedding 前是否做了归一化很多模型输出的向量是未归一化的用余弦距离的话可以先统一 normalize 到单位长度再使用内积或余弦算子结果会稳定很多。5.4 查询慢的调参清单查询慢的时候别急着改代码先按这个顺序排查确认是否走索引EXPLAIN 看执行计划没走索引先解决 5.2。HNSW 的话调大 hnsw.ef_search从 40 到 100 试试召回率提升明显代价是查询变慢一点。IVFFlat 的话调大 probesSET ivfflat.probes 10;同时检查 lists 是否合理。检查过滤条件的可选择性如果 WHERE 过滤后数据仍然很多考虑调整 SQL 结构先缩小数据范围。确认 PostgreSQL 配置shared_buffers、work_mem 是否太小向量索引是否放得进内存。5.5 数据更新与删除的注意点向量数据更新的时候HNSW 索引支持增量维护但大批量 UPDATE 或 DELETE 之后索引可能会产生碎片导致查询变慢。我的习惯是批量重新生成 embedding 时先删掉旧向量再分批插入避免频繁的原地更新如果发现查询越来越慢、索引膨胀得厉害就重建索引REINDEX INDEX chunks_embedding_idx;备份恢复的时候也要注意pg_dump 默认会导出数据恢复后建议重建一次索引或者恢复后再执行 REINDEX避免索引状态不一致。这个问题在 pgvector 上比普通表更明显因为向量索引体积大、结构复杂。6. 个人的一些实操体会6.1 从最小可用开始别一上来就追求完美我第一次用 pgvector 的时候上来就建了 IVFFlat 索引、调了一堆参结果数据就三万条查询 5ms 和 8ms 的差别根本感知不到反而被参数选择折磨。后来学乖了先裸表跑通流程验证业务逻辑数据量到十万以上再考虑索引。向量检索的调参空间很大但很多参数在数据量不够时根本没有意义。如果你要直接上生产我建议把 HNSW 作为默认选择m16、ef_construction64 是官方默认值大多数场景下已经很稳。只在延迟仍然不达标时才去动 ef_search而且每动一次都要做一次召回率评估别只看延迟。6.2 embedding 模型比索引参数更影响效果检索效果好不好很大程度取决于 embedding 模型选得对不对其次是切片策略最后才是 pgvector 的索引参数。很多人花大量时间调 hnsw.ef_search却懒得换一个更适合中文场景的 embedding 模型这是本末倒置。中文知识库场景我自己实测过一些模型OpenAI 的 text-embedding-3-small 效果稳但数据出境是个问题国产开源模型比如 BGE-M3、m3e-large、text2vec 在中文语义上表现不差而且本地部署没有数据泄露风险。切片策略上按段落切、每个切片 300~500 字、重叠 50 字左右是我现在用的默认方案。6.3 扩展方向从 pgvector 延伸出去pgvector 本身还在快速迭代我比较关注几个方向halfvec 类型向量用半精度存储内存占用直接减半对 HNSW 这种吃内存的索引很友好sparsevec 稀疏向量类型适合维度极高但大部分为 0 的表示带过滤条件的 HNSW 索引过滤 检索会更高效。另一个很现实的扩展方向是把 pgvector 接进 RAG 链路做记忆系统。我之前做过一个小项目把用户历史对话的语义向量存在 PostgreSQL 里每次新问题来的时候先做一次向量检索找到相关的历史对话再交给大模型。整个过程没有引入任何新的中间件就是一张表加一条 SQL维护成本接近零。最后再分享一个小经验如果你用 Prisma 这类不支持原生 vector 类型的 ORM别硬凹直接在项目里建一个 repository 层统一用$queryRaw封装向量查询。这样 ORM 管业务数据pgvector 管向量检索两头都干净。我在生产项目里这么用了半年没出过问题反而是早期想把 vector 塞进 Prisma schema 的做法浪费了整整一天。