Spring AI: Vector Similarity Search with Redis
Traditional keyword searches rely on lexical matching and token stemming, which often fall short when queries express intent through synonyms or contextual descriptions rather than exact terms.
Vector similarity search solves this by transforming unstructured text into dense vector embeddings where semantic proximity is represented geometrically. In this tutorial, we will explore how to implement vector similarity search using Spring AI with Redis as a Vector Store and verify the implementation using Testcontainers.
Why Redis?
Redis with RediSearch and RedisJSON modules (available in Redis Stack) provides an efficient, low-latency platform for vector similarity search, caching, and serving as a database. By utilizing Redis for vector embeddings:
-
In-Memory Performance: Benefit from Redis’s ultra-low latency for search queries.
-
Versatility: Perform hybrid queries combining vector similarity with text search or metadata filtering.
-
Operational Simplicity: Easily integrate as a high-speed caching and query layer in modern application architectures.
Dependencies
We include spring-ai-starter-vector-store-redis and Jedis alongside Testcontainers in build.gradle.kts, using the Spring AI BOM to manage Spring AI dependency versions:
val springAiVersion = "2.0.1"
dependencies {
implementation(platform("org.springframework.ai:spring-ai-bom:${springAiVersion}"))
implementation("org.springframework.boot:spring-boot-starter-webmvc")
implementation("org.springframework.ai:spring-ai-starter-vector-store-redis")
implementation("redis.clients:jedis")
testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("org.springframework.ai:spring-ai-spring-boot-testcontainers")
testImplementation("org.testcontainers:testcontainers-junit-jupiter")
testImplementation("com.redis:testcontainers-redis:2.2.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
Configuration
Configure the RedisVectorStore parameters in src/main/resources/application.properties:
spring.application.name=ai-redis
spring.ai.vectorstore.redis.index-name=spring-ai-document-index
spring.ai.vectorstore.redis.prefix=spring-ai-document:
spring.data.redis.client-type=jedis
spring.ai.vectorstore.redis.initialize-schema=true
Setting initialize-schema=true prompts Spring AI to create the requisite index for storing the vectors. index-name selects the index name, while spring.data.redis.client-type=jedis selects the Redis client used by the vector store.
Implementation
Document Model
DocumentItem serves as the domain record for ingesting content and metadata:
package zin.rashidi.boot.ai.redis.document;
import java.util.Map;
import java.util.UUID;
public record DocumentItem(
UUID id,
String text,
Map<String, Object> metadata
) {}
Semantic Search Service
DocumentSearchService wraps Spring AI’s VectorStore to ingest documents and execute similarity searches:
package zin.rashidi.boot.ai.redis.document;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DocumentSearchService {
private final VectorStore vectorStore;
public DocumentSearchService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void add(List<DocumentItem> items) {
var documents = items.stream()
.map(item -> new Document(item.id().toString(), item.text(), item.metadata()))
.toList();
vectorStore.add(documents);
}
public List<Document> search(String query, int topK, double similarityThreshold) {
return vectorStore.similaritySearch(
SearchRequest.builder()
.query(query)
.topK(topK)
.similarityThreshold(similarityThreshold)
.build()
);
}
}
Integration Testing with Testcontainers
We verify vector storage and retrieval against a real Redis instance (Redis Stack) provisioned with the Testcontainers Redis image.
Testcontainers Configuration
TestcontainersConfiguration wires the Redis container with @ServiceConnection:
package zin.rashidi.boot.ai.redis;
import com.redis.testcontainers.RedisStackContainer;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
@TestConfiguration(proxyBeanMethods = false)
public class TestcontainersConfiguration {
@Bean
@ServiceConnection("redis")
RedisStackContainer redisContainer() {
return new RedisStackContainer(RedisStackContainer.DEFAULT_IMAGE_NAME.withTag("latest"));
}
}
Deterministic Test Embedding Model
To ensure tests execute hermetically without external API dependencies or rate limits, DocumentSearchTests defines an in-memory EmbeddingModel.
It clusters domain keywords into explicit vector dimensions so that the query vector aligns deterministically with the target document:
@TestConfiguration
static class TestEmbeddingConfiguration {
@Bean
@Primary
EmbeddingModel testEmbeddingModel() {
return new EmbeddingModel() {
private float[] createVector(String text) {
var vector = new float[384];
var lower = (text != null) ? text.toLowerCase() : "";
// Microservices / Spring cluster -> dimension 0
if (lower.contains("microservice") || lower.contains("spring")) {
vector[0] = 1.0f;
}
// Testing / Testcontainers cluster -> dimension 1
else if (lower.contains("testcontainer") || lower.contains("testing")) {
vector[1] = 1.0f;
}
// Default / fallback -> dimension 2
else {
vector[2] = 1.0f;
}
return vector;
}
@Override
public EmbeddingResponse call(EmbeddingRequest request) {
return request.getInstructions().stream()
.map(text -> new Embedding(createVector(text), 0))
.collect(collectingAndThen(toUnmodifiableList(), EmbeddingResponse::new));
}
@Override
public float[] embed(Document document) {
return createVector(document.getText());
}
};
}
}
Verifying Retrieval
@BeforeEach
void add() {
documents.add(List.of(
new DocumentItem(
UUID.randomUUID(),
"Spring Boot simplifies microservice development with convention over configuration.",
Map.of("category", "framework")
),
new DocumentItem(
UUID.randomUUID(),
"Testcontainers provides disposable, real database containers for integration tests.",
Map.of("category", "testing")
)
));
}
@Test
@DisplayName("should index documents and retrieve semantically similar content")
void searchReturnsSemanticallyRelevantDocuments() {
var results = documents.search("microservices in Java", 1, 0.0);
assertThat(results)
.hasSize(1)
.first()
.extracting("text").isEqualTo("Spring Boot simplifies microservice development with convention over configuration.");
}