Refine Jackson ObjectMapper handling

ObjectMapper instantiation is costly, so unless its usage
is one-shot, it is better to create a reusable instance
for upcoming usage.

Also, before this commit, serialization of most Kotlin
classes was not supported due to the lack of proper
Jackson KotlinModule detection.

This commit:
 - Avoids per invocation ObjectMapper instantiation when
   relevant
 - Automatically detects and enables well-known Jackson
   modules including the Kotlin one
 - Removes org.springframework.ai.vectorstore.JsonUtils
   which looks not needed anymore

More optimizations are possible like reusing more
ObjectMapper instances, but this could introduce more breaking
changes so this commit intends to be a good first step.

Kotlin tests will be provided in a follow-up commit.

Additional changes:
 - Update ModelOptionsUtils to use JacksonUtils.instantiateAvailableModules()
 - Add missing license headers
 - Add missing author Javadoc comments
This commit is contained in:
Sébastien Deleuze
2024-10-23 14:44:18 +02:00
committed by Christian Tzolov
parent 9f6ca77bbc
commit f21b8a42b5
26 changed files with 244 additions and 155 deletions

View File

@@ -10,7 +10,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.azure.openai.metadata.AzureOpenAiImageGenerationMetadata;
@@ -22,6 +22,7 @@ import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.ai.image.ImageResponseMetadata;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.util.JacksonUtils;
import org.springframework.util.Assert;
import java.util.List;
@@ -33,6 +34,7 @@ import static java.lang.String.format;
* {@link OpenAIClient}.
*
* @author Benoit Moussaud
* @author Sebastien Deleuze
* @see ImageModel
* @see com.azure.ai.openai.OpenAIClient
* @since 1.0.0
@@ -47,6 +49,8 @@ public class AzureOpenAiImageModel implements ImageModel {
private final AzureOpenAiImageOptions defaultOptions;
private final ObjectMapper objectMapper;
public AzureOpenAiImageModel(OpenAIClient openAIClient) {
this(openAIClient, AzureOpenAiImageOptions.builder().withDeploymentName(DEFAULT_DEPLOYMENT_NAME).build());
}
@@ -56,6 +60,11 @@ public class AzureOpenAiImageModel implements ImageModel {
Assert.notNull(options, "AzureOpenAiChatOptions must not be null");
this.openAIClient = microsoftOpenAiClient;
this.defaultOptions = options;
this.objectMapper = JsonMapper.builder()
.addModules(JacksonUtils.instantiateAvailableModules())
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.build();
}
public AzureOpenAiImageOptions getDefaultOptions() {
@@ -88,11 +97,8 @@ public class AzureOpenAiImageModel implements ImageModel {
}
private String toPrettyJson(Object object) {
ObjectMapper objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.registerModule(new JavaTimeModule());
try {
return objectMapper.writeValueAsString(object);
return this.objectMapper.writeValueAsString(object);
}
catch (JsonProcessingException e) {
return "JsonProcessingException:" + e + " [" + object.toString() + "]";

View File

@@ -25,7 +25,9 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.springframework.ai.util.JacksonUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
@@ -116,6 +118,8 @@ public class VertexAiPaLm2Api {
private final String embeddingModel;
private final ObjectMapper objectMapper;
/**
* Create a new chat completion api.
* @param apiKey vertex apiKey.
@@ -138,6 +142,7 @@ public class VertexAiPaLm2Api {
this.chatModel = model;
this.embeddingModel = embeddingModel;
this.apiKey = apiKey;
this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build();
Consumer<HttpHeaders> jsonContentHeaders = headers -> {
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
@@ -154,7 +159,7 @@ public class VertexAiPaLm2Api {
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getStatusCode().isError()) {
throw new RuntimeException(String.format("%s - %s", response.getStatusCode().value(),
new ObjectMapper().readValue(response.getBody(), ResponseError.class)));
objectMapper.readValue(response.getBody(), ResponseError.class)));
}
}
};

View File

@@ -21,8 +21,11 @@ import static com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_
import java.lang.reflect.Type;
import java.util.Objects;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.util.JacksonUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.lang.NonNull;
@@ -34,7 +37,6 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.github.victools.jsonschema.generator.Option;
import com.github.victools.jsonschema.generator.SchemaGenerator;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
@@ -54,6 +56,7 @@ import com.github.victools.jsonschema.module.jackson.JacksonOption;
* @author Sebastian Ullrich
* @author Kirk Lund
* @author Josh Long
* @author Sebastien Deleuze
*/
public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
@@ -65,12 +68,10 @@ public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
/**
* The target class type reference to which the output will be converted.
*/
@SuppressWarnings({ "FieldMayBeFinal" })
private TypeReference<T> typeRef;
private final TypeReference<T> typeRef;
/** The object mapper used for deserialization and other JSON operations. */
@SuppressWarnings("FieldMayBeFinal")
private ObjectMapper objectMapper;
private final ObjectMapper objectMapper;
/**
* Constructor to initialize with the target type's class.
@@ -149,7 +150,7 @@ public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
SchemaGeneratorConfig config = configBuilder.build();
SchemaGenerator generator = new SchemaGenerator(config);
JsonNode jsonNode = generator.generateSchema(this.typeRef.getType());
ObjectWriter objectWriter = new ObjectMapper().writer(new DefaultPrettyPrinter()
ObjectWriter objectWriter = this.objectMapper.writer(new DefaultPrettyPrinter()
.withObjectIndenter(new DefaultIndenter().withLinefeed(System.lineSeparator())));
try {
this.jsonSchema = objectWriter.writeValueAsString(jsonNode);
@@ -201,10 +202,10 @@ public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
* @return Configured object mapper.
*/
protected ObjectMapper getObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.registerModule(new JavaTimeModule());
return mapper;
return JsonMapper.builder()
.addModules(JacksonUtils.instantiateAvailableModules())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.build();
}
/**

View File

@@ -26,6 +26,13 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.springframework.ai.util.JacksonUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
@@ -33,9 +40,9 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.github.victools.jsonschema.generator.Option;
import com.github.victools.jsonschema.generator.OptionPreset;
import com.github.victools.jsonschema.generator.SchemaGenerator;
@@ -46,12 +53,6 @@ import com.github.victools.jsonschema.module.jackson.JacksonModule;
import com.github.victools.jsonschema.module.jackson.JacksonOption;
import com.github.victools.jsonschema.module.swagger2.Swagger2Module;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
/**
* Utility class for manipulating {@link ModelOptions} objects.
*
@@ -61,10 +62,11 @@ import org.springframework.util.ObjectUtils;
*/
public abstract class ModelOptionsUtils {
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
public static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.registerModule(new JavaTimeModule());
.addModules(JacksonUtils.instantiateAvailableModules())
.build();
private static final List<String> BEAN_MERGE_FIELD_EXCISIONS = List.of("class");

View File

@@ -21,18 +21,22 @@ import java.util.function.Function;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.model.function.FunctionCallbackContext.SchemaType;
import org.springframework.ai.util.JacksonUtils;
import org.springframework.util.Assert;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.databind.json.JsonMapper;
/**
* Note that the underlying function is responsible for converting the output into format
* that can be consumed by the Model. The default implementation converts the output into
* String before sending it to the Model. Provide a custom function responseConverter
* implementation to override this.
*
* @author Christian Tzolov
* @author Sebastien Deleuze
*
*/
public class FunctionCallbackWrapper<I, O> extends AbstractFunctionCallback<I, O> {
@@ -90,10 +94,7 @@ public class FunctionCallbackWrapper<I, O> extends AbstractFunctionCallback<I, O
private String inputTypeSchema;
private ObjectMapper objectMapper = new ObjectMapper()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.registerModule(new JavaTimeModule());
private ObjectMapper objectMapper;
public Builder<I, O> withName(String name) {
Assert.hasText(name, "Name must not be empty");
@@ -142,7 +143,14 @@ public class FunctionCallbackWrapper<I, O> extends AbstractFunctionCallback<I, O
Assert.hasText(this.name, "Name must not be empty");
Assert.hasText(this.description, "Description must not be empty");
Assert.notNull(this.responseConverter, "ResponseConverter must not be null");
Assert.notNull(this.objectMapper, "ObjectMapper must not be null");
if (this.objectMapper == null) {
this.objectMapper = JsonMapper.builder()
.addModules(JacksonUtils.instantiateAvailableModules())
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.build();
}
if (this.inputType == null) {
if (this.function != null) {

View File

@@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.github.victools.jsonschema.generator.SchemaGenerator;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
@@ -30,6 +31,8 @@ import com.github.victools.jsonschema.module.jackson.JacksonModule;
import java.util.Map;
import java.util.Objects;
import org.springframework.ai.util.JacksonUtils;
import static com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON;
import static com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12;
@@ -91,7 +94,7 @@ public class BeanOutputParser<T> implements OutputParser<T> {
SchemaGeneratorConfig config = configBuilder.build();
SchemaGenerator generator = new SchemaGenerator(config);
JsonNode jsonNode = generator.generateSchema(this.clazz);
ObjectWriter objectWriter = new ObjectMapper().writer(new DefaultPrettyPrinter()
ObjectWriter objectWriter = this.objectMapper.writer(new DefaultPrettyPrinter()
.withObjectIndenter(new DefaultIndenter().withLinefeed(System.lineSeparator())));
try {
this.jsonSchema = objectWriter.writeValueAsString(jsonNode);
@@ -142,9 +145,10 @@ public class BeanOutputParser<T> implements OutputParser<T> {
* @return Configured object mapper.
*/
protected ObjectMapper getObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper;
return JsonMapper.builder()
.addModules(JacksonUtils.instantiateAvailableModules())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.build();
}
/**

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2024 - 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.util;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.databind.Module;
import org.springframework.beans.BeanUtils;
import org.springframework.core.KotlinDetector;
import org.springframework.util.ClassUtils;
/**
* Utility methods for Jackson.
*
* @author Sebastien Deleuze
*/
public abstract class JacksonUtils {
/**
* Instantiate well-known Jackson modules available in the classpath.
* <p>
* Supports the follow-modules: <code>Jdk8Module</code>, <code>JavaTimeModule</code>,
* <code>ParameterNamesModule</code> and <code>KotlinModule</code>.
* @return The list of instantiated modules.
*/
@SuppressWarnings("unchecked")
public static List<Module> instantiateAvailableModules() {
List<Module> modules = new ArrayList<>();
try {
Class<? extends com.fasterxml.jackson.databind.Module> jdk8ModuleClass = (Class<? extends Module>) ClassUtils
.forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", null);
com.fasterxml.jackson.databind.Module jdk8Module = BeanUtils.instantiateClass(jdk8ModuleClass);
modules.add(jdk8Module);
}
catch (ClassNotFoundException ex) {
// jackson-datatype-jdk8 not available
}
try {
Class<? extends com.fasterxml.jackson.databind.Module> javaTimeModuleClass = (Class<? extends Module>) ClassUtils
.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", null);
com.fasterxml.jackson.databind.Module javaTimeModule = BeanUtils.instantiateClass(javaTimeModuleClass);
modules.add(javaTimeModule);
}
catch (ClassNotFoundException ex) {
// jackson-datatype-jsr310 not available
}
try {
Class<? extends com.fasterxml.jackson.databind.Module> parameterNamesModuleClass = (Class<? extends Module>) ClassUtils
.forName("com.fasterxml.jackson.module.paramnames.ParameterNamesModule", null);
com.fasterxml.jackson.databind.Module parameterNamesModule = BeanUtils
.instantiateClass(parameterNamesModuleClass);
modules.add(parameterNamesModule);
}
catch (ClassNotFoundException ex) {
// jackson-module-parameter-names not available
}
// Kotlin present?
if (KotlinDetector.isKotlinPresent()) {
try {
Class<? extends com.fasterxml.jackson.databind.Module> kotlinModuleClass = (Class<? extends Module>) ClassUtils
.forName("com.fasterxml.jackson.module.kotlin.KotlinModule", null);
Module kotlinModule = BeanUtils.instantiateClass(kotlinModuleClass);
modules.add(kotlinModule);
}
catch (ClassNotFoundException ex) {
// jackson-module-kotlin not available
}
}
return modules;
}
}

View File

@@ -32,12 +32,14 @@ import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.observation.conventions.VectorStoreProvider;
import org.springframework.ai.observation.conventions.VectorStoreSimilarityMetric;
import org.springframework.ai.util.JacksonUtils;
import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention;
@@ -64,11 +66,14 @@ import io.micrometer.observation.ObservationRegistry;
* @author Dingmeng Xue
* @author Mark Pollack
* @author Christian Tzolov
* @author Sebastien Deleuze
*/
public class SimpleVectorStore extends AbstractObservationVectorStore {
private static final Logger logger = LoggerFactory.getLogger(SimpleVectorStore.class);
private final ObjectMapper objectMapper;
protected Map<String, Document> store = new ConcurrentHashMap<>();
protected EmbeddingModel embeddingModel;
@@ -84,6 +89,7 @@ public class SimpleVectorStore extends AbstractObservationVectorStore {
Objects.requireNonNull(embeddingModel, "EmbeddingModel must not be null");
this.embeddingModel = embeddingModel;
this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build();
}
@Override
@@ -172,9 +178,8 @@ public class SimpleVectorStore extends AbstractObservationVectorStore {
public void load(File file) {
TypeReference<HashMap<String, Document>> typeRef = new TypeReference<>() {
};
ObjectMapper objectMapper = new ObjectMapper();
try {
Map<String, Document> deserializedMap = objectMapper.readValue(file, typeRef);
Map<String, Document> deserializedMap = this.objectMapper.readValue(file, typeRef);
this.store = deserializedMap;
}
catch (IOException ex) {
@@ -189,9 +194,8 @@ public class SimpleVectorStore extends AbstractObservationVectorStore {
public void load(Resource resource) {
TypeReference<HashMap<String, Document>> typeRef = new TypeReference<>() {
};
ObjectMapper objectMapper = new ObjectMapper();
try {
Map<String, Document> deserializedMap = objectMapper.readValue(resource.getInputStream(), typeRef);
Map<String, Document> deserializedMap = this.objectMapper.readValue(resource.getInputStream(), typeRef);
this.store = deserializedMap;
}
catch (IOException ex) {
@@ -200,8 +204,7 @@ public class SimpleVectorStore extends AbstractObservationVectorStore {
}
private String getVectorDbAsJson() {
ObjectMapper objectMapper = new ObjectMapper();
ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter();
ObjectWriter objectWriter = this.objectMapper.writerWithDefaultPrettyPrinter();
String json;
try {
json = objectWriter.writeValueAsString(this.store);

View File

@@ -52,9 +52,9 @@ public class BedrockAnthropicChatAutoConfiguration {
@ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class })
public AnthropicChatBedrockApi anthropicApi(AwsCredentialsProvider credentialsProvider,
AwsRegionProvider regionProvider, BedrockAnthropicChatProperties properties,
BedrockAwsConnectionProperties awsProperties) {
BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) {
return new AnthropicChatBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(),
new ObjectMapper(), awsProperties.getTimeout());
objectMapper, awsProperties.getTimeout());
}
@Bean

View File

@@ -52,9 +52,9 @@ public class BedrockAnthropic3ChatAutoConfiguration {
@ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class })
public Anthropic3ChatBedrockApi anthropic3Api(AwsCredentialsProvider credentialsProvider,
AwsRegionProvider regionProvider, BedrockAnthropic3ChatProperties properties,
BedrockAwsConnectionProperties awsProperties) {
BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) {
return new Anthropic3ChatBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(),
new ObjectMapper(), awsProperties.getTimeout());
objectMapper, awsProperties.getTimeout());
}
@Bean

View File

@@ -50,9 +50,9 @@ public class BedrockCohereChatAutoConfiguration {
@ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class })
public CohereChatBedrockApi cohereChatApi(AwsCredentialsProvider credentialsProvider,
AwsRegionProvider regionProvider, BedrockCohereChatProperties properties,
BedrockAwsConnectionProperties awsProperties) {
BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) {
return new CohereChatBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(),
new ObjectMapper(), awsProperties.getTimeout());
objectMapper, awsProperties.getTimeout());
}
@Bean

View File

@@ -51,9 +51,9 @@ public class BedrockCohereEmbeddingAutoConfiguration {
@ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class })
public CohereEmbeddingBedrockApi cohereEmbeddingApi(AwsCredentialsProvider credentialsProvider,
AwsRegionProvider regionProvider, BedrockCohereEmbeddingProperties properties,
BedrockAwsConnectionProperties awsProperties) {
BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) {
return new CohereEmbeddingBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(),
new ObjectMapper(), awsProperties.getTimeout());
objectMapper, awsProperties.getTimeout());
}
@Bean

View File

@@ -52,9 +52,9 @@ public class BedrockAi21Jurassic2ChatAutoConfiguration {
@ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class })
public Ai21Jurassic2ChatBedrockApi ai21Jurassic2ChatBedrockApi(AwsCredentialsProvider credentialsProvider,
AwsRegionProvider regionProvider, BedrockAi21Jurassic2ChatProperties properties,
BedrockAwsConnectionProperties awsProperties) {
BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) {
return new Ai21Jurassic2ChatBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(),
new ObjectMapper(), awsProperties.getTimeout());
objectMapper, awsProperties.getTimeout());
}
@Bean

View File

@@ -52,9 +52,10 @@ public class BedrockLlamaChatAutoConfiguration {
@ConditionalOnMissingBean
@ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class })
public LlamaChatBedrockApi llamaApi(AwsCredentialsProvider credentialsProvider, AwsRegionProvider regionProvider,
BedrockLlamaChatProperties properties, BedrockAwsConnectionProperties awsProperties) {
BedrockLlamaChatProperties properties, BedrockAwsConnectionProperties awsProperties,
ObjectMapper objectMapper) {
return new LlamaChatBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(),
new ObjectMapper(), awsProperties.getTimeout());
objectMapper, awsProperties.getTimeout());
}
@Bean

View File

@@ -50,9 +50,9 @@ public class BedrockTitanChatAutoConfiguration {
@ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class })
public TitanChatBedrockApi titanChatBedrockApi(AwsCredentialsProvider credentialsProvider,
AwsRegionProvider regionProvider, BedrockTitanChatProperties properties,
BedrockAwsConnectionProperties awsProperties) {
BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) {
return new TitanChatBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(),
new ObjectMapper(), awsProperties.getTimeout());
objectMapper, awsProperties.getTimeout());
}
@Bean

