☰
PostgreSQL + pgvector 实现 RAG 语义搜索:从零搭建与调优
2026/10/7 21:55:25 网站建设 项目流程

最近几个月,我几乎每周都会被同一个问题打扰:“我想给知识库接一个 RAG(检索增强生成)能力,语义搜索是不是得上专门的向量数据库?好多教程都在推 pgvector,这玩意儿到底行不行?”

我的答案一直很明确:如果你项目里已经有 PostgreSQL,而且没有专职 DBA 团队去运维一套独立的向量库,那先别折腾新组件。你完全可以在 PostgreSQL 上装一个 pgvector 扩展,把向量当成一种普通字段来存、来查、来跟业务 SQL 无缝 join。我这一年里用这套方案做了好几个 RAG 项目,从内部文档问答到客户工单智能检索都跑过,踩过不少坑,也总结了一套从零搭建的完整方法。这篇文章就把整个过程拆开讲清楚,覆盖语义搜索的原理、pgvector 的安装选型、RAG 管线的落地、知识库形态的选择,以及生产环境调优时最容易翻车的几个点。

如果你是第一次接触语义搜索,或者正准备给自己的 PostgreSQL 数据库加一个“会理解语义”的检索能力,这篇文章可以直接照着操作。已经跑通过基础 RAG 的人,也可以重点看第 6 章和第 7 章,那部分都是我在真实业务里被数据教做人的经验。

1. 为什么先别急着换向量库:语义搜索和 RAG 的真实瓶颈

1.1 你遇到的其实是“关键词搜索搜不到东西”的问题

传统的 PostgreSQL 搜索,最常用的就是LIKE、ILIKE和全文检索(tsvector)。这些方案有一个共同的死穴:它们只能做字面匹配,做不了意图匹配。

举个例子。你在一个菜谱知识库里搜“番茄炒蛋的做法”,全文检索能召回所有包含“番茄”“炒蛋”字样的菜谱。但如果你换一种问法,比如“西红柿鸡蛋怎么做好吃?”这时候你用的是“西红柿”,不是“番茄”,全文检索大概率就抓瞎了,因为它的倒排索引里没有“西红柿”和“番茄”之间的语义关联。

再比如企业内部知识库,“我们的报销流程是什么”和“费用申请走什么手续”,字面上只有“报销”和“费用”这两个词可能沾边,但语义上指的是同一件事。这类问题用传统搜索做,召回率会低到让人怀疑人生。

这就是语义搜索要解决的问题:让检索系统理解输入文本的语义相似度,而不是字符重合度。而实现这一点的技术底座,就是把文本转换成向量。

1.2 “嵌入向量”是什么,一句话版本

嵌入向量(embedding)本质上是一个浮点数组,比如[0.012, -0.034, 0.092, ...],通常有 768 维、1024 维甚至 1536 维。这个数组是由一个嵌入模型把输入文本压缩成的“语义坐标”。

你可以把它理解为“把一句话压成一张地图上的坐标点”。语义接近的句子,坐标点靠近;语义无关的句子,坐标点离得远。检索时不需要理解语言,只需要计算两个坐标点的距离,找到离查询点最近的几个点就行了。

而 pgvector 做的事情非常简单粗暴:它让 PostgreSQL 多了一种vector数据类型,同时提供了一套高效计算向量距离的算法,还有加速检索的索引结构。你原来怎么用 JSONB、数组字段,就怎么用 vector 字段,完全不需要把一个表搬到另一个系统里。

1.3 pgvector 与专用向量数据库的真实取舍

这几年专用向量数据库确实火,Milvus、Weaviate、Qdrant 各有拥趸。但它们在带来检索速度的同时,也带来了一整套新问题:数据同步、双写一致性、运维监控、故障恢复,每一项都是成本。

我拿我实际项目的体验做一个对比,说实话,如果你的数据量在千万级以内,pgvector 的差距没有想象中那么大:

