Postgres 混合搜索实践:配合 pgvector 与 tsvector 解决向量检索精确匹配失真
- 作者

- 姓名
- Nino
- 职业
- Senior Tech Editor
在构建检索增强生成(RAG)应用时,许多开发者都会遇到一个令人棘手的现象:单靠纯向量检索(Vector Search)在真实的生产场景中常常表现平庸。形如 ORDER BY embedding <=> $1 LIMIT 5 的经典 SQL 语句在 Demo 阶段效果拔群,但在用户输入精确错误码(如 ERR_MODULE_NOT_FOUND)、特定产品 SKU 或客户名称时,检索召回率会迅速崩溃。
本文总结了在 PostgreSQL 17 上结合 pgvector 0.8.0 搭建生产级混合搜索(Hybrid Search)系统的架构经验。通过使用倒数排名融合算法(Reciprocal Rank Fusion, RRF)将向量相似度与 Postgres 内置的全文本搜索(Full-Text Search)进行融合,无需引入独立的向量数据库,即可在既有业务数据库中解决精确匹配失效的问题。
纯向量检索为何在精确匹配场景下失效
向量嵌入(Embedding)的核心机制是将文本映射到高维连续空间中以捕捉语义相关性,而非记录字面 Token。这种特性在概念性查询(例如将“如何退订服务”与“订阅终止”进行匹配)时非常有效,但在面对低熵、高度确切的关键词时则暴露出明显的局限性。
当用户搜索明确的错误代码(如 ERR_MODULE_NOT_FOUND)时,嵌入模型会将该文本转化为靠近“import”、“module”和“error”等常规概念的高维向量。最终,纯向量搜索会返回 5 段通用的排错指南,而忽略了真正包含该错误码的唯一文档。
三类容易失效的查询场景
根据实际工程经验,纯向量检索在以下三类查询中极易失真:
- 精确错误码与日志:系统抛出的特定 Exception 字符串或日志 ID。
- 产品 SKU 与订单号:如发票编号、物料编码等高密度数值字符组合。
- 专有名词与人名:罕见的企业名称、缩写词或特有产品代号。
在这三类场景中,关键信息取决于出现频率极低的字面 Token。将这些稀有 Token 平均化到 1536 维的向量空间中时,其特征信号会被严重稀释为背景噪音。
此外,文本分块(Chunking)尺寸过大也是加剧该问题的原因之一。一个 1500 词的文本块仅生成一个代表全文平均语义的向量,其中单句精准的 SKU 信息很难显著改变整体向量的方向。将分块大小压缩至 200–400 Token 并保留适度重叠,对召回率的提升效果往往远超直接更换嵌入模型。
全文本搜索(Full-Text Search)的互补价值
Postgres 的全文本搜索(tsvector 与 tsquery)具有与向量搜索完全相反的特性:
- 向量搜索:语义理解能力强,但缺少精确字面匹配保障。
- 全文本搜索:字面匹配精度极高,但毫无语义联想能力(例如
websearch_to_tsquery('english', 'how do I stop being billed')无法直接匹配标题为 "Subscription termination" 的文档)。
因此,采用**混合搜索(Hybrid Search)**将两者结合,可以完美补齐各自的短板。
pgvector 0.8.0 核心能力剖析
pgvector 是 PostgreSQL 的扩展插件,支持在原生数据库中存储向量、计算距离及构建近似最近邻(ANN)索引。在 0.8.0 版本中,它提供了四种存储数据类型:
vector:标准的 4 字节单精度浮点数。halfvec:2 字节半精度浮点数(pgvector 0.7.0 引入)。bit:二进制量化类型。sparsevec:稀疏向量存储。
距离运算符与索引算子匹配
在编写 SQL 查询时,必须确保距离运算符与索引的算子类(Operator Class)精准对应。如果类型不匹配,Postgres 将在不抛出任何异常的情况下直接回退至全表顺序扫描:
| 运算符 | 距离类型 | 对应索引算子类(Operator Class) |
|---|---|---|
<=> | 余弦距离(Cosine Distance) | vector_cosine_ops |
<-> | 欧氏距离(L2 Distance) | vector_l2_ops |
<#> | 负内积(Negative Inner Product) | vector_ip_ops |
<+> | L1 / 曼哈顿距离 | vector_l1_ops |
<=> | 余弦距离 | vector_cosine_ops |
对于通过 n1n.ai 调用的 OpenAI text-embedding-3-small 或其他主流归一化 Embedding 模型,通常使用 <=> 运算符并搭配 vector_cosine_ops 索引。
混合搜索表结构设计
在 Postgres 中设计混合搜索数据表时,建议将文本内容、向量数据以及自动生成的全文本检索列置于同一张表中:
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE chunks (
id bigserial PRIMARY KEY,
document_id bigint NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
body text NOT NULL,
embedding vector(1536) NOT NULL,
tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', body)) STORED
);
-- 为语义向量搜索创建 HNSW 索引
CREATE INDEX chunks_embedding_hnsw
ON chunks USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 为全文关键字搜索创建 GIN 索引
CREATE INDEX chunks_tsv_gin ON chunks USING gin (tsv);
使用 GENERATED ALWAYS AS ... STORED 能够确保 tsv 列在 body 字段更新时由数据库自动维护,避免应用层数据不一致。
利用倒数排名融合(RRF)合并结果
将向量距离得分与全文检索得分直接加权相加是非常危险的做法。余弦距离通常在 0 到 2 之间浮动,而 ts_rank_cd 的输出是非有界的浮点数。直接调参往往会导致系统极不稳定。
倒数排名融合(Reciprocal Rank Fusion, RRF) 不依赖具体的分值大小,而是利用各检索分支中文档的排名顺序进行归一化评分。其计算公式如下:
其中 代表文档 在第 个检索分支中的排名位置(从 1 开始),平滑常数 通常取固定值 60。
单 SQL 语句下的 RRF 实现
通过通用表表达式(CTE),分别提取前 50 个语义候选集与前 50 个关键字候选集,再使用 LEFT JOIN 进行合并:
WITH semantic AS (
SELECT id, RANK() OVER (ORDER BY embedding <=> $1::vector) AS rank
FROM chunks
ORDER BY embedding <=> $1::vector
LIMIT 50
),
keyword AS (
SELECT c.id, RANK() OVER (ORDER BY ts_rank_cd(c.tsv, q) DESC) AS rank
FROM chunks c, websearch_to_tsquery('english', $2) q
WHERE c.tsv @@ q
ORDER BY ts_rank_cd(c.tsv, q) DESC
LIMIT 50
)
SELECT c.id, c.body,
COALESCE(1.0 / (60 + s.rank), 0.0)
+ COALESCE(1.0 / (60 + k.rank), 0.0) AS score
FROM chunks c
LEFT JOIN semantic s ON s.id = c.id
LEFT JOIN keyword k ON k.id = c.id
WHERE s.id IS NOT NULL OR k.id IS NOT NULL
ORDER BY score DESC
LIMIT 10;
关键工程细节
LEFT JOIN与COALESCE搭配:保证仅在一个分支中取得极高排名的文档依然能够脱颖而出。例如精准匹配到 SKU 的记录即使向量排名靠后,也能凭借全文检索的第一名获得极高总分。websearch_to_tsquery函数:该解析函数对用户输入的非法字符容错率高,天然支持双引号精确短语和-排除词,不会像to_tsquery那样因特殊符号引发语法报错。- 候选集规模(Candidate Pool):两个分支各取 50 条结果进行融合,最后切片输出前 10 条,能够保证 RRF 算法有足够的重叠度进行重排序。
HNSW 与 IVFFlat 索引选型策略
pgvector 提供了两种主要 Approximate Nearest Neighbor (ANN) 索引形式:
| 特性 | HNSW (Hierarchical Navigable Small World) | IVFFlat (Inverted File Flat) |
|---|---|---|
| 空表创建 | 支持 | 不支持(需要先填充代表性数据) |
| 构建参数 | m, ef_construction | lists |
| 查询控制参数 | hnsw.ef_search(默认: 40) | ivfflat.probes(默认: 1) |
| 构建速度 | 较慢 | 较快 |
| 索引体积 / 内存 | 较大 | 较小 |
| 召回率与延迟比 | 优秀(单位延迟下召回率极高) | 高并发下召回率下降明显 |
| 增量写入支持 | 优秀,动态维持平衡 | 随着写入增加性能退化,需定期 Rebuild |
结论:在绝大多数生产场景中,推荐直接选择 HNSW 索引。仅在内存资源严重受限且无法承受长索引构建时间的边缘场景下考虑 IVFFlat。
解决 HNSW 在带有 WHERE 条件时的检索空洞问题
在多租户系统或带有权限过滤(如 WHERE tenant_id = 123)的场景下,标准 HNSW 查询可能会返回少于 LIMIT 所请求的条数。原因在于 HNSW 默认先根据图结构搜集 ef_search 个候选节点,随后再应用 WHERE 过滤条件。如果目标租户的数据仅占总量的 0.1%,默认的 40 个候选节点被过滤后可能所剩无几。
pgvector 0.8.0 的突破:iterative_scan
针对该痛点,pgvector 0.8.0 引入了 hnsw.iterative_scan 参数,使图遍历过程在过滤条件满足所需数量前持续扫描:
BEGIN;
-- 在事务内开启迭代扫描
SET LOCAL hnsw.iterative_scan = 'relaxed_order';
SET LOCAL hnsw.max_scan_tuples = 20000;
SET LOCAL hnsw.ef_search = 100;
-- 执行混合搜索 SQL...
COMMIT;
'relaxed_order':以较高吞吐量返回结果,微调了精确距离顺序以提升性能。'strict_order':确保在迭代评估过程中严格按向量距离排序。hnsw.max_scan_tuples:限制单次查询扫描的最大节点上限,防止复杂过滤拖垮数据库。
生产环境性能调优指南
在将使用 Claude 3.5 Sonnet、OpenAI o3 或 DeepSeek-V3 的 RAG 系统接入 Postgres 时,优化底层数据库参数是降低整体 P95 延迟的关键所在。
1. 突破 2000 维索引限制与 halfvec 应用
标准的 pgvector 向量索引上限为 2000 维。如果使用 3072 维的高精度模型(如 text-embedding-3-large),将无法直接对 vector(3072) 建立 HNSW 索引。
解决方案是将向量存储或转换为 halfvec(3072),并配合 halfvec_cosine_ops 构建索引,该算子最高支持 4000 维,同时能降低 50% 的内存开销:
ALTER TABLE chunks ADD COLUMN embedding_half halfvec(3072);
CREATE INDEX chunks_half_hnsw ON chunks
USING hnsw (embedding_half halfvec_cosine_ops);
2. 加速索引构建速度
Postgres 默认的内存分配对于构建高维 HNSW 图索引过于保守。在对数十万级数据建索引前,应提升会话级别的内存配置:
SET maintenance_work_mem = '2GB';
SET max_parallel_maintenance_workers = 4;
3. 避免 TOAST 溢出与数据页膨胀
一个 1536 维的单精度向量约占用 6 KB 空间。Postgres 会自动将超过 2 KB 的列转存至 TOAST 独立存储区,导致每次读取都触发额外的 IO 操作。在设计数据库时,应保持 chunk 主表结构精简,将文档元数据(作者、关联文档、复杂 JSON)隔离至独立的表,仅在 RRF 排序完成后再进行主键关联(JOIN)。
4. 优化 Embedding API 网络延迟
在真实研发环境中,通过 API 生成查询向量(Embedding)的网络耗时往往占据了整个检索链路的大部分时间。在接入大型语言模型服务时,采用如 n1n.ai 这样高速稳定的 API 聚合服务,能大幅缩短检索前置延迟。
无论你的系统是基于 LangChain、LlamaIndex 还是原生 SDK 构建,通过 n1n.ai 都能极其便捷地接入各种主流模型的 Embedding API。
常见问题解答 (FAQ)
是否必须放弃 Postgres 转用专用向量数据库?
在绝大多数数据量在千万级以下的应用中完全没有必要。将向量留在 Postgres 能够享受统一的备份方案、稳定的连接池管理、ACID 事务一致性,并允许在同一条 SQL 中直接完成业务权限校验与向量检索。
使用 OpenAI 向量时应该选择哪种距离运算符?
推荐使用余弦距离运算符(<=>),并建立带有 vector_cosine_ops 的 HNSW 索引。切记运算符与索引类型必须保持一致。
混合搜索是否需要为每个分块生成两次 Embedding?
不需要。混合搜索仅需要一个 vector 列用于语义检索,以及一个由原始文本生成的 tsvector 列用于全文检索。
应该向大模型输入多少条检索结果?
通常建议从两个分支各检索 50 条候选集,经由 RRF 融合后,截取前 5 至 10 条相关度最高的内容填入 Prompt。输入过多不相关的上下文反而会导致模型的推理准确率下降。
总结 checklist
- 结合语义与字面检索:利用 RRF 融合
pgvector与tsvector的优势。 - 精准配置索引:针对归一化向量默认采用基于
vector_cosine_ops的 HNSW 索引。 - 解决过滤漏检:在带有元数据过滤条件的向量查询中开启
hnsw.iterative_scan = 'relaxed_order'。 - 降低链路延迟:搭配如 n1n.ai 的高性能 API 基础设施降低向量生成耗时。
Get a free API key at n1n.ai