Skip to content

Commit d543de0

Browse files
codebase/spring-ai-semantic-caching [BAEL-9489] (#18898)
* init project structure * implement semantic caching * add test case * remove unnecessary configuration setting * minor refactor * desensitize flagged test data
1 parent 24bb077 commit d543de0

File tree

8 files changed

+231
-0
lines changed

8 files changed

+231
-0
lines changed

spring-ai-modules/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<module>spring-ai-introduction</module>
2626
<module>spring-ai-mcp</module>
2727
<module>spring-ai-multiple-llms</module>
28+
<module>spring-ai-semantic-caching</module>
2829
<module>spring-ai-text-to-sql</module>
2930
<module>spring-ai-vector-stores</module>
3031
</modules>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>com.baeldung</groupId>
8+
<artifactId>spring-ai-modules</artifactId>
9+
<version>0.0.1</version>
10+
<relativePath>../pom.xml</relativePath>
11+
</parent>
12+
13+
<artifactId>spring-ai-semantic-caching</artifactId>
14+
<version>0.0.1</version>
15+
<name>spring-ai-semantic-caching</name>
16+
17+
<dependencies>
18+
<dependency>
19+
<groupId>org.springframework.boot</groupId>
20+
<artifactId>spring-boot-starter-web</artifactId>
21+
</dependency>
22+
<dependency>
23+
<groupId>org.springframework.ai</groupId>
24+
<artifactId>spring-ai-starter-model-openai</artifactId>
25+
</dependency>
26+
<dependency>
27+
<groupId>org.springframework.ai</groupId>
28+
<artifactId>spring-ai-starter-vector-store-redis</artifactId>
29+
</dependency>
30+
<dependency>
31+
<groupId>org.springframework.boot</groupId>
32+
<artifactId>spring-boot-starter-test</artifactId>
33+
<scope>test</scope>
34+
</dependency>
35+
</dependencies>
36+
37+
<properties>
38+
<java.version>21</java.version>
39+
<spring-ai.version>1.0.3</spring-ai.version>
40+
</properties>
41+
42+
<dependencyManagement>
43+
<dependencies>
44+
<dependency>
45+
<groupId>org.springframework.ai</groupId>
46+
<artifactId>spring-ai-bom</artifactId>
47+
<version>${spring-ai.version}</version>
48+
<type>pom</type>
49+
<scope>import</scope>
50+
</dependency>
51+
</dependencies>
52+
</dependencyManagement>
53+
54+
<build>
55+
<plugins>
56+
<plugin>
57+
<groupId>org.springframework.boot</groupId>
58+
<artifactId>spring-boot-maven-plugin</artifactId>
59+
</plugin>
60+
</plugins>
61+
</build>
62+
63+
</project>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.baeldung.semantic.cache;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
class Application {
8+
9+
public static void main(String[] args) {
10+
SpringApplication.run(Application.class, args);
11+
}
12+
13+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.baeldung.semantic.cache;
2+
3+
import org.springframework.ai.embedding.EmbeddingModel;
4+
import org.springframework.ai.vectorstore.redis.RedisVectorStore;
5+
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
6+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.context.annotation.Configuration;
9+
import redis.clients.jedis.JedisPooled;
10+
11+
@Configuration
12+
@EnableConfigurationProperties(SemanticCacheProperties.class)
13+
class LLMConfiguration {
14+
15+
@Bean
16+
JedisPooled jedisPooled(RedisProperties redisProperties) {
17+
return new JedisPooled(redisProperties.getUrl());
18+
}
19+
20+
@Bean
21+
RedisVectorStore vectorStore(
22+
JedisPooled jedisPooled,
23+
EmbeddingModel embeddingModel,
24+
SemanticCacheProperties semanticCacheProperties
25+
) {
26+
return RedisVectorStore
27+
.builder(jedisPooled, embeddingModel)
28+
.contentFieldName(semanticCacheProperties.contentField())
29+
.embeddingFieldName(semanticCacheProperties.embeddingField())
30+
.metadataFields(
31+
RedisVectorStore.MetadataField.text(semanticCacheProperties.metadataField()))
32+
.build();
33+
}
34+
35+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.baeldung.semantic.cache;
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties;
4+
5+
@ConfigurationProperties(prefix = "com.baeldung.semantic.cache")
6+
record SemanticCacheProperties(
7+
Double similarityThreshold,
8+
String contentField,
9+
String embeddingField,
10+
String metadataField
11+
) {}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.baeldung.semantic.cache;
2+
3+
import org.springframework.ai.document.Document;
4+
import org.springframework.ai.vectorstore.SearchRequest;
5+
import org.springframework.ai.vectorstore.VectorStore;
6+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
7+
import org.springframework.stereotype.Service;
8+
9+
import java.util.List;
10+
import java.util.Optional;
11+
12+
@Service
13+
@EnableConfigurationProperties(SemanticCacheProperties.class)
14+
class SemanticCachingService {
15+
16+
private final VectorStore vectorStore;
17+
private final SemanticCacheProperties semanticCacheProperties;
18+
19+
SemanticCachingService(VectorStore vectorStore, SemanticCacheProperties semanticCacheProperties) {
20+
this.vectorStore = vectorStore;
21+
this.semanticCacheProperties = semanticCacheProperties;
22+
}
23+
24+
void save(String question, String answer) {
25+
Document document = Document
26+
.builder()
27+
.text(question)
28+
.metadata(semanticCacheProperties.metadataField(), answer)
29+
.build();
30+
vectorStore.add(List.of(document));
31+
}
32+
33+
Optional<String> search(String question) {
34+
SearchRequest searchRequest = SearchRequest.builder()
35+
.query(question)
36+
.similarityThreshold(semanticCacheProperties.similarityThreshold())
37+
.topK(1)
38+
.build();
39+
List<Document> results = vectorStore.similaritySearch(searchRequest);
40+
41+
if (results.isEmpty()) {
42+
return Optional.empty();
43+
}
44+
45+
Document result = results.getFirst();
46+
return Optional
47+
.ofNullable(result.getMetadata().get(semanticCacheProperties.metadataField()))
48+
.map(String::valueOf);
49+
}
50+
51+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
spring:
2+
ai:
3+
openai:
4+
api-key: ${OPENAI_API_KEY}
5+
embedding:
6+
options:
7+
model: text-embedding-3-small
8+
dimensions: 512
9+
data:
10+
redis:
11+
url: ${REDIS_URL}
12+
13+
com:
14+
baeldung:
15+
semantic:
16+
cache:
17+
similarity-threshold: 0.8
18+
content-field: question
19+
embedding-field: embedding
20+
metadata-field: answer
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.baeldung.semantic.cache;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
5+
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.boot.test.context.SpringBootTest;
8+
9+
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
10+
11+
@SpringBootTest
12+
@EnabledIfEnvironmentVariables({
13+
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*"),
14+
@EnabledIfEnvironmentVariable(named = "REDIS_URL", matches = ".*")
15+
})
16+
class SemanticCacheLiveTest {
17+
18+
@Autowired
19+
private SemanticCachingService semanticCachingService;
20+
21+
@Test
22+
void whenUsingSemanticCache_thenCacheReturnsAnswerForSemanticallyRelatedQuestion() {
23+
String question = "How many sick leaves can I take?";
24+
String answer = "No leaves allowed! Get back to work!!";
25+
semanticCachingService.save(question, answer);
26+
27+
String rephrasedQuestion = "How many days sick leave can I take?";
28+
assertThat(semanticCachingService.search(rephrasedQuestion))
29+
.isPresent()
30+
.hasValue(answer);
31+
32+
String unrelatedQuestion = "Can I get a raise?";
33+
assertThat(semanticCachingService.search(unrelatedQuestion))
34+
.isEmpty();
35+
}
36+
37+
}

0 commit comments

Comments
 (0)