维度pgvector + PostgreSQL专用向量数据库
运维成本复用既有 PostgreSQL,不用引入新组件需要单独部署、监控、备份
数据一致性业务数据和向量天然在同一事务里需要处理两个系统之间的同步延迟
查询能力向量检索 + 关系过滤 + JOIN 一条 SQL 搞定向量检索强,复杂关系查询偏弱
大规模能力千万级以内表现稳定,亿级需要更多调优分布式能力强,适合超大吞吐
学习成本会 PostgreSQL 就会用需要学习新 API 和新生态

所以我现在的选型原则很固定:数据量不大、延迟要求不是极端严格的场景,一律 pgvector 起步;如果未来真到了亿级向量、需要水平扩展的时候,再考虑迁到专用向量库也不迟,因为你的语义搜索逻辑、嵌入模型、检索接口是一样的,迁移成本可控。

2. 环境准备:PostgreSQL 版本选择和不同平台安装 pgvector

2.1 PostgreSQL 版本怎么选,这事别纠结

先回答一个高频问题:“PostgreSQL 下载哪个版本好?”

从支持和性能表现看,我建议直接用你所在操作系统软件源里能提供的最新稳定版。写这篇文章时,PostgreSQL 16 和 17 都是不错的选择。版本太老的两个主要风险:一是 pgvector 对旧版本 PG 的支持会逐渐减弱,二是新版本在并行查询、WAL 写入、COPY导入上的改进相当明显,对向量库这种偏 IO 密集的业务来说收益不小。

我见过有些生产库还在用 PG 11 或 PG 12,想着“稳”。说实话,如果只是普通业务表,PG 11 再顶几年也没问题。但你要上 pgvector 做语义搜索,我建议至少 PG 13 以上,因为 pgvector 的 HNSW 索引在较新版本上配合得更好,你之后调试执行计划时也会省事很多。

2.2 Windows:预编译扩展和 Docker 两条路都给你

Windows 上装 PostgreSQL 本身不难,官网下载安装器,一路下一步就行。真正麻烦的是 pgvector 扩展。它有对应的原生 DLL,必须与你安装的 PostgreSQL 主版本号、位数(64 位)完全匹配,不然会报出找不到扩展控制文件的错误。

如果你不想折腾,最省心的方案是用 Docker:

docker run -d \ --name pgvector-demo \ -e POSTGRES_PASSWORD=postgres \ -p 5432:5432 \ pgvector/pgvector:pg16

这个官方镜像已经把 pgvector 编译进去了,容器起来之后直接用。日常开发调试,我强烈推荐这条路线,因为你本地装的 PostgreSQL 和线上版本很容易不一致,而镜像可以精确锁定版本号,避免了“本地能跑、线上不能跑”的魔幻问题。

如果你因为公司网络或安全策略不能跑 Docker,那就走预编译二进制方案。去 pgvector 的 GitHub Releases 页面找对应你 PostgreSQL 版本的 zip 包,里面会有vector.dll和相关的.control、.sql文件,把它们复制到 PostgreSQL 安装目录的share/extension和lib目录下,然后在 psql 里执行CREATE EXTENSION vector就能验证是否成功。注意:Windows 下 DLL 版本不匹配是最常见的报错来源,一定要逐个核对。

2.3 Ubuntu 和 Linux 环境:apt 和源码编译都要掌握

Ubuntu 下最简单的方式是直接用 PostgreSQL 官方提供的 apt 仓库:

sudo apt install postgresql-16 postgresql-server-dev-16 git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git cd pgvector make sudo make install

这里postgresql-server-dev-16是很多人会漏掉的一个包。make编译 pgvector 时需要用到 PostgreSQL 的头文件和pg_config工具,如果没装这个开发包,编译时会出现找不到postgres.h之类的错误。

还有一类场景是离线环境。我在客户现场遇到过完全断网的 Linux 服务器,这时候有两个办法:

