RAG加上Metadata过滤后完全搜不到结果?从字段类型到索引完整排查

文章摘要

企业RAG为了实现租户、部门、权限、版本和有效期控制,通常会在向量检索中加入Metadata过滤。很多项目却在加过滤后出现结果从几十条变成零条,关闭过滤又恢复正常。根因可能是字段未写入、类型不一致、布尔表达式错误、日期格式不同、过滤索引缺失、权限集合为空或过滤发生在错误阶段。本文提供一套可复用的排查顺序和数据验证方法。

一、典型故障

不带过滤:

问题:公司差旅住宿标准是多少?
Top K:10条

带过滤:

tenant_id = T001
AND department_id IN [D01, D02]
AND status = EFFECTIVE

结果:

0条

很多人会立刻怀疑Embedding或向量数据库,但只要关闭过滤就能召回,说明问题通常在:

Payload数据
过滤表达式
权限上下文
索引

二、第一步:确认字段是否真的写进数据库

代码中定义了Metadata,不代表数据已经写入。

入库前打印:

{
  "chunk_id": "C001",
  "content": "一线城市住宿标准为500元",
  "metadata": {
    "tenant_id": "T001",
    "department_id": "D01",
    "status": "EFFECTIVE"
  }
}

再直接查询数据库中的Point或Row,确认实际Payload:

{
  "tenantId": "T001",
  "departmentId": "D01",
  "documentStatus": "EFFECTIVE"
}

这里已经发现命名不一致:

写入字段:tenantId
过滤字段:tenant_id

向量数据库不会自动做驼峰与下划线映射。

建议定义常量:

public final class KnowledgeMetadata {

    public static final String TENANT_ID =
            "tenant_id";

    public static final String DEPARTMENT_ID =
            "department_id";

    public static final String STATUS =
            "status";

    private KnowledgeMetadata() {
    }
}

写入与查询必须共用同一常量。

三、第二步:检查字段类型

最常见的类型错误:

写入数字1
过滤字符串"1"

或者:

写入数组["D01", "D02"]
按单值字符串比较

示例:

{
  "security_level": 3,
  "department_ids": ["D01", "D02"],
  "enabled": true
}

错误过滤:

security_level = "3"
department_ids = "D01"
enabled = "true"

正确过滤需要匹配数据库的字段类型和数组语义。

建议建立Metadata Schema:

字段 类型 示例
tenant_id string T001
department_ids string[] [D01,D02]
security_level integer 3
enabled boolean true
effective_date date或时间戳 2026-07-01

四、第三步:从单条件开始测试

不要一次排查五个AND条件。

错误做法:

tenant
AND department
AND role
AND status
AND date

正确方法:

1. 只过滤tenant_id
2. 加status
3. 加department
4. 加security_level
5. 最后加日期

每增加一个条件,记录结果数量:

条件 结果数
无过滤 100
tenant_id 60
+ status 52
+ department 18
+ date 0

这样可以立即定位问题在日期条件。

五、日期字段是高发区

常见写法混在一起:

2026-07-23
2026-07-23T00:00:00Z
1784736000
1784736000000

它们分别可能是:

  • 日期字符串;
  • ISO时间;
  • 秒级时间戳;
  • 毫秒级时间戳。

如果写入毫秒、查询使用秒,结果一定错误。

推荐统一使用:

UTC毫秒时间戳

或数据库明确支持的日期类型。

文档有效期过滤:

effective_at  now
)

注意:

expired_at = null

与:

expired_at字段不存在

在不同数据库中语义可能不同。

六、权限集合为空

业务代码:

List departments =
        permissionService
                .findAccessibleDepartments(userId);

如果权限服务异常返回空数组:

department_id IN []

通常会匹配零条。

但这可能被误以为是向量检索故障。

建议在构建过滤器前校验:

if (departments.isEmpty()) {
    throw new AccessDeniedException(
            "用户没有可访问部门"
    );
}

不要静默发起一个必定返回零条的查询。

同时区分:

没有权限
没有知识
检索故障

三种结果给用户的提示应该不同。

七、AND和OR组合错误

业务需求:

租户必须匹配
并且
部门匹配或文档为公开

正确逻辑:

tenant = T001
AND (
  department IN 用户部门
  OR visibility = PUBLIC
)

错误逻辑:

(
  tenant = T001
  AND department IN 用户部门
)
OR visibility = PUBLIC

第二种写法可能让其他租户的公开文档进入结果。

另一个错误:

tenant = T001
AND department = D01
AND department = D02

