这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions spring-ai-modules/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<module>spring-ai-introduction</module>
<module>spring-ai-mcp</module>
<module>spring-ai-multiple-llms</module>
<module>spring-ai-semantic-caching</module>
<module>spring-ai-text-to-sql</module>
<module>spring-ai-vector-stores</module>
<module>spring-ai-agentic-patterns</module>
Expand Down
63 changes: 63 additions & 0 deletions spring-ai-modules/spring-ai-semantic-caching/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.baeldung</groupId>
<artifactId>spring-ai-modules</artifactId>
<version>0.0.1</version>
<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>spring-ai-semantic-caching</artifactId>
<version>0.0.1</version>
<name>spring-ai-semantic-caching</name>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<properties>
<java.version>21</java.version>
<spring-ai.version>1.0.3</spring-ai.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -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
) {}
Original file line number Diff line number Diff line change
@@ -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<String> search(String question) {
SearchRequest searchRequest = SearchRequest.builder()
.query(question)
.similarityThreshold(semanticCacheProperties.similarityThreshold())
.topK(1)
.build();
List<Document> 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);
}

}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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();
}

}