一是找一台能联网的同架构机器,把 PostgreSQL 的安装包和 pgvector 源码仓库一起打包拷过去。编译时如果系统缺少依赖库,也是同样的思路:先下载依赖的.deb包,拷贝到目标机上用dpkg -i安装。

二是直接用 PostgreSQL 的二进制包加扩展的完整目录,整体拷贝到同版本、同架构的机器上复用。这个办法走的是“目录级移植”,操作更快,但对系统库版本有要求,搞不好会因为 glibc 版本不一致出问题。我个人更推荐第一种:老老实实在离线机器上源码编译,虽然多花十分钟,但成功率高得多。

2.4 macOS 安装步骤

macOS 上如果你用 Homebrew,先把 PostgreSQL 装好:

brew install postgresql@16 git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git cd pgvector make sudo make install

装完之后记得确认一下链接路径。Homebrew 的 PostgreSQL 是 keg-only 的,如果你的 PATH 里没有pg_config,编译时要用PATH="/opt/homebrew/opt/postgresql@16/bin:$PATH" make这种方式显式指定工具链。

还有个更快的办法:macOS 上直接用brew install pgvector也可以,Homebrew 官方已经收录了 pgvector 的 formula,一条命令就搞定,适合不想碰源码编译的人。

安装完成后,进入任意一个数据库,执行:

CREATE EXTENSION vector; SELECT extversion FROM pg_extension WHERE extname = 'vector';

能查出版本号,就说明环境没问题了。

3. 跑通第一次语义搜索:建表、嵌入、检索

3.1 建表和启用扩展

语义搜索本质上仍然是一张表、一个字段、一条查询。我们先创建一张最基础的文档表:

CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE docs ( id bigserial PRIMARY KEY, content text NOT NULL, embedding vector(768) );

这里的768是我用的本地嵌入模型nomic-embed-text的输出维度。如果你的嵌入模型是 OpenAI 的text-embedding-3-small,那就是1536;如果是text-embedding-ada-002,也是1536。建表时维度必须和模型输出严格一致,否则插入时会直接报错。

3.2 嵌入向量从哪来

这是新手最容易卡住的一环。你需要一个嵌入模型把文本转成向量,有两条路线:

一条是调用 OpenAI、通义等在线服务的 Embedding 接口,优点是用起来简单,缺点是数据要出网、每次调用要花钱、还要维护密钥。

另一条是用本地模型,我主力用的是 Ollama 加nomic-embed-text。它跑在自己机器上,离线可用,嵌入效果在中文短文本上的表现也够用。用 Python 生成向量非常简单:

import requests def get_embedding(text: str) -> list[float]: resp = requests.post( "http://localhost:11434/api/embeddings", json={"model": "nomic-embed-text", "prompt": text} ) resp.raise_for_status() return resp.json()["embedding"]

这里值得提醒一句:嵌入模型的稳定性和可复现性很重要。一旦你选定了某个模型做数据入库,后面查询阶段也要用同一个模型。如果你上线后把模型从nomic-embed-text换成另一个,老数据的向量空间和新查询的向量空间不兼容,语义检索会直接变成随机检索,这是个隐蔽但致命的坑。

3.3 插入向量数据

有了嵌入函数,插入就变得很朴素了。我一般用psycopg2批量插入:

import psycopg2 conn = psycopg2.connect( "host=localhost dbname=yourdb user=postgres password=postgres" ) texts = [ "番茄炒蛋是一道家常菜,做法简单,适合新手。", "西红柿鸡蛋汤需要在最后撒上葱花提香。", "报销差旅费需要提交发票和行程单。", "员工请假流程:先在系统里提交申请,再由主管审批。", ] with conn.cursor() as cur: for text in texts: embedding = get_embedding(text) cur.execute( "INSERT INTO docs (content, embedding) VALUES (%s, %s)", (text, embedding) ) conn.commit()

