diff --git a/spring-ai-modules/pom.xml b/spring-ai-modules/pom.xml index 17649e22f089..6f8d95626a39 100644 --- a/spring-ai-modules/pom.xml +++ b/spring-ai-modules/pom.xml @@ -20,6 +20,7 @@ spring-ai-introduction spring-ai-mcp spring-ai-multiple-llms + spring-ai-semantic-caching spring-ai-text-to-sql spring-ai-vector-stores spring-ai-agentic-patterns diff --git a/spring-ai-modules/spring-ai-semantic-caching/pom.xml b/spring-ai-modules/spring-ai-semantic-caching/pom.xml new file mode 100644 index 000000000000..ee56ecf5ff7d --- /dev/null +++ b/spring-ai-modules/spring-ai-semantic-caching/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + + com.baeldung + spring-ai-modules + 0.0.1 + ../pom.xml + + + spring-ai-semantic-caching + 0.0.1 + spring-ai-semantic-caching + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.ai + spring-ai-starter-model-openai + + + org.springframework.ai + spring-ai-starter-vector-store-redis + + + org.springframework.boot + spring-boot-starter-test + test + + + + + 21 + 1.0.3 + + + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/Application.java b/spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/Application.java new file mode 100644 index 000000000000..6cfd1cf22d55 --- /dev/null +++ b/spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/Application.java @@ -0,0 +1,13 @@ +package com.baeldung.semantic.cache; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/LLMConfiguration.java b/spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/LLMConfiguration.java new file mode 100644 index 000000000000..ce04cd3a0025 --- /dev/null +++ b/spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/LLMConfiguration.java @@ -0,0 +1,35 @@ +package com.baeldung.semantic.cache; + +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.vectorstore.redis.RedisVectorStore; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import redis.clients.jedis.JedisPooled; + +@Configuration +@EnableConfigurationProperties(SemanticCacheProperties.class) +class LLMConfiguration { + + @Bean + JedisPooled jedisPooled(RedisProperties redisProperties) { + return new JedisPooled(redisProperties.getUrl()); + } + + @Bean + RedisVectorStore vectorStore( + JedisPooled jedisPooled, + EmbeddingModel embeddingModel, + SemanticCacheProperties semanticCacheProperties + ) { + return RedisVectorStore + .builder(jedisPooled, embeddingModel) + .contentFieldName(semanticCacheProperties.contentField()) + .embeddingFieldName(semanticCacheProperties.embeddingField()) + .metadataFields( + RedisVectorStore.MetadataField.text(semanticCacheProperties.metadataField())) + .build(); + } + +} \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/SemanticCacheProperties.java b/spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/SemanticCacheProperties.java new file mode 100644 index 000000000000..23b0fc73670d --- /dev/null +++ b/spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/SemanticCacheProperties.java @@ -0,0 +1,11 @@ +package com.baeldung.semantic.cache; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "com.baeldung.semantic.cache") +record SemanticCacheProperties( + Double similarityThreshold, + String contentField, + String embeddingField, + String metadataField +) {} \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/SemanticCachingService.java b/spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/SemanticCachingService.java new file mode 100644 index 000000000000..a2ae7aa16f53 --- /dev/null +++ b/spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/SemanticCachingService.java @@ -0,0 +1,51 @@ +package com.baeldung.semantic.cache; + +import org.springframework.ai.document.Document; +import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +@EnableConfigurationProperties(SemanticCacheProperties.class) +class SemanticCachingService { + + private final VectorStore vectorStore; + private final SemanticCacheProperties semanticCacheProperties; + + SemanticCachingService(VectorStore vectorStore, SemanticCacheProperties semanticCacheProperties) { + this.vectorStore = vectorStore; + this.semanticCacheProperties = semanticCacheProperties; + } + + void save(String question, String answer) { + Document document = Document + .builder() + .text(question) + .metadata(semanticCacheProperties.metadataField(), answer) + .build(); + vectorStore.add(List.of(document)); + } + + Optional search(String question) { + SearchRequest searchRequest = SearchRequest.builder() + .query(question) + .similarityThreshold(semanticCacheProperties.similarityThreshold()) + .topK(1) + .build(); + List results = vectorStore.similaritySearch(searchRequest); + + if (results.isEmpty()) { + return Optional.empty(); + } + + Document result = results.getFirst(); + return Optional + .ofNullable(result.getMetadata().get(semanticCacheProperties.metadataField())) + .map(String::valueOf); + } + +} \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-semantic-caching/src/main/resources/application.yaml b/spring-ai-modules/spring-ai-semantic-caching/src/main/resources/application.yaml new file mode 100644 index 000000000000..3e513b525e93 --- /dev/null +++ b/spring-ai-modules/spring-ai-semantic-caching/src/main/resources/application.yaml @@ -0,0 +1,20 @@ +spring: + ai: + openai: + api-key: ${OPENAI_API_KEY} + embedding: + options: + model: text-embedding-3-small + dimensions: 512 + data: + redis: + url: ${REDIS_URL} + +com: + baeldung: + semantic: + cache: + similarity-threshold: 0.8 + content-field: question + embedding-field: embedding + metadata-field: answer \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-semantic-caching/src/test/java/com/baeldung/semantic/cache/SemanticCacheLiveTest.java b/spring-ai-modules/spring-ai-semantic-caching/src/test/java/com/baeldung/semantic/cache/SemanticCacheLiveTest.java new file mode 100644 index 000000000000..bd313c0bbdb1 --- /dev/null +++ b/spring-ai-modules/spring-ai-semantic-caching/src/test/java/com/baeldung/semantic/cache/SemanticCacheLiveTest.java @@ -0,0 +1,37 @@ +package com.baeldung.semantic.cache; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@SpringBootTest +@EnabledIfEnvironmentVariables({ + @EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*"), + @EnabledIfEnvironmentVariable(named = "REDIS_URL", matches = ".*") +}) +class SemanticCacheLiveTest { + + @Autowired + private SemanticCachingService semanticCachingService; + + @Test + void whenUsingSemanticCache_thenCacheReturnsAnswerForSemanticallyRelatedQuestion() { + String question = "How many sick leaves can I take?"; + String answer = "No leaves allowed! Get back to work!!"; + semanticCachingService.save(question, answer); + + String rephrasedQuestion = "How many days sick leave can I take?"; + assertThat(semanticCachingService.search(rephrasedQuestion)) + .isPresent() + .hasValue(answer); + + String unrelatedQuestion = "Can I get a raise?"; + assertThat(semanticCachingService.search(unrelatedQuestion)) + .isEmpty(); + } + +} \ No newline at end of file