博主在使用Spring AI的ElasticsearchVectorStore实现ES向量存储的时候,想在metadata中存一个date字段,实体代码如下:

1
2
3
4
5
6
7
8
public class KnowledgeBase {
private String id;
private String title;
private String content;
private String url;
private LocalDate date;
private Map<String, Object> metadata;
}

众所周知,Jackson序列化Java 8时间(Local家族)需要导入jackson-datatype-jsr310模块,博主也是导入了,可是还是报错,尝试了各种方法,都无果后,博主跑去仔细研读了Spring AI源码,总算是发现了罪魁祸首:

1
2
3
4
5
6
7
8
9
protected ElasticsearchVectorStore(Builder builder) {
super(builder);
Assert.notNull(builder.restClient, "RestClient must not be null");
this.initializeSchema = builder.initializeSchema;
this.options = builder.options;
this.filterExpressionConverter = builder.filterExpressionConverter;
String version = Version.VERSION == null ? "Unknown" : Version.VERSION.toString();
this.elasticsearchClient = (ElasticsearchClient)(new ElasticsearchClient(new RestClientTransport(builder.restClient, new JacksonJsonpMapper((new ObjectMapper()).configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false))))).withTransportOptions((t) -> t.addHeader("user-agent", "spring-ai elastic-java/" + version));
}

主要看ElasticsearchClient的创建这一部分,Spring AI直接new JacksonJsonpMapper(),导致博主无论怎么配置Bean都无效,那怎么办,Spring AI设计太不灵活,博主就只能”越狱“了

使用Java的”不讲武德“工具——反射!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
return new ElasticsearchVectorStore(ElasticsearchVectorStore
.builder(elasticsearchRestClient, embeddingModel)
.initializeSchema(true)
.options(vectorStoreOptions)
.batchingStrategy(batchingStrategy)) {
{
try {
// 使用反射注入替换 final 字段
Field field = ElasticsearchVectorStore.class.getDeclaredField("elasticsearchClient");
field.setAccessible(true);
field.set(this, elasticsearchClient);
} catch (Exception e) {
throw new RuntimeException("Failed to override elasticsearchClient", e);
}
}
};

直接new一个匿名的ElasticsearchVectorStore类,然后在匿名类中声明一个静态代码块强制覆盖ElasticsearchClient,直接美美解决序列化异常问题啦~

就看Spring AI团段后续会不会改成传入ElasticsearchRestClient而不是RestClient,或者其他更为灵活的方法。

目前博主这种方式是最为有效的方式了!