From d26df09428fd1cd05cb9350c05ab3e20deefc6d5 Mon Sep 17 00:00:00 2001 From: Hardik Singh Behl Date: Thu, 30 Oct 2025 15:32:01 +0530 Subject: [PATCH 1/6] init project structure --- spring-ai-modules/pom.xml | 1 + .../spring-ai-semantic-caching/pom.xml | 63 +++++++++++++++++++ .../baeldung/semantic/cache/Application.java | 13 ++++ 3 files changed, 77 insertions(+) create mode 100644 spring-ai-modules/spring-ai-semantic-caching/pom.xml create mode 100644 spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/Application.java 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 From c9e8f532cd2450fb03377ec54c05c8ed6f3743be Mon Sep 17 00:00:00 2001 From: Hardik Singh Behl Date: Thu, 30 Oct 2025 15:32:22 +0530 Subject: [PATCH 2/6] implement semantic caching --- .../semantic/cache/LLMConfiguration.java | 36 +++++++++++++ .../cache/SemanticCacheProperties.java | 11 ++++ .../cache/SemanticCachingService.java | 50 +++++++++++++++++++ .../src/main/resources/application.yaml | 20 ++++++++ 4 files changed, 117 insertions(+) create mode 100644 spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/LLMConfiguration.java create mode 100644 spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/SemanticCacheProperties.java create mode 100644 spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/SemanticCachingService.java create mode 100644 spring-ai-modules/spring-ai-semantic-caching/src/main/resources/application.yaml 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..d46731a91e6e --- /dev/null +++ b/spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/LLMConfiguration.java @@ -0,0 +1,36 @@ +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())) + .initializeSchema(true) + .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..ab5f0cbd8090 --- /dev/null +++ b/spring-ai-modules/spring-ai-semantic-caching/src/main/java/com/baeldung/semantic/cache/SemanticCachingService.java @@ -0,0 +1,50 @@ +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(); + String answer = String.valueOf(result.getMetadata().get(semanticCacheProperties.metadataField())); + return Optional.of(answer); + } + +} \ 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 From ac5615df91867cbe2b9be16d0c32cef6bf931248 Mon Sep 17 00:00:00 2001 From: Hardik Singh Behl Date: Thu, 30 Oct 2025 15:33:51 +0530 Subject: [PATCH 3/6] add test case --- .../semantic/cache/SemanticCacheLiveTest.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 spring-ai-modules/spring-ai-semantic-caching/src/test/java/com/baeldung/semantic/cache/SemanticCacheLiveTest.java 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..662e154b0f35 --- /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 for slaves! 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 From 836c1bdb613155ad7ede24ce3f0ad74305554478 Mon Sep 17 00:00:00 2001 From: Hardik Singh Behl Date: Fri, 31 Oct 2025 16:23:38 +0530 Subject: [PATCH 4/6] remove unnecessary configuration setting --- .../main/java/com/baeldung/semantic/cache/LLMConfiguration.java | 1 - 1 file changed, 1 deletion(-) 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 index d46731a91e6e..ce04cd3a0025 100644 --- 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 @@ -29,7 +29,6 @@ RedisVectorStore vectorStore( .embeddingFieldName(semanticCacheProperties.embeddingField()) .metadataFields( RedisVectorStore.MetadataField.text(semanticCacheProperties.metadataField())) - .initializeSchema(true) .build(); } From c523e2437186af22452a3fb39e16dbeb860ad466 Mon Sep 17 00:00:00 2001 From: Hardik Singh Behl Date: Mon, 3 Nov 2025 18:07:22 +0530 Subject: [PATCH 5/6] minor refactor --- .../com/baeldung/semantic/cache/SemanticCachingService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index ab5f0cbd8090..a2ae7aa16f53 100644 --- 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 @@ -43,8 +43,9 @@ Optional search(String question) { } Document result = results.getFirst(); - String answer = String.valueOf(result.getMetadata().get(semanticCacheProperties.metadataField())); - return Optional.of(answer); + return Optional + .ofNullable(result.getMetadata().get(semanticCacheProperties.metadataField())) + .map(String::valueOf); } } \ No newline at end of file From 15adae120319c8ac0dce7fcf9a8e1c60b9b6ef13 Mon Sep 17 00:00:00 2001 From: Hardik Singh Behl Date: Tue, 4 Nov 2025 10:59:41 +0530 Subject: [PATCH 6/6] desensitize flagged test data --- .../java/com/baeldung/semantic/cache/SemanticCacheLiveTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 662e154b0f35..bd313c0bbdb1 100644 --- 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 @@ -21,7 +21,7 @@ class SemanticCacheLiveTest { @Test void whenUsingSemanticCache_thenCacheReturnsAnswerForSemanticallyRelatedQuestion() { String question = "How many sick leaves can I take?"; - String answer = "No leaves allowed for slaves! Get back to work!!"; + String answer = "No leaves allowed! Get back to work!!"; semanticCachingService.save(question, answer); String rephrasedQuestion = "How many days sick leave can I take?";