最近把公司内部的知识库问答从“关键词检索 + 大模型死记硬背”切换到了 pgvector 方案,整体效果提升非常明显。之前同事问一个业务问题,经常捞不到相关文档,或者要把整篇 PDF 塞给大模型让它强行“猜”,现在先用 pgvector 把候选文档捞出来,再交给模型生成回答,整个链路清晰、响应也快。这篇文章就把我用 pgvector 实现 PostgreSQL 语义搜索和 RAG 的完整过程写下来,包含环境搭建、模型选型、索引参数、踩坑记录,适合想自己搭一套本地知识库问答系统的开发者参考。
我默认你已经有基础的 PostgreSQL 使用经验,或者至少知道表、索引、SQL 查询是怎么回事。如果完全没接触过,建议先花半小时把官方教程过一遍,再回来读这篇文章会更顺畅。
1. 为什么是 pgvector:把向量搜索放进你已有的数据库
1.1 传统搜索为什么越来越不够用
很多业务系统里,搜索一直靠两条路:要么是WHERE title LIKE '%关键词%'这类模糊匹配,要么是 PostgreSQL 自带的全文检索tsvector/tsquery。这两者都属于关键词匹配,搜索引擎并不理解“你问的是什么意思”,它只是机械地找出文本里包含哪些词。
举个例子。用户问“最近三个月营收下降的原因是什么”,传统搜索会把“最近”“三个月”“营收”“下降”“原因”这些词拆开,去文档里找包含这些词汇的句子。但实际情况是,文档里可能写的是“Q2 收入同比减少 8%,主要受客户流失影响”,这句话里一个完整关键词都没对上,传统检索就彻底抓瞎了。这就是关键词匹配的天然瓶颈:它匹配的是字面,不是语义。
1.2 向量搜索到底在做什么
语义搜索的思路完全不同。它先把一段文本通过嵌入模型转换成一组固定维度的数字数组,也就是向量。这个向量的厉害之处在于:意思相近的文本,转换出来的向量在空间里也离得很近。
你可以把向量想象成地图上的坐标。坐标相近的地点,通常满足相近的需求;“公司财务情况分析”和“营收变动原因复盘”这两段文字,虽然字面没有重叠词,但向量坐标落在相邻区域,搜索的时候就能成功召回。这种方式从底层绕开了关键词的限制,真正去理解内容之间的关系。
PostgreSQL 里做这件事,靠的就是 pgvector 扩展。它把向量作为一种独立的数据类型加进数据库,提供距离运算操作符和专门的索引类型,让你在熟悉的 SQL 架构里直接完成语义搜索。
1.3 为什么我不推荐单独引入一个向量数据库
很多人一聊语义搜索就想到 Milvus、Qdrant、Pinecone 这些专业向量数据库。它们确实很能打,在高并发、千万级向量规模下有优势。但对大部分中小团队来说,引入一套独立向量数据库意味着多部署一套服务,多维护一组账号和备份策略,还要在业务库和向量库之间做数据同步。数据一致性一旦出问题,两边对不上,排查时你会非常痛苦。
pgvector 的优势在于,它直接跑在已有的 PostgreSQL 实例里,向量数据跟业务数据在同一个库、同一张事务里。业务表更新,向量表可以同事务提交,不需要写一堆 MQ 同步逻辑。备份也简单,pg_dump一下全带走。团队现有的数据库运维经验完全能覆盖,学习成本很低。
当然这不是说专业向量库没用。如果向量规模到了千万级别,查询延迟要求极高,或者需要布隆过滤、多向量混合检索等高级特性,pgvector 会吃力一些,那时候再评估专业向量库也不迟。关键是先明确自己的量级和复杂度,别一上来就上重武器。
2. 环境准备:从零装好 PostgreSQL 和 pgvector
2.1 版本选择:选稳定版还是尝鲜版
pgvector 对 PostgreSQL 的版本要求是 11 及以上,官方建议使用最新稳定版。我个人的建议是:生产环境选 16,已经足够稳定,遇到问题也有大量社区案例可以参考。17 也可以,但如果你依赖的一些第三方运维工具还没适配,会有小麻烦。至于便携版或免安装版,只建议在本地临时体验时用,做完实验别直接拿到生产,环境管理和依赖管理容易出现隐性坑。
Windows 上安装最省事的是用官方安装包,里面包含了 PostgreSQL 服务和 pgAdmin 可视化工具。Linux 环境则优先用发行版自带的包管理工具,比如 CentOS/RHEL 上通过dnf,Ubuntu/Debian 上通过apt。如果你习惯容器化,直接跑官方镜像即可,一个环境变量就能启动一个干净实例,非常适合拿来开发测试。
2.2 安装 pgvector 扩展的三种方式
pgvector 的安装分两步:先装扩展的代码文件,再在数据库里启用扩展。编译安装的流程通常是这样:
git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git cd pgvector make make installmake install一般需要 root 权限。装完之后,在 psql 里执行一句 SQL 激活扩展:
CREATE EXTENSION IF NOT EXISTS vector;如果用的是 PostgreSQL 官方 Docker 镜像,可以选带pgvector标记的镜像,或者自己写 Dockerfile 处理。Windows 下则可以下载预编译的安装包。装完验证一下:
SELECT vector '[1,2,3]';能正常返回向量值,说明扩展安装成功了。这一步看起来简单,但环境差异导致编译失败的情况不在少数,centos 上少装了postgresql-devel头文件是报错高发区。
2.3 建立第一张带向量字段的表
激活扩展后,创建一张带向量字段的表非常直白。假设我们要存一批产品文档,每段文本对应一个 768 维的向量,SQL 可以这样写:
CREATE TABLE doc_chunks ( id bigserial PRIMARY KEY, content text, content_vec vector(768), created_at timestamptz DEFAULT now() );有一点必须注意:vector(768)里的维度数要和你选的嵌入模型输出维度完全一致。选了 768 维的模型,就不能塞 512 维的向量进去,否则数据库直接报错。我在一开始就吃过这个亏,换模型之后忘记改字段类型,批量插入时被一连串维度错误打懵。后续我会把这个维度的配置集中到一个常量或者环境变量里管理,避免散落各处。
3. 语义搜索实现:数据入库与相似度查询
3.1 嵌入模型怎么选:云端 API 还是本地模型
语义搜索的质量上限,其实更多取决于嵌入模型,而不是 pgvector 本身。模型输出的向量质量不行,后面怎么调索引都救不回来。
目前主流的嵌入模型分两大派:一类是云端 API,比如 OpenAI 的 text-embedding-ada-002、text-embedding-3-small;另一类是本地开源模型,比如 BGE、M3E、GTE 系列。选择时主要权衡三点:
- 数据隐私敏感度:业务数据不允许出内网,就必须用本地模型。
- 硬件资源:本地模型对显存和内存有要求,没有 GPU 的话,CPU 也能跑但速度会慢几倍。
- 效果和语言适配:中文场景下,国产的 BGE、M3E 系列效果一直在线,不需要额外的翻译或预处理。
本地部署嵌入模型,目前比较省心的方案是配合 Ollama 使用。Ollama 能把模型封装成本地 HTTP 服务,一行命令启动,调用方式和 OpenAI 接口兼容。比如拉取一个 bge-m3 也没多复杂,启动后通过curl或者第三方 SDK 就能拿到向量。这对零基础想搭本地知识库的人来说非常友好,不用手动折腾 Python 环境、CUDA 配置之类的东西。
3.2 批量入库流程与代码实现
数据入库一般分三步:读取原始文档、切分成 chunk、逐条调用嵌入模型、再把文本和向量一起插入数据库。
我写了一个简单的 Python 脚本演示核心逻辑,这里用的是 psycopg2 驱动:
import psycopg2 from openai import OpenAI client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama") conn = psycopg2.connect( host="localhost", dbname="rag_db", user="postgres", password="your_password" ) def embed_text(text: str) -> list[float]: resp = client.embeddings.create(model="bge-m3", input=text) return resp.data[0].embedding chunks = [ "PostgreSQL 是一个功能强大的开源关系型数据库。", "pgvector 扩展使得 PostgreSQL 支持向量检索。", # ... ] for chunk in chunks: vec = embed_text(chunk) with conn.cursor() as cur: cur.execute( "INSERT INTO doc_chunks (content, content_vec) VALUES (%s, %s)", (chunk, vec) ) conn.commit()批量入库时有个性能细节值得注意:尽量合并插入而不是一条条 commit。每条记录都单独 commit 会产生大量磁盘同步开销,数据量一大明显拖慢速度。我一般会把数据攒到几百条一批,用executemany或者拼成一条多值 INSERT 提交一次,速度能提升好几倍。
3.3 查询向量相似度:三种距离怎么选
pgvector 提供三种距离操作符,对应不同的数学度量:
<=>:余弦距离,衡量方向差异,不受向量长度影响。文本语义相似度一般用它。<->:欧氏距离,衡量空间绝对距离。<#>:负内积,适合向量已经归一化的场景。
日常文本场景我基本只用<=>。语义相似的核心是方向一致,而不是长短一致。有些模型生成的向量模长差别很大,如果用欧氏距离,模长长的向量容易被误判为“更接近”,结果反而不如余弦距离稳定。
查询语句长这样:
SELECT id, content, content_vec <=> '[0.123, -0.456, ...]' AS distance FROM doc_chunks ORDER BY content_vec <=> '[0.123, -0.456, ...]' LIMIT 5;拿到的distance值越小,表示相似度越高。这里不要再加一个绝对的distance < 0.1这类阈值,因为不同模型的向量分布差异很大,阈值不好通用,不如直接取 Top-N。N 根据下游任务需要设定,RAG 场景一般 3 到 5 条就够用。
3.4 索引选择:IVFFlat 还是 HNSW
pgvector 目前支持两种索引,理解它们的区别对性能影响极大。
IVFFlat(倒排文件扁平索引)的思路是先聚类,把向量分成多个桶,查询时只搜索跟目标向量最近的几个桶。建索引时要指定lists参数,通常参考公式lists = 数据量 / 1000。比如 10 万条数据,lists 设 100 左右。它的问题在于:先有数据、后建索引,并且精度对 lists 参数的选取比较敏感,桶数太少或太多都会掉召回率。
HNSW(分层小世界图索引)是更现代的方案。它把向量组织成多层的图结构,搜索时从顶层快速下探到合适区域,再在底层精细遍历。HNSW 在精度和查询速度的平衡上表现更好,特别是数据量翻倍时不太需要重建索引,只是内存占用略高一些。
建索引的 SQL 示例:
CREATE INDEX ON doc_chunks USING hnsw (content_vec vector_cosine_ops);vector_cosine_ops表示按余弦距离建索引,其他选择还有vector_l2_ops和vector_ip_ops,分别对应欧氏距离和内积。我的建议是:数据量不大、开发阶段直接用 HNSW,参数默认就能跑出不错的效果;等数据规模上涨,再回头考虑是否需要调参或换 IVFFlat。
4. RAG 落地:把 pgvector 变成知识库问答的核心引擎
4.1 RAG 整体工作流程
RAG 的中文全称是检索增强生成,核心思路可以概括成一句话:先检索,后生成。用户提问之后,系统先通过向量检索找到知识库里最相关的内容片段,把这些片段作为上下文背景拼进提示词,再交给大模型组织答案。
这解决了大模型的两个老问题:一是知识陈旧,训练数据里根本没有最近的信息;二是容易一本正经地胡说八道,编造不存在的细节。数据库里的向量检索相当于让模型“开卷考试”,它只能在提供的材料范围内回答问题,出现幻觉的概率大幅降低。
最近常听到的 Agentic RAG、GraphRAG、Ontology RAG 这些概念,本质上都是在 RAG 的不同环节做增强,比如引入多轮工具调用、在检索前增加实体关系图谱过滤等。但地基都是同一套:把文档变成向量,用向量召回相关片段。把 pgvector 这一层跑通,之后想玩这些进阶玩法也方便。
4.2 文档拆分:chunk 大小和重叠策略
文档拆分这一步,很多人会低估它的重要性。拆分太粗,一大段包含很多主题,检索出来的片段噪声大;拆分太细,语义被截断,单条片段信息量不足。这块没有绝对标准,但有几个经验性参数可以参照:
- chunk 大小:500 到 1000 个 token 是比较稳妥的范围,中文差不多对应 500 到 1000 字。太短容易信息不足,太长则容易被无关内容稀释。
- 重叠:相邻 chunk 之间保留 50 到 100 个 token 的重叠,能防止关键句子被切断后丢失上下文。
具体拆分工具,直接用 langchain 的RecursiveCharacterTextSplitter或者 LlamaIndex 的切分器即可。也有人问有没有本地拆解工具,其实这类库本身就是本地跑的,不需要额外服务。拆的时候最好按段落、标题、代码块天然边界切,而不是硬按固定字符数截断。比如一个函数定义被拆到两个 chunk 里,检索时语义结构就乱了。
4.3 提高检索命中率:重排与检索评测
RAG 落地后,最先遇到的指标问题就是命中率不高:明明知识库里有答案,模型就是捞不到。这是很常见的新手困扰,排查优先级应该高于调提示词。
我自己的经验是把检索评测放到和模型评测同等的地位。准备一组测试问题,每个问题对应确定的标准答案段落,然后统计 Top-5 命中率。如果命中率本身就低,说明向量召回链路有问题,后面模型回答得再好也白搭。
提升命中率还有一个非常有效的技巧:加上重排器(reranker)。召回阶段先用向量快速捞回 Top-50,再用一个交叉编码器模型对这些候选项重新打对每一对“问题-文档”的匹配度得分,最后取 Top-5。这种两段式检索(召回 + 精排)能把命中率提升十个百分点不止。代价是多了一层模型调用,本地跑的话需要额外算力。
4.4 知识库能存图片吗
这个问题是很多知识库实践者必然会碰到的。标准答案是:纯图片不能直接进向量库,但图片的语义描述可以。嵌入模型处理的是文本,不是像素。如果你想让知识库理解一张产品架构图,就得先用多模态模型(比如具备识图能力的本地或云端模型)生成一段详细文字描述,再把描述文本嵌入成向量存入 pgvector。
图片本身的文件则可以存在本地磁盘或对象存储里,库里的向量字段对应描述文本,内容字段存文件路径或 URL。检索时通过文本匹配找到相关描述,再顺带返回图片地址。这是一个非常实用的解决方案,我建议团队在做多模态知识库时都按这个思路走。
5. 性能调优与常见问题排查
5.1 HNSW 参数调整的三板斧
HNSW 索引有三个核心参数,理解它们的关系才能调到适合自己的点:
m:每个节点最大连接数。越大连接越紧密,召回率越高,内存占用也越高。ef_construction:建索引时动态列表大小。越大建索引越慢,但图质量更好。ef_search:搜索时动态列表大小。越大召回越准,但查询延迟越高。
实际调优时,先把ef_search从默认值开始往上加,观察召回率提升和延迟增长之间的平衡。我常用的一组配置是m = 16、ef_construction = 100、ef_search = 50,日常业务响应都在 10 毫秒级别。如果数据量到了百万级,可能需要继续往上调,但每次调整后都建议用真实查询集做回归测试,避免凭感觉乱调。
注意 HNSW 建索引的操作是阻塞式的,数据量大的表建索引可能耗时几分钟到几十分钟,生产环境要安排在业务低峰期进行操作。
5.2 我踩过的坑:常见问题速查表
我把实际项目里遇到过的典型问题整理成一张表,方便大家对照排查:
| 问题现象 | 常见原因 | 解决办法 |
|---|---|---|
| 向量维度报错 | 模型输出维度与字段定义不一致 | 统一管理维度配置,插入前校验长度 |
| 查询走了全表扫描,速度很慢 | 没建索引或索引没生效 | 确认用EXPLAIN查看执行计划 |
| 结果召回率低,相关文档捞不到 | 文档拆分太粗、重叠不足 | 减小 chunk 大小,增加重叠比例 |
| 中文效果明显差 | 用了纯英文语料训练的模型 | 换成 BGE、M3E 这类中文优化模型 |
| 插入大量数据极慢 | 每行数据单独 commit | 改为批量插入,减少事务数 |
| 磁盘空间增长吓人 | 向量字段加索引后占用翻倍 | 评估 HNSW 参数,定期清理废弃数据 |
这里再单独提一个:EXPLAIN看执行计划是排查性能问题最快的方式,如果索引路径上没有出现Index Scan using ..._hnsw,说明优化器可能因为统计信息不准选择了全表扫描,跑一次ANALYZE更新统计信息往往就能解决。
5.3 数据量再大一点怎么办:分区与水平扩展
pgvector 在单表几百万向量内完全够用,但如果你真想把它推到千万级,有几个方向可以提前规划。
首先是为日期或业务 ID 做分区表。比如按月份分区,旧数据直接冻结或归档,查询只走对应分区,能显著减少扫描范围。分区表用法和普通表一致,pgvector 索引也可以建在分区上。
其次是业务隔离。多个业务线共用同一套知识库时,不要把所有数据塞进一张表,而是按业务 ID 分开,或者干脆分库。这样即使某个业务的数据暴涨,也只会拖慢它自己的查询,不至于影响全局。
最后是读写分离。主库负责写入和更新,从库负责向量查询。RAG 场景里查询频率远高于写入频率,把查询流量导给从库能大幅降低主库压力。只要能容忍几秒钟的复制延迟,这个架构可以撑到很大的查询量。
6. 最后再分享两个调试技巧
第一个技巧是可视化验证 embedding 效果。向量检索调参之前,先拿几个相近和不相近的句子测一下距离值,把结果打印出来做个粗略判断。如果模型对“退货流程”和“退款进度”这类高度相关的句子距离都很远,那说明模型选型有问题,别把时间浪费在调索引上。
第二个技巧是在查询日志里记录距离分数。每次搜索都把命中的距离分数写进日志,随着业务积累,你会慢慢形成对“什么样的距离分算靠谱”的直觉,后续设置重排阈值、观测检索质量退化都会方便很多。这些经验数据比任何理论参数都更贴合你自己的数据分布。
从 PostgreSQL 里加一个扩展,到跑通完整的 RAG 链路,整个过程比我预想的要顺。pgvector 最大的价值在于让我少维护一套系统,同时又拿到了具备语义理解能力的检索。真到向量数据规模爆炸的那一天,再考虑迁到专业向量库也不迟,但眼下它确实是性价比极高的选择。