View File

@@ -51,9 +51,9 @@ public class BedrockTitanEmbeddingAutoConfiguration {
@ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class })
public TitanEmbeddingBedrockApi titanEmbeddingBedrockApi(AwsCredentialsProvider credentialsProvider,
AwsRegionProvider regionProvider, BedrockTitanEmbeddingProperties properties,
BedrockAwsConnectionProperties awsProperties) {
BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) {
return new TitanEmbeddingBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(),
new ObjectMapper(), awsProperties.getTimeout());
objectMapper, awsProperties.getTimeout());
}
@Bean

View File

@@ -39,6 +39,7 @@ import io.micrometer.observation.ObservationRegistry;
* @author Christian Tzolov
* @author Eddú Meléndez
* @author Soby Chacko
* @author Sebastien Deleuze
*/
@AutoConfiguration
@ConditionalOnClass({ EmbeddingModel.class, RestClient.class, ChromaVectorStore.class, ObjectMapper.class })
@@ -54,12 +55,13 @@ public class ChromaVectorStoreAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ChromaApi chromaApi(ChromaApiProperties apiProperties,
ObjectProvider<RestClient.Builder> restClientBuilderProvider, ChromaConnectionDetails connectionDetails) {
ObjectProvider<RestClient.Builder> restClientBuilderProvider, ChromaConnectionDetails connectionDetails,
ObjectMapper objectMapper) {
String chromaUrl = String.format("%s:%s", connectionDetails.getHost(), connectionDetails.getPort());
var chromaApi = new ChromaApi(chromaUrl, restClientBuilderProvider.getIfAvailable(RestClient::builder),
new ObjectMapper());
objectMapper);
if (StringUtils.hasText(connectionDetails.getKeyToken())) {
chromaApi.withKeyToken(connectionDetails.getKeyToken());

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.ai.testcontainers.service.connection.chroma;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.ai.autoconfigure.vectorstore.chroma.ChromaVectorStoreAutoConfiguration;
import org.springframework.ai.document.Document;
@@ -83,6 +84,11 @@ class ChromaContainerConnectionDetailsFactoryTest {
@ImportAutoConfiguration(ChromaVectorStoreAutoConfiguration.class)
static class Config {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean
public EmbeddingModel embeddingModel() {
return new TransformersEmbeddingModel();

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.ai.testcontainers.service.connection.chroma;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.ai.autoconfigure.vectorstore.chroma.ChromaVectorStoreAutoConfiguration;
import org.springframework.ai.document.Document;
@@ -87,6 +88,11 @@ class ChromaWithToken2ContainerConnectionDetailsFactoryTest {
@ImportAutoConfiguration(ChromaVectorStoreAutoConfiguration.class)
static class Config {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean
public EmbeddingModel embeddingModel() {
return new TransformersEmbeddingModel();

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.ai.testcontainers.service.connection.chroma;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.ai.autoconfigure.vectorstore.chroma.ChromaVectorStoreAutoConfiguration;
import org.springframework.ai.document.Document;
@@ -85,6 +86,11 @@ class ChromaWithTokenContainerConnectionDetailsFactoryTest {
@ImportAutoConfiguration(ChromaVectorStoreAutoConfiguration.class)
static class Config {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean
public EmbeddingModel embeddingModel() {
return new TransformersEmbeddingModel();

View File

@@ -32,6 +32,7 @@ import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingOptionsBuilder;
import org.springframework.ai.embedding.TokenCountBatchingStrategy;
import org.springframework.ai.observation.conventions.VectorStoreProvider;
import org.springframework.ai.util.JacksonUtils;
import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
@@ -42,6 +43,9 @@ import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import io.micrometer.observation.ObservationRegistry;
/**
@@ -50,10 +54,11 @@ import io.micrometer.observation.ObservationRegistry;
* their similarity to a query, using the {@link ChromaApi} and {@link EmbeddingModel} for
* embedding calculations. For more information about how it does this, see the official
* <a href="https://www.trychroma.com/">Chroma website</a>.
*
*
* @author Christian Tzolov
* @author Fu Cheng
*
* @author Sebastien Deleuze
*
*/
public class ChromaVectorStore extends AbstractObservationVectorStore implements InitializingBean {
@@ -79,6 +84,8 @@ public class ChromaVectorStore extends AbstractObservationVectorStore implements
private final BatchingStrategy batchingStrategy;
private final ObjectMapper objectMapper;
public ChromaVectorStore(EmbeddingModel embeddingModel, ChromaApi chromaApi, boolean initializeSchema) {
this(embeddingModel, chromaApi, DEFAULT_COLLECTION_NAME, initializeSchema);
}
@@ -101,6 +108,7 @@ public class ChromaVectorStore extends AbstractObservationVectorStore implements
this.initializeSchema = initializeSchema;
this.filterExpressionConverter = new ChromaFilterExpressionConverter();
this.batchingStrategy = batchingStrategy;
this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build();
}
public void setFilterExpressionConverter(FilterExpressionConverter filterExpressionConverter) {
@@ -152,8 +160,8 @@ public class ChromaVectorStore extends AbstractObservationVectorStore implements
Assert.notNull(query, "Query string must not be null");
float[] embedding = this.embeddingModel.embed(query);
Map<String, Object> where = (StringUtils.hasText(nativeFilterExpression))
? JsonUtils.jsonToMap(nativeFilterExpression) : Map.of();
Map<String, Object> where = (StringUtils.hasText(nativeFilterExpression)) ? jsonToMap(nativeFilterExpression)
: Map.of();
var queryRequest = new ChromaApi.QueryRequest(embedding, request.getTopK(), where);
var queryResponse = this.chromaApi.queryCollection(this.collectionId, queryRequest);
var embeddings = this.chromaApi.toEmbeddingResponseList(queryResponse);
@@ -179,6 +187,16 @@ public class ChromaVectorStore extends AbstractObservationVectorStore implements
return responseDocuments;
}
@SuppressWarnings("unchecked")
private Map<String, Object> jsonToMap(String jsonText) {
try {
return (Map<String, Object>) this.objectMapper.readValue(jsonText, Map.class);
}
catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public String getCollectionName() {
return this.collectionName;
}

View File

@@ -1,79 +0,0 @@
/*
* Copyright 2023 - 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.vectorstore;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Utility class for JSON processing. Provides methods for converting JSON strings to maps
* and lists, and for converting between lists of different numeric types.
*
* @author Christian Tzolov
*/
public class JsonUtils {
/**
* Converts a JSON string to a map.
* @param jsonText the JSON string to convert
* @return the map representation of the JSON string
* @throws RuntimeException if an error occurs during the conversion
*/
public static Map<String, Object> jsonToMap(String jsonText) {
try {
return (Map<String, Object>) new ObjectMapper().readValue(jsonText, Map.class);
}
catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
// /**
// * Converts a list of doubles to a list of floats.
// * @param embeddingDouble the list of doubles to convert
// * @return the list of floats
// */
// public static List<Float> toFloatList(List<Double> embeddingDouble) {
// return embeddingDouble.stream().map(Number::floatValue).toList();
// }
/**
* Converts a list of doubles to a float array.
* @param embedding the list of doubles to convert
* @return the float array
*/
public static float[] toFloatArray(List<Float> embedding) {
float[] embeddingFloat = new float[embedding.size()];
int i = 0;
for (Float d : embedding) {
embeddingFloat[i++] = d.floatValue();
}
return embeddingFloat;
}
/**
* Converts a list of floats to a list of doubles.
* @param floats the list of floats to convert
* @return the list of doubles
*/
public static List<Double> toDouble(List<Float> floats) {
return floats.stream().map(f -> f.doubleValue()).toList();
}
}

View File

@@ -24,6 +24,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.document.Document;
@@ -32,6 +33,7 @@ import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingOptionsBuilder;
import org.springframework.ai.embedding.TokenCountBatchingStrategy;
import org.springframework.ai.observation.conventions.VectorStoreProvider;
import org.springframework.ai.util.JacksonUtils;
import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention;
@@ -62,6 +64,7 @@ import reactor.util.annotation.NonNull;
* @author Christian Tzolov
* @author Thomas Vitale
* @author Soby Chacko
* @author Sebastien Deleuze
*/
public class GemFireVectorStore extends AbstractObservationVectorStore implements InitializingBean {
@@ -81,6 +84,8 @@ public class GemFireVectorStore extends AbstractObservationVectorStore implement
private final BatchingStrategy batchingStrategy;
private final ObjectMapper objectMapper;
/**
* Configures and initializes a GemFireVectorStore instance based on the provided
* configuration.
@@ -127,6 +132,7 @@ public class GemFireVectorStore extends AbstractObservationVectorStore implement
.toString();
this.client = WebClient.create(base);
this.batchingStrategy = batchingStrategy;
this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build();
}
// Create Index Parameters
@@ -419,10 +425,9 @@ public class GemFireVectorStore extends AbstractObservationVectorStore implement
document.getContent(), document.getMetadata()))
.toList());
ObjectMapper objectMapper = new ObjectMapper();
String embeddingsJson = null;
try {
String embeddingString = objectMapper.writeValueAsString(upload);
String embeddingString = this.objectMapper.writeValueAsString(upload);
embeddingsJson = embeddingString.substring("{\"embeddings\":".length());
}
catch (JsonProcessingException e) {
@@ -498,8 +503,7 @@ public class GemFireVectorStore extends AbstractObservationVectorStore implement
createRequest.setVectorSimilarityFunction(vectorSimilarityFunction);
createRequest.setFields(fields);
ObjectMapper objectMapper = new ObjectMapper();
String index = objectMapper.writeValueAsString(createRequest);
String index = this.objectMapper.writeValueAsString(createRequest);
client.post()
.contentType(MediaType.APPLICATION_JSON)

View File

@@ -17,6 +17,8 @@ package org.springframework.ai.vectorstore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import io.micrometer.observation.ObservationRegistry;
import org.slf4j.Logger;
@@ -26,6 +28,7 @@ import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.model.EmbeddingUtils;
import org.springframework.ai.observation.conventions.VectorStoreProvider;
import org.springframework.ai.observation.conventions.VectorStoreSimilarityMetric;
import org.springframework.ai.util.JacksonUtils;
import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext.Builder;
@@ -64,6 +67,7 @@ import java.util.stream.Collectors;
*
* @author Rahul Mittal
* @author Christian Tzolov
* @author Sebastien Deleuze
* @see <a href=
* "https://help.sap.com/docs/hana-cloud-database/sap-hana-cloud-sap-hana-database-vector-engine-guide/introduction">SAP
* HANA Database Vector Engine Guide</a>
@@ -79,6 +83,8 @@ public class HanaCloudVectorStore extends AbstractObservationVectorStore {
private final HanaCloudVectorStoreConfig config;
private final ObjectMapper objectMapper;
public HanaCloudVectorStore(HanaVectorRepository<? extends HanaVectorEntity> repository,
EmbeddingModel embeddingModel, HanaCloudVectorStoreConfig config) {
@@ -94,6 +100,7 @@ public class HanaCloudVectorStore extends AbstractObservationVectorStore {
this.repository = repository;
this.embeddingModel = embeddingModel;
this.config = config;
this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build();
}
@Override
@@ -142,7 +149,7 @@ public class HanaCloudVectorStore extends AbstractObservationVectorStore {
return searchResult.stream().map(c -> {
try {
return new Document(c.get_id(), c.toJson(), Collections.emptyMap());
return new Document(c.get_id(), this.objectMapper.writeValueAsString(c), Collections.emptyMap());
}
catch (JsonProcessingException e) {
throw new RuntimeException(e);

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.ai.vectorstore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.Column;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
@@ -40,10 +38,6 @@ public abstract class HanaVectorEntity {
public HanaVectorEntity() {
}
public String toJson() throws JsonProcessingException {
return new ObjectMapper().writeValueAsString(this);
}
public String get_id() {
return _id;
}

View File

@@ -17,6 +17,7 @@ package org.springframework.ai.vectorstore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.pgvector.PGvector;
import io.micrometer.observation.ObservationRegistry;
import org.postgresql.util.PGobject;
@@ -29,6 +30,7 @@ import org.springframework.ai.embedding.EmbeddingOptionsBuilder;
import org.springframework.ai.embedding.TokenCountBatchingStrategy;
import org.springframework.ai.observation.conventions.VectorStoreProvider;
import org.springframework.ai.observation.conventions.VectorStoreSimilarityMetric;
import org.springframework.ai.util.JacksonUtils;
import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
@@ -60,6 +62,7 @@ import java.util.UUID;
* @author Muthukumaran Navaneethakrishnan
* @author Thomas Vitale
* @author Soby Chacko
* @author Sebastien Deleuze
* @since 1.0.0
*/
public class PgVectorStore extends AbstractObservationVectorStore implements InitializingBean {
@@ -100,7 +103,7 @@ public class PgVectorStore extends AbstractObservationVectorStore implements Ini
private final PgDistanceType distanceType;
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper;
private final boolean removeExistingVectorStoreTable;
@@ -154,6 +157,8 @@ public class PgVectorStore extends AbstractObservationVectorStore implements Ini
super(observationRegistry, customObservationConvention);
this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build();
this.vectorTableName = (null == vectorTableName || vectorTableName.isEmpty()) ? DEFAULT_TABLE_NAME
: vectorTableName.trim();
logger.info("Using the vector table name: {}. Is empty: {}", this.vectorTableName,
@@ -231,7 +236,7 @@ public class PgVectorStore extends AbstractObservationVectorStore implements Ini
private String toJson(Map<String, Object> map) {
try {
return objectMapper.writeValueAsString(map);
return this.objectMapper.writeValueAsString(map);
}
catch (JsonProcessingException e) {
throw new RuntimeException(e);