pgvector 0.8 评测:过滤向量检索的召回修复与边界

RAG 召回率上不去,很多人的第一反应是换 embedding 模型。但在带元数据过滤的检索里,更常见的原因是:向量索引先按距离取了 40 条候选,过滤条件一过只剩 3 条,LIMIT 10 永远凑不满。问题不在模型,在索引的取数顺序。

pgvector 0.8.0 引入的迭代索引扫描(iterative index scan),正是冲着这个缺口来的。这篇从机制到配置把它拆开,并给出什么时候该用、什么时候该换库的判断标准。

问题背景:过滤条件让向量索引"提前交卷"

典型场景是 SaaS 知识库:一张 doc_chunks 表存所有租户的切片,检索时带 tenant_id 和 source 过滤。HNSW(Hierarchical Navigable Small World,分层可导航小世界图)索引的查询过程是:从入口点出发,逐层贪心靠近查询向量,最终在底层取出 hnsw.ef_search 个候选(默认 40),再交给执行器做过滤。

关键机制在于顺序:候选集在过滤之前就已经固定。 如果 tenant_id = 'a' 只占全表 1%,40 个候选里期望命中 0.4 条。返回 0 条不是 bug,是设计使然。

传统解法各有代价:调大 ef_search 会让延迟线性上升且上限只有 1000;给每个租户建 partial index 会在租户数上百后索引爆炸、写入放大;把过滤条件下推成 SQL 子查询再排序,则退化为全表扫描。pgvector 0.8.0 给的是第四条路。

核心概念:迭代扫描把"取一次"改成"取到够为止"

迭代索引扫描的定义很直白:当过滤后结果不足 LIMIT 时,索引继续向后扫描,直到凑够行数或触达安全上限。它由三个 GUC 参数控制:

  • hnsw.iterative_scan:off(默认)/ relaxed_order / strict_order
  • hnsw.max_scan_tuples:单次查询最多扫描的索引元组数,默认 20000
  • hnsw.scan_mem_multiplier:扫描内存相对 work_mem 的倍数,默认 1

为什么这样设计: 无限扫描会让一个高选择性过滤的查询拖垮实例,所以必须有一个可预算的上限;而"是否保证严格按距离排序"是正确性与延迟的取舍,交给使用方显式声明,而不是隐式决定。

适用边界也很清楚:它修复的是"过滤后不足"的问题,不解决"过滤条件本身无法走索引"的问题。若过滤列没有 B-tree 索引,扫描仍会退化成逐行回表。

关键机制:relaxed_order 与 strict_order 的取舍

relaxed_order 允许返回结果不完全按距离升序------因为迭代过程中新扫到的行可能比已返回的更近。它更快,但任何基于分数阈值的截断都会失真 :你可能用第 10 条的分数做 score < 0.3 过滤,而真正最近的第 3 条还在后面没扫到。

strict_order 保证顺序正确,代价是可能需要扫描更多元组、占用更多内存。工程上的判断很直接:

  • 只取 Top-K 做 RAG 上下文拼接 → relaxed_order 足够
  • 要做分数阈值截断、去重合并、或多路召回后重排 → 必须 strict_order

配置示例(会话级生效,便于按接口区分):

sql 复制代码
-- 建表与索引:1536 维对应 text-embedding-3-small
CREATE TABLE doc_chunks (
  id        bigserial PRIMARY KEY,
  tenant_id text NOT NULL,
  source    text,
  content   text,
  embedding vector(1536)
);

-- 过滤列必须有 B-tree 索引,否则迭代扫描仍要回表逐行判断
CREATE INDEX ON doc_chunks (tenant_id, source);

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

-- 关键:开启迭代扫描,并给扫描量一个预算
SET hnsw.iterative_scan = strict_order;
SET hnsw.max_scan_tuples = 40000;   -- 约为默认值的 2 倍,按 P95 延迟调
SET hnsw.ef_search = 100;

SELECT id, content, embedding <=> $1 AS distance
FROM doc_chunks
WHERE tenant_id = 'tenant_a' AND source = 'faq'
ORDER BY embedding <=> $1
LIMIT 10;

LangChain 侧接入时有一个容易忽略的边界:langchain-postgres 的 PGVector 把过滤条件放在 JSONB metadata 上,而不是业务表的独立列。若 tenant_id 是独立列,要么把它冗余进 metadata,要么绕开封装直接写 SQL。

python 复制代码
# 示例:langchain-postgres + OpenAI 兼容接口,版本以实际安装为准
from langchain_postgres import PGVector
from langchain_openai import OpenAIEmbeddings

emb = OpenAIEmbeddings(
    model="text-embedding-3-small",
    base_url="https://api.example.com/v1",  # 兼容接口
    api_key
相关推荐
黎羽_1 天前
《开源 PR 实战》系列(三):22 个 PR 之后,批量提 PR 的打法
开源·github·代码评审·pull request·协作
fthux1 天前
总想着“忙完再休息”?让 Tick Rest 提醒你,先歇一会儿
开源·github·mac·swift·提醒·消息提醒·tick rest·tick rest for mac·消息提醒app
dong_junshuai1 天前
每天一个开源项目#156 Astryx:主题编译、异步按钮与源码接管
github
逛逛GitHub1 天前
把你的 AirPods 当鼠标用,这 3 个黑科技 GitHub 开源项目有趣。
github
徐小夕2 天前
开源Vector-Graph-RAG:向量+图谱双引擎,打破纯向量RAG的效果天花板
前端·vue.js·github
GitHub实测笔记2 天前
zoxide 本机实测:一万条记查询中 5.7ms0.10 版 import --from
github
Brookty2 天前
Git与GitHub系统学习|本地Git工作模型:终端与进程、CWD、HEAD/Index/工作树及Git对象
git·github
miofly2 天前
Genie 4新增原生音频,实时世界模型加入声音生成
开源·github
dong_junshuai2 天前
每天一个开源项目#155 MuJoCo:从接触求解到控制时序的物理引擎
github
GitHub实测笔记2 天前
eza 本机实测:一万文件 -l 中位 48ms、ls 21.5ms;脚本里裸跑 eza 输出为空
github