一个单值字段不可能同时等于D01和D02。

应该使用:

department IN [D01, D02]

八、数组字段的匹配语义

文档Payload:

{
  "allowed_roles": [
    "PRODUCT_MANAGER",
    "ADMIN"
  ]
}

查询用户角色:

PRODUCT_MANAGER

需要使用数组包含或Match Any语义,而不是整体数组相等。

错误:

allowed_roles = "PRODUCT_MANAGER"

正确语义:

allowed_roles包含PRODUCT_MANAGER

不同向量数据库的API不同,不能直接复制SQL表达式。

九、过滤索引没有建立

部分数据库允许不建Payload索引也能过滤,但大数据量下可能:

  • 查询非常慢;
  • 超时;
  • 资源消耗高;
  • 被上层当成空结果或失败;
  • 查询计划不稳定。

经常用于过滤的字段应建立索引:

tenant_id
department_id
status
version
effective_at
security_level

但不是所有Metadata都要建索引。

例如:

原始文件名
描述性备注
页面摘要

如果不参与过滤,不必增加索引成本。

十、数据更新后Payload没有同步

文档状态从:

DRAFT

更新为:

EFFECTIVE

如果只更新业务数据库,没有更新向量库Payload,过滤仍会排除该文档。

推荐使用Outbox或事件同步:

业务数据库更新
→ 写Outbox事件
→ 同步向量库Payload
→ 记录成功版本

每个Point保留:

source_version
metadata_version
updated_at

可以定期对账。

十一、过滤是在召回前还是召回后

召回前过滤

先限制候选集合
→ 再向量搜索

优点:

  • 权限更安全;
  • 性能通常更好;
  • 不会把无权限文档传到应用层。

召回后过滤

先搜索全部数据
→ 应用层删除无权限结果

风险:

  • Top K被无权限文档占满;
  • 删除后结果变成零条;
  • 无权限内容已经进入中间链路;
  • Trace和日志可能泄露内容。

企业RAG的权限过滤应尽量下推到数据库查询阶段。

十二、Top K与过滤后的候选不足

某些实现先取Top5,再过滤:

Top5
→ 权限过滤
→ 剩0条

即使整个库中存在50条有权限文档,也不会继续搜索。

正确方案:

  • 使用原生过滤;
  • 提高候选数量;
  • 使用迭代扫描;
  • 根据过滤选择性动态调整;
  • 检测候选不足后继续召回。

十三、多租户字段是否来自可信上下文

错误:

String tenantId =
        request.getTenantId();

用户可以伪造请求体中的租户ID。

正确:

认证Token
→ 服务端解析用户
→ 查询用户所属租户
→ 构造过滤器

过滤条件必须来自认证和权限服务,而不是模型或用户自由输入。

十四、建立过滤器调试日志

建议记录:

request_id
user_id
tenant_id
permission_snapshot_version
filter_expression
filter_hash
candidate_count
returned_count
search_duration_ms

敏感权限不应完整写入普通日志,可以记录哈希或数量。

调试环境可以输出结构化过滤器:

{
  "must": [
    {"tenant_id": "T001"},
    {"status": "EFFECTIVE"}
  ],
  "should": [
    {"department_id": "D01"},
    {"visibility": "PUBLIC"}
  ]
}

十五、自动化测试矩阵

至少覆盖:

场景 预期
同租户同部门 可见
同租户其他部门 不可见
其他租户 不可见
公开文档 按规则可见
已过期文档 不可见
草稿 不可见
null字段 按明确规则处理
字段缺失 按明确规则处理
多角色 任一匹配可见
无权限集合 明确拒绝

十六、完整排查顺序

1. 不带过滤确认有结果
2. 查看数据库真实Payload
3. 核对字段名称
4. 核对字段类型
5. 单条件逐步增加
6. 检查日期和null语义
7. 检查权限集合
8. 检查AND/OR括号
9. 检查数组匹配
10. 检查Payload索引
11. 检查Metadata同步
12. 确认过滤发生在召回前

总结

RAG加过滤后搜不到结果,最常见的根因不是向量模型,而是:

字段没写入
字段名不一致
类型不同
日期格式错误
权限集合为空
布尔表达式错误
Metadata未同步
过滤阶段错误

排查时从单条件和真实Payload开始,能够比反复调整相似度阈值更快找到问题。

延伸阅读

如果你正在关注企业级 AI 应用、RAG、Agent、MCP 与大模型工程化落地,欢迎访问 智元界

https://www.zyentor.com/

智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。