数据量大的时候,同样可以用COPY或INSERT INTO ... SELECT ...的高效方式导入。小规模学习阶段,executemany就足够了,不用一开始就把工程优化做过头。

3.4 三种距离度量选哪个

pgvector 提供三种距离运算,这是初学者最容易搞混的地方:

距离算法运算符特点与适用场景
欧氏距离<->范数差异敏感,适合图像向量等幅度有意义的场景
余弦距离<=>只看方向、不管幅度,文本语义匹配首选
内积距离<#>通常配合向量归一化使用,对高维稀疏数据有效

文本语义搜索领域,我建议直接用余弦距离。因为它把“频率”和“语义”解耦了:一段长文本和一段短文本,只要语义方向一致,得分就不会因为长度差异被压得太低。

一条语义搜索查询长这样:

SELECT id, content, 1 - (embedding <=> $1::vector) AS similarity FROM docs ORDER BY embedding <=> $1::vector LIMIT 5;

<=>返回的是“距离”,距离越小越相似。如果你想让数值直观一点,可以像上面这样转成“相似度”:1 - 距离。

3.5 建索引,不然表一大会死给你看

没有索引时,pgvector 会老老实实地做全表扫描:把每一行的向量都算一遍距离,再排序取前 5。数据量小的时候看不出来,一旦到几十万行,一次查询可能就是数百毫秒甚至秒级。

所以要在检索之前先把索引建好。pgvector 支持两种索引,我推荐优先考虑 HNSW:

CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops);

注意这里vector_cosine_ops要和查询时使用的距离运算符匹配。你用<=>就必须配vector_cosine_ops;用<->就配vector_l2_ops;用<#>就配vector_ip_ops。配错会导致索引无法使用。

4. 用 pgvector 组装一套真实可用的 RAG 管线

4.1 文档预处理:拆分粒度决定了检索质量的上限

很多 RAG 项目效果不好,问题不在大模型,也不在向量库,而是文档没有拆好。

我见过两种极端:一种是把整个 PDF 当成一条记录存进去,结果向量维度平均化了,查询向量只跟其中最接近的一小段相似,由于其他部分拉远了距离,整体相似度得分偏低;另一种是把文档切成两三句话的小段,结果每个 chunk 携带的信息量太小,上下文不完整,召回的段落看起来相关但缺少前因后果。

我的经验是,通用文档按 500 到 800 字切分,重叠区域设 100 字左右,这样既有足够的上下文,又不会因为 chunk 太大降低检索精度。具体公式是chunk_size取 2 到 3 倍的嵌入模型最大输入 token 数的三分之一,然后再根据你实际业务的句子密度微调。

另外,拆分之后要记得把原始文档 ID、段落序号、标题路径一起存进表里,方便以后追溯答案来源。RAG 系统一定要能回答“这段内容出自哪个文档”,否则没法过审,也没法审计。

4.2 入库:把切好的段落写进 docs 表

下面是我在实际项目里常用的建表结构:

CREATE TABLE doc_chunks ( id bigserial PRIMARY KEY, doc_id bigint NOT NULL, chunk_index int NOT NULL, title text, content text NOT NULL, embedding vector(768), created_at timestamptz DEFAULT now() ); CREATE INDEX ON doc_chunks USING hnsw (embedding vector_cosine_ops);

入库脚本和上一章的插入逻辑几乎一样,只是多循环一层:先对文档做分块,再逐块生成向量,最后批量写入。这一步建议放到离线任务里跑,不阻塞用户的实时查询。

4.3 检索:向量召回加元数据过滤

RAG 的检索不只有一个向量条件。生产环境里,我们通常还会加过滤条件,比如“只查 2024 年之后的文档”“只查技术类文档”等。

pgvector 在这里特别顺手,因为它归根结底还是 SQL。你可以把向量相似度、关系条件、全文检索等写进同一条查询:

SELECT doc_id, title, content, 1 - (embedding <=> $1::vector) AS similarity FROM doc_chunks WHERE doc_id IN (SELECT doc_id FROM docs WHERE doc_type = 'manual') AND created_at >= '2024-01-01' ORDER BY embedding <=> $1::vector LIMIT 8;

这套组合拳是专用向量数据库很难给到的:既有向量语义检索,又有传统结构化查询,还不用把数据复制到另一个系统里做 join。这就是我坚持用 pgvector 的根本原因。

4.4 生成:把召回的段落拼进 Prompt 交给大模型

检索只是前半场,RAG 的后半场是把召回结果组合起来喂给大模型生成答案。

一个最简单的完整 RAG 函数如下:

def answer_question(question: str) -> str: # 1. 将问题向量化 question_emb = get_embedding(question) # 2. 从 PostgreSQL 检索 top_k 内容 with conn.cursor() as cur: cur.execute(""" SELECT title, content FROM doc_chunks ORDER BY embedding <=> %s::vector LIMIT 5 """, (question_emb,)) chunks = cur.fetchall() # 3. 拼装上下文 context = "\n\n".join( f"[来源:{title}]\n{content}" for title, content in chunks ) # 4. 组装 prompt 并调用本地大模型 prompt = f"""请根据以下资料回答问题。 如果资料内容不足以回答,请直接说明“资料中没有相关信息”,不要编造。 问题:{question} 资料: {context} """ resp = requests.post( "http://localhost:11434/api/generate", json={"model": "qwen2.5:7b", "prompt": prompt, "stream": False} ) return resp.json()["response"]

这个函数虽然短,但已经是完整的 RAG 闭环。核心思想是“先检索到证据,再让模型根据证据回答”。你不需要让大模型硬记所有知识,也不需要每次重新训练,知识更新只需要重新清洗、拆分、嵌入、入库即可。

4.5 用框架折腾还是自己写,我的看法是分阶段

如果你刚入门,我的建议是先用上面这种原生写法跑通一个最小流程,把概念搞清楚。不要一上来就引 LangChain、LlamaIndex,因为这些框架的抽象层级很高,一个问题背后会牵扯到好几个概念,出了问题你根本不知道是嵌入模型的错,还是检索器的错,还是 prompt 模板的错。

当你理解整个链路之后,再决定要不要引框架。真入了生产环境,LangChain 和 LlamaIndex 都提供了 pgvector 作为向量存储的集成,可以直接对接,省去不少样板代码。但从可控性角度讲,我至今还是偏向自己维护一层简单的检索接口,因为框架升级时最容易把向量配置参数悄悄改掉,那才是生产事故的温床。

5. 知识库形态的边界:向量 RAG、结构知识库和知识图谱

5.1 三类知识库的能力差异

很多人会把“RAG 知识库”理解成一个筐,什么都能装。实际上知识库的形态直接影响检索方式和答案质量。我按实战用途做了个对比:

形态检索方式典型场景优势短板
向量 RAG 知识库向量相似度检索文档问答、客服、知识库搜索非结构化文本无需人工整理精度有限,需要辅助手段
结构知识库SQL、精确匹配财务数据、订单、人员信息高精度、强事务性数据结构化成本高
知识图谱(KG)知识库图查询、多跳推理多实体关系的问答,比如供应链推理路径清晰,可解释性好构建工作量很大

最典型的错误是:把结构化的数据(比如订单金额、库存数量)硬塞进向量库里,然后问“上个月销售额是多少”,向量库根本算不出精确聚合结果。这种场景就应该走 SQL,或者用 Text-to-SQL 把自然语言翻译成 SQL 查询。向量库负责非结构化的语义召回,SQL 负责精确数字计算,两者是配合关系,不是替代关系。

5.2 图片到底能不能存进 RAG 知识库

这是我在后台收到的高频问题:“RAG 知识库能存储图片吗?”

