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 |
in nin | 在/不在集合 | category in ['售后','物流'] |
&& || | 与 / 或 | a == 1 && b == 2 |
NOT | 取反 | NOT (status == 'draft') |
3. 方式二:FilterExpressionBuilder(类型安全)
代码里动态拼条件时更安全、不易写错:
FilterExpressionBuilder b = new FilterExpressionBuilder();
Filter.Expression expr = 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 必备)
// 每个请求都强制带上当前租户,避免串数据
String tenantId = currentTenant();
b.eq("tenantId", tenantId).build();
🔐 安全要点:多租户过滤应在服务端强制注入,绝不能让前端传、也不能漏——否则会跨租户泄露数据。
按分类 + 时间
b.and(
b.eq("category", "公告"),
b.gte("year", 2025)
).build();
动态拼接(有就加,没有不加)
FilterExpressionBuilder b = new FilterExpressionBuilder();
List<Filter.Expression> parts = new ArrayList<>();
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(new Document(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、批量灌库,让知识库有真正的内容。

121

被折叠的 条评论
为什么被折叠?



