#RAG#DevOps

RAG 的元数据过滤检索

生产 RAG 几乎都要"带条件的检索"——只在我部门/某时间段/某产品里搜。纯向量召回做不到精确过滤,要靠元数据过滤。本文讲清元数据过滤的价值、怎么给 chunk 打元数据、向量库的过滤能力(pgvector SQL/Qdrant payload)、过滤优先于检索的性能收益,以及权限隔离也靠它。

本文要点

  • 主题聚焦:RAG、DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2025年11月7日·更新于 2026年7月23日

如果你的 RAG demo 只跑过 5 个 PDF,大概永远不会遇到元数据过滤的问题。一旦文档量上去——几十个部门、上千个产品手册、跨三年的工单、内外网权限分开——你会发现生产环境的检索请求 80% 都是"带条件的":只在我部门里搜、只查最近一个月、只看这个产品的文档、只能看到我有权限的部分。

这些条件,纯向量召回一个都做不到。向量只能告诉你"语义上像不像",告诉不了你"这条记录是不是财务部的、是不是还没过期、用户有没有权限看"。这一层精确性,得靠**元数据过滤(metadata filtering)**补上。它是生产 RAG 和 demo RAG 之间最硬的一道分水岭。

一、为什么纯向量召回不够用

naive RAG 的链路是 query embedding → 全库 top-k 余弦召回。它假设"最相关的就是最该回答的",但生产里"相关"前面永远站着一堆限定词:

  • 时间窗口:查"退款政策",最相似的可能是三年前那版——但已经作废了。向量召回照给不误,模型据此一本正经地答了一堆过时规则。
  • 产品/业务线:查"如何导出报表",客服产品、财务产品、BI 产品里都有"导出报表"的文档,语义上个个都像。用户只关心他自己用的那个。
  • 部门/租户:HR 知识库和法务知识库混在一个索引里,跨部门查询直接串台。
  • 权限:外部合作方能看的内部文档,绝对不能进 prompt。这关系到合规,不是"答案准不准"的小事。

纯向量召回对这四类约束毫无表达能力。要么你在 prompt 里求模型"请只参考 XXX 时间内的内容"——它根本做不到,你给它什么它用什么;要么你把过滤下沉到检索层,让模型压根看不到不合规的 chunk。后者才是生产级的做法。

一句话:embedding 负责找"像不像",元数据负责卡"能不能用"。两条腿,缺一条都站不稳。

二、给 chunk 打元数据:从入库就规划好

元数据过滤的成败,在 chunk 入库那一刻就决定了。检索时能怎么过滤,完全取决于你打标时存了什么。embedding 是血肉,元数据是骨架——骨架没搭好,血肉再准也立不起来。

每个 chunk 落库时,至少该带上这几类元数据:

字段作用典型值
source来源溯源confluence, freshdesk, gitbook
department / tenant部门/租户隔离finance, hr, customer_id:42
product / category业务线/分类billing, auth, mobile-app
doc_type文档类型policy, runbook, faq, changelog
created_at / updated_at时效性ISO 时间戳,必须可比较
version / status版本与状态v2.1, draft, published, deprecated
visibility / acl权限标签public, internal, confidential, [g:finance-staff]

两个实战要点:

第一,chunk 级元数据 = 文档级继承 + chunk 自身属性。文档属于哪个部门、哪个产品,从父文档继承下来;但章节标题、是否是"已知问题/历史版本",是 chunk 自己的。入库管线里把这两层合并,别只存文档级。

第二,时间字段必须存成可比较的类型,不要塞字符串。Qdrant payload 里存成 datetime,pgvector 里用 timestamptz 或可比较的字符串格式。我见过把"2024 年 3 月"原样存进去的,后面范围过滤根本没法做,只能回去重灌。

三、向量库的过滤能力:pgvector / Qdrant / Milvus

主流向量库都支持过滤,但表达力差很多。分三档看。