能,但要分清楚存什么。图片本身是二进制大文件,你不该把图片内容直接当作vector塞进 PostgreSQL。更合理的做法有两种:

第一种是存图片的“语义描述”。把图片交给多模态模型生成一段文字描述,再把这段描述转成向量入库,图片文件本身放到对象存储里,数据库里只存 URL 路径。查询时通过文字描述来召回,返回结果时给用户图片链接。这是成本最低、最稳的路线,也是市面上大多数“看图搜索”产品的实际实现。

第二种是直接存多模态向量。如果你用的是支持图文对齐的模型(比如 CLIP 类),图片和文本可以编码到同一个向量空间,那就可以把图片的向量直接存在vector字段里,和文本向量一起检索。但这种方案对模型的要求更高,维护成本也更高,一般场景用第一种就够了。

5.3 什么时候值得做知识图谱 RAG

纯向量 RAG 在实体关系复杂、需要多跳推理的场景下会明显力不从心。

举个例子,“有哪些供应商同时给 A 项目和 B 项目供货?”这个问题如果你把所有合同文本切块做向量检索,大概率召回的段落只有涉及 A 项目或 B 项目的片段,模型要跨段落推理 A 和 B 的公共供应商,这是向量检索不擅长的。

这种场景适合用知识图谱 RAG:先把供应商、项目、合同抽成实体和关系,存入图数据库;回答问题时先用向量召回相关实体,再沿着图谱做多跳查询,把路径证据交给大模型生成答案。pgvector 在这个架构里的位置仍然是“召回入口”,负责把自然语言问题映射到图谱中的候选实体,但真正的推理由图查询完成。

我这里再说清楚:知识图谱 RAG 不是替代向量 RAG,而是补位。图谱构建成本很高,只有当你确定高频问题需要多跳推理时,才值得建设。

6. 性能与质量调优:把语义搜索从“能跑”变成“好用”

6.1 HNSW 和 IVFFlat,到底选谁

pgvector 提供两种索引:

HNSW 是近些年主流向量索引,它基于“小世界图”的思想,检索时从图中随机找入口节点,逐步收敛到最近邻区域。它构建慢、占用空间大,但查询延迟低、召回率高,不需要预先聚类,适合大部分生产场景。

IVFFlat 则是先对全量数据做聚类,把向量分成若干 list 桶,查询时只搜索距离最近的几个桶。它在小数据集上表现不错,但有两个明显前提:数据要相对稳定,且要定期ANALYZE,否则桶分布和实际数据脱节,召回率会明显下跌。

我给你的选型建议非常简单粗暴:数据量几十万到千万级,无脑用 HNSW。只有当你的 HNSW 内存占用实在扛不住,或者检索延迟要求特别苛刻时,再回头考虑 IVFFlat。

HNSW 建索引语句:

CREATE INDEX ON doc_chunks USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);

m控制每个节点的最大连接数,值越大精度越高但内存越多;ef_construction控制建索引时的搜索范围,越大索引质量越高但构建越慢。这两个参数不需要一上来就调得很大,默认值在小数据集上就能跑得不错,等数据量上来了再做针对性压测。

6.2 查询速度慢,先查执行计划,别盲目调参

我在帮朋友排查性能问题时,最常见的情况是:索引建了,但查询根本没走索引,因为执行计划选择了全表扫描。排查方式很简单:

EXPLAIN ANALYZE SELECT id, content FROM doc_chunks ORDER BY embedding <=> $1::vector LIMIT 10;

如果看到Seq Scan on doc_chunks,说明你的查询条件让优化器认为全表扫描更便宜。常见原因有两个:一是表里数据量太小,优化器觉得走索引反而更慢;二是你的距离算子和索引的ops不匹配,比如你用<->查,却建了vector_cosine_ops的索引,优化器用不上索引,只能扫表。

