从 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,导致元数据根本没存进去。
四条经验
- 别用 SimpleVectorStore 做生产------重启一次数据全丢
- Redis VectorStore 的 metadataFields 必须配------不配元数据存不进去,过滤必返回空
- 有问题先翻源码------别轻信社区传言,我信了"FilterExpression 空壳"的说法,结果是自己配错了
- 搜索问题加上版本号------Spring AI 迭代很快,去年的问题今年可能已经修了
你在 Spring AI RAG 上踩过什么坑?评论区聊聊,说不定能帮到其他人~