pgvector:直接 SQL,最强表达力。 因为它就是 Postgres,过滤就是 WHERE,你可以写任意复杂的 SQL:范围、组合、JOIN 业务表、聚合后过滤,全能。

sql
-- 带元数据过滤的向量检索(pgvector)
SELECT id, content, metadata,
       embedding <=> $1::vector AS distance  -- 余弦距离
FROM chunks
WHERE department = 'finance'
  AND product   = 'billing'
  AND status    = 'published'
  AND updated_at >= '2024-01-01'
  AND visibility = ANY($2::text[])           -- 用户可见性集合
ORDER BY embedding <=> $1::vector
LIMIT 10;

这段 SQL 的意思是:在财务部 billing 产品的、已发布的、2024 年以后的、用户有权限看的 chunk 里,找语义最相关的 10 条。过滤和向量检索写在同一条语句里,由查询计划器统一优化,这正是 pgvector 的杀手锏。

Qdrant:payload 过滤,语义化 API。 Qdrant 给每个点带一个 payload(JSON),过滤用 Filter(must / must_not / should) 表达,接近 Elasticsearch 的 DSL:

python
from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue, Range

client = QdrantClient(url="http://localhost:6333")

res = client.query_points(
    collection_name="docs",
    query=query_vector,
    query_filter=Filter(
        must=[
            FieldCondition(key="department", match=MatchValue(value="finance")),
            FieldCondition(key="product",    match=MatchValue(value="billing")),
            FieldCondition(key="status",     match=MatchValue(value="published")),
            FieldCondition(key="updated_at",
                           range=Range(gte="2024-01-01T00:00:00Z")),
        ],
        must_not=[
            FieldCondition(key="status", match=MatchValue(value="deprecated")),
        ],
    ),
    limit=10,
)

must 是 AND,should 是 OR,must_not 是排除,组合起来覆盖绝大多数场景。配合 payload index(对常过滤字段建索引),Qdrant 的过滤性能很强,这也是它在中大规模场景的拿手好戏。

Milvus:标量字段 + 表达式。 Milvus 用标量字段存元数据,过滤用类 SQL 的 expr 字符串:expr='department == "finance" and product == "billing" and updated_at >= 1704067200'。表达力介于 pgvector 和 Qdrant 之间,胜在海量分布式。

选型上一句话:要最强过滤表达力和"和业务数据同库"选 pgvector;要中大规模低延迟+DSL 体验选 Qdrant;亿级分布式再上 Milvus。

四、过滤优先:先缩小集合,再做向量检索

过滤的实现策略有两种,生产里要分清:

  • post-filter(后过滤):先在全库做向量 top-k,再用元数据筛掉不符合的。问题大——top-10 召回可能有 9 条不满足条件,最后只剩 1 条,既不准又不够数。想补就只能 overfetch(召回 top-100 再过滤),延迟和成本都上去了。
  • pre-filter(先过滤):先用元数据把候选集缩小到满足条件的子集,再在这个子集里做向量检索。又准又快,没有 overfetch 的空耗。

生产几乎无脑选 pre-filter。 pgvector 天然就是 pre-filter——Postgres 执行 SQL 时 WHERE 在 ORDER BY 之前,过滤先于向量排序发生,查询计划器会自动用 B-tree 索引把集合砍小,再做向量比较。Qdrant 也是 pre-filter 设计,过滤用 payload index 加速后,再进入向量索引。这种"先缩小再检索"的执行顺序,是过滤能带来性能收益的根本原因。

五、性能收益:集合越小,越快越准

"加了过滤会不会变慢?"——恰恰相反,过滤后通常更快。

道理很直接:向量检索的计算量正比于候选集大小。全库 1000 万条做 ANN,和过滤后 5 万条做精确扫描,后者常常更快——尤其当过滤选择性高(砍掉 99% 数据)时,pgvector 会跳过 HNSW 索引,直接在过滤后的子集上做精确 kNN,反而既快又精确。Qdrant 对高选择性过滤同样有专门优化。