另外还有一个容易被忽略的点:加了WHERE过滤条件后,优化器可能先按过滤条件扫描,再在结果集上做距离排序,这时候如果过滤后的结果集比较大,性能依然不行。我的做法是控制过滤条件的选择性,如果一定要过滤,给过滤字段也建上普通 B-tree 索引,并让执行计划走“索引过滤 + 向量再排序”的路线。

6.3 召回率不够怎么办:混合检索和 rerank

向量检索再强,也会有犯傻的时候。比如你搜“如何申请发票”,向量检索可能会召回大量讲“发票类型”“发票格式”的段落,而没召回真正讲“申请流程”的那一段。这时候要解决的不是向量搜索引擎,而是检索策略。

我目前的检索策略是组合拳:

  1. 第一路:向量相似度检索,召回 top 20。
  2. 第二路:PostgreSQL 全文检索(tsvector)召回 top 20。
  3. 两路结果做去重合并,然后用一个重排序模型(rerank model)对合并结果打分,取 top 5 作为最终上下文。

全文检索负责字面匹配,向量检索负责语义匹配,两者互补。重排序模型模型量级一般较小,但能把“表面上像、实际上偏题”的结果压下去。这一步对 RAG 回答质量的提升是肉眼可见的,尤其是应用在客服问答和内部知识库时,错误率能降一半以上。

还有一个门槛更低但同样有效的方法:召回后用小模型直接做一遍关键词过滤。比如先要求召回的段落必须包含问题里的核心实体词,如果都不包含,再退回到纯向量结果。这在冷启动阶段比引一个 rerank 模型更省事。

7. 一点踩坑复盘与私人经验

7.1 版本不匹配:最常见的“CREATE EXTENSION”失败

我见过太多人卡在CREATE EXTENSION vector这一步报错,错误信息是could not open extension control file。十次里九次是 pgvector 的版本和 PostgreSQL 主版本不匹配:你装的是 PG 16,但下载的 pgvector 是编译给 PG 15 的;或者你在 Windows 上复制 DLL 时装错了位数。

解决思路很简单:先把 PostgreSQL 的版本号记死,再去对应版本号找扩展包。别用“大概是最新版”这种心态装环境,编译型扩展对版本强敏感。

7.2 向量字段别开得太大,维度太多会撑爆 PostgreSQL

嵌入模型的输出维度直接决定了表的存储膨胀速度。768 维比 1536 维少一半空间,但模型能力强很多,两者在检索效果上的差距并没有那么大。我在生产项目里已经不止一次把 OpenAI 的 1536 维向量换成轻量模型,效果差不多,但数据库的磁盘占用和构建索引时间明显下降。

如果你确实需要 1536 维,可以考虑 pgvector 0.7.0 之后的halfvec类型,它是半精度浮点存储,能把内存占用降一半。代价是精度略降,适合对召回的细微差别不敏感的场景。

7.3 切片策略不要照搬模板,要按文档结构来

最后一个经验是我花了最多学费换来的:文档切片大小没有万能答案。

在技术文档里,标题、表格、代码块都是天然的语义边界,把它硬按 500 字切,往往会切断一个完整的表格,召回的段落残缺不全。我现在的一般做法是:先按文档结构提取标题层级,以章节为单位切块;超过上限的章节再往下按段落拆,并保留所属标题的上下文。这样召回的 chunk 自带“出处”,配合大模型也更不容易产生幻觉。

另外强调一点:插入之后如果数据更新频繁,记得定时做ANALYZE,让优化器看到新的统计信息。很多线上检索变慢的问题,最后查出来的原因不是索引坏了,而是统计信息太久没更新,优化器做了错误的选择。

这套方案从最开始搭建到现在,我评估过不少替代品,最终还是留在 PostgreSQL 加 pgvector 的组合上。它并不完美,但它足够日常,也足够让人把精力放在真正重要的事情上:数据清洗、切片、召回策略和提示词设计。如果你正准备从一个简单的文档问答开始做语义搜索,我建议你也从这个组合开始,别一上来就把架子搭得太重。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询