Spring AI 11 · 元数据过滤 FilterExpression
11 · 元数据过滤 FilterExpression
🎯 学完能做什么:用字符串表达式和 FilterExpressionBuilder 两种方式,按 metadata 精确圈定检索范围,实现多租户、分类、时间过滤。
⏱️ 预计耗时:40 分钟(含动手)
🔗 依赖前置:第 09、10 章 + pgvector 文档14(带过滤的向量检索)
🧩 难度:🟡
一句话:向量检索负责「找最像的」,元数据过滤负责「限定在哪些里面找」;两者结合才能做出多租户、分权限、按分类的生产级检索。
1. 为什么需要过滤
只靠相似度会检索到「全库最像的」,但业务上你往往只想在特定范围内找:
只查 租户 1001 的资料 只查 分类=售后 的资料 只查 2025 年之后更新的资料这些「范围」信息就存在灌库时写的metadata(第 09 章)里,过滤就是对 metadata 下条件。
2. 方式一:字符串表达式(直观)
List<Document>hits=vectorStore.similaritySearch(SearchRequest.builder().query("退货政策").topK(3).filterExpression("category == '售后' && tenantId == '1001'").build());支持的运算符:
| 运算符 | 含义 | 示例 |
|---|---|---|
==!= | 等于 / 不等于 | category == '售后' |
>>=<<= | 比较 | year >= 2025 |
innin | 在/不在集合 | category in ['售后','物流'] |
&&|| | 与 / 或 | a == 1 && b == 2 |
NOT | 取反 | NOT (status == 'draft') |
3. 方式二:FilterExpressionBuilder(类型安全)
代码里动态拼条件时更安全、不易写错:
FilterExpressionBuilderb=newFilterExpressionBuilder();Filter.Expressionexpr=b.and(b.eq("tenantId","1001"),b.in("category","售后","物流")).build();List<Document>hits=vectorStore.similaritySearch(SearchRequest.builder().query("退货").topK(3).filterExpression(expr).build());常用构建方法:eq / ne / gt / gte / lt / lte / in / nin / and / or / not,与字符串运算符一一对应。
4. 典型场景
多租户隔离(SaaS 必备)
// 每个请求都强制带上当前租户,避免串数据StringtenantId=currentTenant();b.eq("tenantId",tenantId).build();🔐 安全要点:多租户过滤应在服务端强制注入,绝不能让前端传、也不能漏——否则会跨租户泄露数据。
按分类 + 时间
b.and(b.eq("category","公告"),b.gte("year",2025)).build();动态拼接(有就加,没有不加)
FilterExpressionBuilderb=newFilterExpressionBuilder();List<Filter.Expression>parts=newArrayList<>();parts.add(b.eq("tenantId",tenantId).build());if(category!=null)parts.add(b.eq("category",category).build());// 用 and 逐个合并 parts ...5. 过滤 + 相似度的执行关系
similaritySearch: 1) 先按 filterExpression 圈定候选(metadata 命中的行) 2) 在候选里按向量相似度排序,取 topK / 过阈值⚠️ 性能提醒:过滤条件很「窄」(命中极少)时,纯向量索引可能出现召回不足。这在 pgvector 文档
21、22(带过滤检索的召回塌陷、迭代扫描)有系统讲解,生产必读。
6. 让过滤生效的前提:metadata 要灌对
过滤能不能用,取决于灌库时有没有写对应字段(回顾第 09 章):
// 灌库时写入可过滤字段vectorStore.add(List.of(newDocument(content,Map.of("tenantId","1001","category","售后","year",2026))));🔑 「先想清楚要按什么筛,再决定 metadata 放什么」——过滤能力是灌库阶段就注定的。
7. 常见坑
| 现象 | 原因 |
|---|---|
| 过滤后结果为空 | metadata 里根本没这个字段/值;或类型不符(数字写成字符串) |
| 过滤没生效 | 字段名拼错,或值大小写/类型不匹配 |
| 窄过滤召回很差 | 命中太少 + ANN 索引特性,见 pgvector 21/22 |
| 跨租户看到别人数据 | 服务端没强制注入租户过滤 |
8. 一句话总结
元数据过滤用filterExpression(字符串或 Builder),在相似检索前圈定范围,是多租户、分类、时间等生产场景的基础;能不能过滤取决于灌库时 metadata 写得对不对。
🎉第二阶段完成!你现在掌握了:生成向量、配置 pgvector、写入(含幂等)、相似检索、元数据过滤——RAG 的数据层已经打通。
➡️ 下一阶段(第 12–15 章)进入文档处理 ETL:把真实的 PDF/Markdown 读进来、分块、富化 metadata、批量灌库,让知识库有真正的内容。
