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 上踩过什么坑?评论区聊聊,说不定能帮到其他人~


相关推荐
子兮曰5 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰5 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
爱勇宝5 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
胡写代码5 天前
别再前后端各写一套表单校验了
java·后端
1点东西5 天前
做了近两年的Agent开发,其实真正要学的就是这五件事
llm·agent·ai编程
七牛云行业应用5 天前
Qwen-Image-2.1 开源部署完整指南:Diffusers、ComfyUI 与推理服务
ai编程
大勇前进5 天前
原生 PHP 还是 Laravel?小项目到底要不要上框架
后端
yuzhi_liu5 天前
我用 LangGraph4j 实现 Multi-Agent Supervisor
后端
alsmile5 天前
Node-RED 之外,国产规则引擎的新方案:基于标准语法,Go 先行实现
后端·开源·go
大白805 天前
PHP 内存溢出排查思路:看懂报错日志,精准定位问题
后端