Spring AI RAG踩坑:我骂了半年的FilterExpression,其实是背锅侠

从 SimpleVectorStore 切到 Redis VectorStore,metadata 过滤直接罢工。我以为是 Spring AI 的 FilterExpression 是空壳,翻完源码才发现------是我自己少配了一个字段。


背景:为什么需要 metadata 过滤

我在做一个基于 Spring AI 的简历优化应用,核心是 RAG 检索。用户上传简历后,AI 根据岗位 JD 生成优化建议。

问题来了------不同行业、不同岗位、不同经验级别的简历,优化侧重点完全不同。10 年经验的技术总监不能参考应届生的模板。

所以检索逻辑需要精准过滤:

less 复制代码
FilterExpressionBuilder builder = new FilterExpressionBuilder();
FilterExpression filter = builder.and(
    builder.eq("industry", "互联网"),
    builder.eq("experienceLevel", "中级")
).build();
​
List<Document> results = vectorStore.similaritySearch(
    SearchRequest.builder()
        .query("Java开发简历模板")
        .topK(5)
        .filterExpression(filter)
        .build()
);

逻辑没问题,接下来就是选 VectorStore 了。


SimpleVectorStore:开局很顺

一开始用的 SimpleVectorStore,内存版,配置极简:

typescript 复制代码
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
    return new SimpleVectorStore(embeddingModel);
}

三行代码搞定。然后 metadata 过滤完美运行------按行业过滤没问题、按经验级别过滤没问题、组合过滤也没问题。

我当时觉得 Spring AI 这波稳了。


切到 Redis VectorStore:过滤直接废了

数据量上来之后,内存版重启丢数据的问题不能忍,必须换持久化存储。

改配置:

scss 复制代码
@Bean
public VectorStore vectorStore(JedisPooled jedis, EmbeddingModel embeddingModel) {
    return RedisVectorStore.builder()
        .jedisPooled(jedis)
        .embeddingModel(embeddingModel)
        .build();
}

看起来比 SimpleVectorStore 就多两个参数,没什么难度。

结果一试检索------返回空。

Redis 里有数据,FT.SEARCH 也能搜到。但只要带上 FilterExpression,结果就是空的。


误判:我以为是 FilterExpression 的锅

搜了一圈,发现社区里确实有人吐槽 Spring AI 的 FilterExpression 不好用,说底层实现有问题。

我信了。

在代码里加了个注释:// TODO: FilterExpression在Redis VectorStore里疑似空壳,等官方修复

上一篇文章里也暗示了这一点。

但这个判断是错的。


翻源码:真相让人尴尬

有天闲下来翻了 Spring AI 的源码,看完就傻了。

SimpleVectorStore 的 FilterExpression 实现,完整得不能再完整。 底层遍历文档、解析表达式、逐条匹配,逻辑清晰,根本不是空壳。

然后翻 Redis VectorStore 的 doSave 方法,发现了关键:

typescript 复制代码
// RedisVectorStore源码(简化)
public void doSave(List<Document> documents) {
    if (this.metadataFields != null) {
        // 只有配了metadataFields,metadata才会被提取并存储
        for (Document doc : documents) {
            Map<String, Object> meta = extractMetadata(doc, this.metadataFields);
            // 存到Redis...
        }
    }
}

metadataFields 没配置 → metadata 存不进去 → 过滤时匹配不上 → 返回空。

这不是 FilterExpression 的锅,是 Redis VectorStore 少配了 metadataFields。


正解:Redis VectorStore 配置三件套

翻文档、反复测试,终于搞清楚完整配置。三件套一个都不能少:

scss 复制代码
@Bean
public VectorStore vectorStore(JedisPooled jedis, EmbeddingModel embeddingModel) {
    return RedisVectorStore.builder()
        .jedisPooled(jedis)
        .embeddingModel(embeddingModel)
        .initializeSchema(true)              // ① 自动创建索引
        .indexName("resume-ai")              // ② 索引名
        .prefix("doc:")                      // ③ key前缀
        .metadataFields(                     // ④ 元数据字段schema(关键!)
            MetadataField.text("experienceLevel"),
            MetadataField.text("industry"),
            MetadataField.text("position")
        )
        .build();
}

逐个说明:

配置项 作用 踩坑点
initializeSchema(true) 自动创建 Redis 搜索索引 手动建可能格式不对
indexName + prefix 索引名和 key 前缀,隔离不同项目 不设会跟其他数据混
metadataFields 告诉 Redis 哪些是元数据字段 不配就存不进去,过滤必返回空!

重点说 metadataFields。它的本质是一个 schema,告诉 Redis:"这些文档里有 experienceLevel、industry、position 字段,过滤时靠它们。"

没配这个,Redis 不知道你存了什么字段,FilterExpression 再怎么写也是白搭。


验证:配齐之后

配齐三件套,重新写入数据,再跑过滤:

less 复制代码
FilterExpressionBuilder builder = new FilterExpressionBuilder();
FilterExpression filter = builder.and(
    builder.eq("industry", "互联网"),
    builder.eq("experienceLevel", "中级")
).build();
​
List<Document> results = vectorStore.similaritySearch(
    SearchRequest.builder()
        .query("Java开发简历模板")
        .topK(5)
        .filterExpression(filter)
        .build()
);
// ✅ 结果正常返回

对比:SimpleVectorStore vs Redis VectorStore

特性 SimpleVectorStore Redis VectorStore
存储 内存,重启丢失 Redis 持久化
FilterExpression ✅ 完整实现 ✅ 完整实现
metadata 过滤 开箱即用 需配 metadataFields
生产可用 ❌ ✅
配置复杂度 零配置 三件套必须配齐

结论:两个 VectorStore 的 FilterExpression 都不是空壳。 是我在切 Redis 的时候少配了 metadataFields,导致元数据根本没存进去。


四条经验

  1. 别用 SimpleVectorStore 做生产------重启一次数据全丢
  2. Redis VectorStore 的 metadataFields 必须配------不配元数据存不进去,过滤必返回空
  3. 有问题先翻源码------别轻信社区传言,我信了"FilterExpression 空壳"的说法,结果是自己配错了
  4. 搜索问题加上版本号------Spring AI 迭代很快,去年的问题今年可能已经修了

你在 Spring AI RAG 上踩过什么坑?评论区聊聊,说不定能帮到其他人~


相关推荐
脚本自动化开发3 小时前
折叠双屏MU156LA:写稿修图游戏体验
后端
脚本自动化开发4 小时前
AI短剧800集讲规矩,观众忘了是AI
后端
小虎AI生活5 小时前
WorkBuddy 积分不够烧?我找到了每天白送 5000 的平台
aigc·ai编程
Csvn6 小时前
安装、升级与目录结构
后端·nginx
皮皮林5516 小时前
万能通用!权限系统就该这么设计
后端
GoAI7 小时前
DeepSeek Harness Memory (一)主流记忆插件总结
后端·算法·面试
卸任8 小时前
Vibe coding工具使用心得
前端·ai编程
橙子家8 小时前
RESTful 接口:它并非标准或协议,而是一种【架构风格】
后端
T_Wizard8 小时前
同一个问题写了 4 遍:官方 SDK / HTTP / Spring AI / LangChain4j,到底该选哪个
java·ai编程
小兔子9 小时前
dict.get 的默认值每次都会求值,哪怕键本来就在
后端