更关键的是去噪带来的答案质量提升。top-k 里少几条跨部门/过期/无关的噪声 chunk,模型就少几分胡说八道的可能。我见过一个案例:同样的问题,加了时间+产品过滤后,准确率从 62% 升到 88%,召回延迟还降了 40%——因为没过滤时 ANN 在大集合里兜圈子,过滤后直奔目标子集。

过滤不是检索的负担,是检索的加速器和降噪器。

六、权限隔离:把安全边界下沉到检索层

权限隔离是元数据过滤最严肃的应用,这不是优化,是合规红线。

错误做法:全库召回,然后"让模型别引用没权限的内容",或者召回后再在应用层裁剪。前者完全不可控(模型已经看到了机密内容,prompt 注入就能套出来),后者有泄漏风险(裁剪逻辑一旦有 bug 就穿帮)。

正确做法:把权限标签变成检索的硬过滤条件,强制在检索层隔离。 用户的可见性集合(visibility / acl)作为 must / WHERE 条件注入,从源头就让机密 chunk 不在候选集里,模型根本看不到。

python
# 把用户权限作为强制过滤条件,硬注入
user_visible = resolve_acl(user)  # e.g. ["public", "internal", "g:finance-staff"]
query_filter=Filter(must=[
    FieldCondition(key="acl", match=MatchAny(any=user_visible)),
    # ...其余业务过滤
])

这样即使 LLM 被 prompt 注入,它也无法"召唤"出权限外的内容——因为检索器压根没返回。安全边界要建在能被强制的最底层,而不是建在"模型承诺不看"的礼貌之上。

七、实战:多条件组合与索引优化

落地时,索引优化决定过滤检索的真实延迟。几个关键点:

pgvector 侧:对高频过滤字段建 B-tree 索引(独立列)或表达式索引(JSONB)。复合过滤注意索引列顺序——等值条件在前,范围在后,和传统 SQL 调优一致。向量索引用 HNSW 配合 iterative_filter,在大集合高选择性过滤下表现稳定。

sql
-- 高频过滤字段的复合索引(部门 + 产品 + 状态)
CREATE INDEX idx_chunks_filter ON chunks (department, product, status);
-- 时间字段单独索引,支持范围扫描
CREATE INDEX idx_chunks_updated ON chunks (updated_at DESC);

把最稳定、选择性最高的过滤字段从 JSONB 拆成独立列,索引和统计信息都更准——这是 pgvector 用到中期最值钱的优化。

Qdrant 侧:对常过滤的 payload 字段建 payload index(keyword / integer / datetime)。must 里的高选择性条件放前面,帮助引擎快速缩小集合。多条件组合时,优先过滤掉数据量最大的那个条件(比如先按 tenant 再按 product),剪枝效率最高。

避免过度过滤:组合条件太多可能把候选集砍到接近 0,导致 top-k 永远凑不齐。给关键条件留"宽进严排"的余地——硬条件(权限、租户)必过滤,软条件(时间、相关度)可在 rerank 阶段加权,而不是在召回层一刀切死。

小结

  • 元数据过滤是生产 RAG 的必需品,不是锦上添花。生产检索 80% 是带条件的,纯向量召回表达不了时间/产品/权限这类精确约束。
  • 过滤的成败在入库时决定:每个 chunk 都要带齐来源、部门、产品、时间、权限、状态;时间字段务必存成可比较类型。
  • 三家向量库各有拿法:pgvector 直接 SQL 最强表达力,Qdrant payload + Filter 性能稳健,Milvus 标量 + expr 胜在海量分布式。
  • 过滤优先(pre-filter)是生产默认:先缩小集合再检索,又快又准,顺带把噪声也去掉了——过滤是加速器,不是负担。
  • 权限隔离必须下沉到检索层,用硬过滤让机密 chunk 从源头不在候选集里,别指望模型自觉。

金句:embedding 解决"像不像",元数据解决"能不能用";前者决定回答得准不准,后者决定系统能不能上线。

Related

相关推荐