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 小时前
qwen-deep-research 接入实录:两阶段调用、SSE 解析、报告结构与 35 处不可追溯的引用
人工智能·github
用户9948438810701 小时前
开源了一个把老 PHP 站点自动升级到 PHP 8.5 的工具(应对 PHP2Shell 漏洞)
github
用户7273744138841 小时前
用 sha256 校验 Windows 下载文件:从 EZA Modern LS 说起
github
miofly2 小时前
Gemini 4 Argon发布,机器人ERQA推理得分87%但缺可比基准
开源·github
Apageoflove2 小时前
把 ComfyUI 改造成无人值守视频产线:状态机、顺序锁与 ComfyUI 插件工程实践
人工智能·github
CoovallyAIHub4 小时前
Anthropic 昨晚发布 Claude Duet:AI 第一次主动找你干活
github·agent·设计
逛逛GitHub5 小时前
又盘点了 4 个稀奇古怪的 GitHub 项目,第一个很抽象。
github
m4Rk_8 小时前
【论文阅读】Agent记忆机制(106):COUNTERMEM——用可执行世界模型验证“如果当时换个动作”的反事实记忆
论文阅读·人工智能·学习·开源·github
十方空烬8 小时前
开源雷达 · 2026-10-21:技能开始像软件包一样被治理
开源·github