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/
智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。