Add builder pattern to RedisVectorStore and refactor package name

Refactors RedisVectorStore to use the builder pattern for improved
configuration and usability. The changes include:

* Move classes to org.springframework.ai.vectorstore.redis package
* Add RedisBuilder with comprehensive configuration options
* Deprecate RedisVectorStoreConfig in favor of builder pattern
* Enhance documentation with detailed usage examples
* Improve error handling and parameter validation

This change makes RedisVectorStore configuration more intuitive and
consistent with other vector stores in the project.
This commit is contained in:
Soby Chacko
2024-12-09 19:48:51 -05:00
committed by Mark Pollack
parent cde8f7446c
commit f69d879ec9
7 changed files with 459 additions and 224 deletions

View File

@@ -45,41 +45,15 @@ TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Man
TIP: Refer to the xref:getting-started.adoc#repositories[Repositories] section to add Milestone and/or Snapshot Repositories to your build file.
The vector store implementation can initialize the requisite schema for you, but you must opt-in by specifying the `initializeSchema` boolean in the appropriate constructor or by setting `...initialize-schema=true` in the `application.properties` file.
NOTE: this is a breaking change! In earlier versions of Spring AI, this schema initialization happened by default.
Please have a look at the list of <<redisvector-properties,configuration parameters>> for the vector store to learn about the default values and configuration options.
Additionally, you will need a configured `EmbeddingModel` bean. Refer to the xref:api/embeddings.adoc#available-implementations[EmbeddingModel] section for more information.
Here is an example of the needed bean:
[source,java]
----
@Bean
public EmbeddingModel embeddingModel() {
// Can be any other EmbeddingModel implementation.
return new OpenAiEmbeddingModel(new OpenAiApi(System.getenv("SPRING_AI_OPENAI_API_KEY")));
}
----
To connect to Redis you need to provide access details for your instance.
A simple configuration can either be provided via Spring Boot's _application.properties_,
[source,properties]
----
spring.ai.vectorstore.redis.uri=<your redis instance uri>
spring.ai.vectorstore.redis.index=<your index name>
spring.ai.vectorstore.redis.prefix=<your prefix>
# API key if needed, e.g. OpenAI
spring.ai.openai.api.key=<api-key>
----
Please have a look at the list of xref:#_configuration_properties[configuration parameters] for the vector store to learn about the default values and configuration options.
Now you can Auto-wire the Redis Vector Store in your application and use it
Now you can auto-wire the `RedisVectorStore` as a vector store in your application.
[source,java]
----
@@ -99,146 +73,144 @@ vectorStore.add(documents);
List<Document> results = this.vectorStore.similaritySearch(SearchRequest.query("Spring").withTopK(5));
----
=== Configuration properties
[[redisvector-properties]]
=== Configuration Properties
You can use the following properties in your Spring Boot configuration to customize the Redis vector store.
To connect to Redis and use the `RedisVectorStore`, you need to provide access details for your instance.
A simple configuration can be provided via Spring Boot's `application.yml`,
[stripes=even]
[source,yaml]
----
spring:
data:
redis:
uri: <redis instance uri>
ai:
vectorstore:
redis:
initialize-schema: true
index-name: custom-index
prefix: custom-prefix
batching-strategy: TOKEN_COUNT # Optional: Controls how documents are batched for embedding
----
Properties starting with `spring.ai.vectorstore.redis.*` are used to configure the `RedisVectorStore`:
[cols="2,5,1",stripes=even]
|===
|Property| Description | Default value
|`spring.ai.vectorstore.redis.uri`| Server connection URI | `redis://localhost:6379`
|`spring.ai.vectorstore.redis.index`| Index name | `default-index`
|`spring.ai.vectorstore.redis.initialize-schema`| Whether to initialize the required schema | `false`
|`spring.ai.vectorstore.redis.prefix`| Prefix | `default:`
|Property | Description | Default Value
|`spring.ai.vectorstore.redis.initialize-schema`| Whether to initialize the required schema | `false`
|`spring.ai.vectorstore.redis.index-name` | The name of the index to store the vectors | `spring-ai-index`
|`spring.ai.vectorstore.redis.prefix` | The prefix for Redis keys | `embedding:`
|`spring.ai.vectorstore.redis.batching-strategy` | Strategy for batching documents when calculating embeddings. Options are `TOKEN_COUNT` or `FIXED_SIZE` | `TOKEN_COUNT`
|===
== Metadata filtering
== Metadata Filtering
You can leverage the generic, portable link:https://docs.spring.io/spring-ai/reference/api/vectordbs.html#_metadata_filters[metadata filters] with RedisVectorStore as well.
You can leverage the generic, portable xref:api/vectordbs.adoc#metadata-filters[metadata filters] with Redis as well.
For example, you can use either the text expression language:
[source,java]
----
vectorStore.similaritySearch(
SearchRequest
.query("The World")
.withTopK(TOP_K)
.withSimilarityThreshold(SIMILARITY_THRESHOLD)
.withFilterExpression("country in ['UK', 'NL'] && year >= 2020"));
vectorStore.similaritySearch(SearchRequest.defaults()
.withQuery("The World")
.withTopK(TOP_K)
.withSimilarityThreshold(SIMILARITY_THRESHOLD)
.withFilterExpression("country in ['UK', 'NL'] && year >= 2020"));
----
or programmatically using the expression DSL:
or programmatically using the `Filter.Expression` DSL:
[source,java]
----
FilterExpressionBuilder b = new FilterExpressionBuilder();
vectorStore.similaritySearch(
SearchRequest
.query("The World")
.withTopK(TOP_K)
.withSimilarityThreshold(SIMILARITY_THRESHOLD)
.withFilterExpression(b.and(
b.in("country", "UK", "NL"),
b.gte("year", 2020)).build()));
vectorStore.similaritySearch(SearchRequest.defaults()
.withQuery("The World")
.withTopK(TOP_K)
.withSimilarityThreshold(SIMILARITY_THRESHOLD)
.withFilterExpression(b.and(
b.in("country", "UK", "NL"),
b.gte("year", 2020)).build()));
----
The portable filter expressions get automatically converted into link:https://redis.io/docs/interact/search-and-query/query/[Redis search queries].
For example, the following portable filter expression:
NOTE: Those (portable) filter expressions get automatically converted into link:https://redis.io/docs/interact/search-and-query/query/[Redis search queries].
For example, this portable filter expression:
[source,sql]
----
country in ['UK', 'NL'] && year >= 2020
----
is converted into Redis query:
is converted into the proprietary Redis filter format:
[source]
[source,text]
----
@country:{UK | NL} @year:[2020 inf]
----
== Manual configuration
== Manual Configuration
If you prefer not to use the auto-configuration, you can manually configure the Redis Vector Store.
Add the Redis Vector Store and Jedis dependencies
Instead of using the Spring Boot auto-configuration, you can manually configure the Redis vector store. For this you need to add the `spring-ai-redis-store` to your project:
[source,xml]
----
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-redis-store</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>5.1.0</version>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-redis-store</artifactId>
</dependency>
----
TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
or to your Gradle `build.gradle` build file.
Then, create a `RedisVectorStore` bean in your Spring configuration:
[source,groovy]
----
dependencies {
implementation 'org.springframework.ai:spring-ai-redis-store'
}
----
Create a `JedisPooled` bean:
[source,java]
----
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
RedisVectorStoreConfig config = RedisVectorStoreConfig.builder()
.withURI("redis://localhost:6379")
// Define the metadata fields to be used
// in the similarity search filters.
.withMetadataFields(
MetadataField.tag("country"),
MetadataField.numeric("year"))
.build();
public JedisPooled jedisPooled() {
return new JedisPooled("<host>", 6379);
}
----
return new RedisVectorStore(config, embeddingModel);
Then create the `RedisVectorStore` bean using the builder pattern:
[source,java]
----
@Bean
public VectorStore vectorStore(JedisPooled jedisPooled, EmbeddingModel embeddingModel) {
return RedisVectorStore.builder()
.jedis(jedisPooled)
.embeddingModel(embeddingModel)
.indexName("custom-index") // Optional: defaults to "spring-ai-index"
.prefix("custom-prefix") // Optional: defaults to "embedding:"
.metadataFields( // Optional: define metadata fields for filtering
MetadataField.tag("country"),
MetadataField.numeric("year"))
.initializeSchema(true) // Optional: defaults to false
.batchingStrategy(new TokenCountBatchingStrategy()) // Optional: defaults to TokenCountBatchingStrategy
.build();
}
// This can be any EmbeddingModel implementation
@Bean
public EmbeddingModel embeddingModel() {
return new OpenAiEmbeddingModel(new OpenAiApi(System.getenv("OPENAI_API_KEY")));
}
----
[NOTE]
====
It is more convenient and preferred to create the `RedisVectorStore` as a Bean.
But if you decide to create it manually, then you must call the `RedisVectorStore#afterPropertiesSet()` after setting the properties and before using the client.
You must list explicitly all metadata field names and types (`TAG`, `TEXT`, or `NUMERIC`) for any metadata field used in filter expressions.
The `metadataFields` above registers filterable metadata fields: `country` of type `TAG`, `year` of type `NUMERIC`.
====
[NOTE]
====
You must list explicitly all metadata field names and types (`TAG`, `TEXT`, or `NUMERIC`) for any metadata field used in filter expression.
The `withMetadataFields` above registers filterable metadata fields: `country` of type `TAG`, `year` of type `NUMERIC`.
====
Then in your main code, create some documents:
[source,java]
----
List<Document> documents = List.of(
new Document("Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!!", Map.of("country", "UK", "year", 2020)),
new Document("The World is Big and Salvation Lurks Around the Corner", Map.of()),
new Document("You walk forward facing the past and you turn back toward the future.", Map.of("country", "NL", "year", 2023)));
----
Now add the documents to your vector store:
[source,java]
----
vectorStore.add(documents);
----
And finally, retrieve documents similar to a query:
[source,java]
----
List<Document> results = vectorStore.similaritySearch(
SearchRequest
.query("Spring")
.withTopK(5));
----
If all goes well, you should retrieve the document containing the text "Spring AI rocks!!".

View File

@@ -22,8 +22,7 @@ import redis.clients.jedis.JedisPooled;
import org.springframework.ai.embedding.BatchingStrategy;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.TokenCountBatchingStrategy;
import org.springframework.ai.vectorstore.RedisVectorStore;
import org.springframework.ai.vectorstore.RedisVectorStore.RedisVectorStoreConfig;
import org.springframework.ai.vectorstore.redis.RedisVectorStore;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
@@ -61,15 +60,16 @@ public class RedisVectorStoreAutoConfiguration {
ObjectProvider<VectorStoreObservationConvention> customObservationConvention,
BatchingStrategy batchingStrategy) {
var config = RedisVectorStoreConfig.builder()
.withIndexName(properties.getIndex())
.withPrefix(properties.getPrefix())
return RedisVectorStore.builder()
.jedis(new JedisPooled(jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort()))
.embeddingModel(embeddingModel)
.initializeSchema(properties.isInitializeSchema())
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
.customObservationConvention(customObservationConvention.getIfAvailable(() -> null))
.batchingStrategy(batchingStrategy)
.indexName(properties.getIndex())
.prefix(properties.getPrefix())
.build();
return new RedisVectorStore(config, embeddingModel,
new JedisPooled(jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort()),
properties.isInitializeSchema(), observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP),
customObservationConvention.getIfAvailable(() -> null), batchingStrategy);
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.vectorstore;
package org.springframework.ai.vectorstore.redis;
import java.text.MessageFormat;
import java.util.List;
@@ -22,7 +22,7 @@ import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.ai.vectorstore.RedisVectorStore.MetadataField;
import org.springframework.ai.vectorstore.redis.RedisVectorStore.MetadataField;
import org.springframework.ai.vectorstore.filter.Filter.Expression;
import org.springframework.ai.vectorstore.filter.Filter.ExpressionType;
import org.springframework.ai.vectorstore.filter.Filter.Group;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.vectorstore;
package org.springframework.ai.vectorstore.redis;
import java.text.MessageFormat;
import java.util.ArrayList;
@@ -54,6 +54,9 @@ import org.springframework.ai.embedding.EmbeddingOptionsBuilder;
import org.springframework.ai.embedding.TokenCountBatchingStrategy;
import org.springframework.ai.observation.conventions.VectorStoreProvider;
import org.springframework.ai.observation.conventions.VectorStoreSimilarityMetric;
import org.springframework.ai.vectorstore.AbstractVectorStoreBuilder;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
@@ -61,21 +64,114 @@ import org.springframework.ai.vectorstore.observation.VectorStoreObservationConv
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* The RedisVectorStore is for managing and querying vector data in a Redis database. It
* offers functionalities like adding, deleting, and performing similarity searches on
* documents.
* Redis-based vector store implementation using Redis Stack with RediSearch and
* RedisJSON.
*
* <p>
* The store uses Redis JSON documents to persist vector embeddings along with their
* associated document content and metadata. It leverages RediSearch for creating and
* querying vector similarity indexes. The RedisVectorStore manages and queries vector
* data, offering functionalities like adding, deleting, and performing similarity
* searches on documents.
* </p>
*
* <p>
* The store utilizes RedisJSON and RedisSearch to handle JSON documents and to index and
* search vector data. It supports various vector algorithms (e.g., FLAT, HSNW) for
* search vector data. It supports various vector algorithms (e.g., FLAT, HNSW) for
* efficient similarity searches. Additionally, it allows for custom metadata fields in
* the documents to be stored alongside the vector and content data.
* </p>
*
* This class requires a RedisVectorStoreConfig configuration object for initialization,
* which includes settings like Redis URI, index name, field names, and vector algorithms.
* It also requires an EmbeddingModel to convert documents into embeddings before storing
* them.
* <p>
* Features:
* </p>
* <ul>
* <li>Automatic schema initialization with configurable index creation</li>
* <li>Support for HNSW and FLAT vector indexing algorithms</li>
* <li>Cosine similarity metric for vector comparisons</li>
* <li>Flexible metadata field types (TEXT, TAG, NUMERIC) for advanced filtering</li>
* <li>Configurable similarity thresholds for search results</li>
* <li>Batch processing support with configurable batching strategies</li>
* </ul>
*
* <p>
* Basic usage example:
* </p>
* <pre>{@code
* RedisVectorStore vectorStore = RedisVectorStore.builder()
* .jedis(jedisPooled)
* .embeddingModel(embeddingModel)
* .indexName("custom-index") // Optional: defaults to "spring-ai-index"
* .prefix("custom-prefix") // Optional: defaults to "embedding:"
* .vectorAlgorithm(Algorithm.HNSW)
* .build();
*
* // Add documents
* vectorStore.add(List.of(
* new Document("content1", Map.of("meta1", "value1")),
* new Document("content2", Map.of("meta2", "value2"))
* ));
*
* // Search with filters
* List<Document> results = vectorStore.similaritySearch(
* SearchRequest.query("search text")
* .withTopK(5)
* .withSimilarityThreshold(0.7)
* .withFilterExpression("meta1 == 'value1'")
* );
* }</pre>
*
* <p>
* Advanced configuration example:
* </p>
* <pre>{@code
* RedisVectorStore vectorStore = RedisVectorStore.builder()
* .jedis(jedisPooled)
* .embeddingModel(embeddingModel)
* .indexName("custom-index")
* .prefix("custom-prefix")
* .contentFieldName("custom_content")
* .embeddingFieldName("custom_embedding")
* .vectorAlgorithm(Algorithm.FLAT)
* .metadataFields(
* MetadataField.tag("category"),
* MetadataField.numeric("year"),
* MetadataField.text("description"))
* .initializeSchema(true)
* .batchingStrategy(new TokenCountBatchingStrategy())
* .build();
* }</pre>
*
* <p>
* Database Requirements:
* </p>
* <ul>
* <li>Redis Stack with RediSearch and RedisJSON modules</li>
* <li>Redis version 7.0 or higher</li>
* <li>Sufficient memory for storing vectors and indexes</li>
* </ul>
*
* <p>
* Vector Algorithms:
* </p>
* <ul>
* <li>HNSW: Default algorithm, provides better search performance with slightly higher
* memory usage</li>
* <li>FLAT: Brute force algorithm, provides exact results but slower for large
* datasets</li>
* </ul>
*
* <p>
* Metadata Field Types:
* </p>
* <ul>
* <li>TAG: For exact match filtering on categorical data</li>
* <li>TEXT: For full-text search capabilities</li>
* <li>NUMERIC: For range queries on numerical data</li>
* </ul>
*
* @author Julien Ruaux
* @author Christian Tzolov
@@ -86,6 +182,7 @@ import org.springframework.util.CollectionUtils;
* @see VectorStore
* @see RedisVectorStoreConfig
* @see EmbeddingModel
* @since 1.0.0
*/
public class RedisVectorStore extends AbstractObservationVectorStore implements InitializingBean {
@@ -119,40 +216,67 @@ public class RedisVectorStore extends AbstractObservationVectorStore implements
private static final String DEFAULT_DISTANCE_METRIC = "COSINE";
private final boolean initializeSchema;
private final JedisPooled jedis;
private final EmbeddingModel embeddingModel;
private final boolean initializeSchema;
private final RedisVectorStoreConfig config;
private final String indexName;
private final String prefix;
private final String contentFieldName;
private final String embeddingFieldName;
private final Algorithm vectorAlgorithm;
private final List<MetadataField> metadataFields;
private final BatchingStrategy batchingStrategy;
private FilterExpressionConverter filterExpressionConverter;
private final FilterExpressionConverter filterExpressionConverter;
@Deprecated(since = "1.0.0-M5", forRemoval = true)
public RedisVectorStore(RedisVectorStoreConfig config, EmbeddingModel embeddingModel, JedisPooled jedis,
boolean initializeSchema) {
this(config, embeddingModel, jedis, initializeSchema, ObservationRegistry.NOOP, null,
new TokenCountBatchingStrategy());
}
@Deprecated(since = "1.0.0-M5", forRemoval = true)
public RedisVectorStore(RedisVectorStoreConfig config, EmbeddingModel embeddingModel, JedisPooled jedis,
boolean initializeSchema, ObservationRegistry observationRegistry,
VectorStoreObservationConvention customObservationConvention, BatchingStrategy batchingStrategy) {
super(observationRegistry, customObservationConvention);
this(builder().jedis(jedis)
.embeddingModel(embeddingModel)
.indexName(config.indexName)
.prefix(config.prefix)
.contentFieldName(config.contentFieldName)
.embeddingFieldName(config.embeddingFieldName)
.vectorAlgorithm(config.vectorAlgorithm)
.metadataFields(config.metadataFields)
.initializeSchema(initializeSchema)
.observationRegistry(observationRegistry)
.customObservationConvention(customObservationConvention)
.batchingStrategy(batchingStrategy));
}
Assert.notNull(config, "Config must not be null");
Assert.notNull(embeddingModel, "Embedding model must not be null");
this.initializeSchema = initializeSchema;
protected RedisVectorStore(RedisBuilder builder) {
super(builder);
this.jedis = jedis;
this.embeddingModel = embeddingModel;
this.config = config;
this.filterExpressionConverter = new RedisFilterExpressionConverter(this.config.metadataFields);
this.batchingStrategy = batchingStrategy;
Assert.notNull(builder.jedis, "JedisPooled must not be null");
this.jedis = builder.jedis;
this.indexName = builder.indexName;
this.prefix = builder.prefix;
this.contentFieldName = builder.contentFieldName;
this.embeddingFieldName = builder.embeddingFieldName;
this.vectorAlgorithm = builder.vectorAlgorithm;
this.metadataFields = builder.metadataFields;
this.initializeSchema = builder.initializeSchema;
this.batchingStrategy = builder.batchingStrategy;
this.filterExpressionConverter = new RedisFilterExpressionConverter(this.metadataFields);
}
public JedisPooled getJedis() {
@@ -168,8 +292,8 @@ public class RedisVectorStore extends AbstractObservationVectorStore implements
for (Document document : documents) {
var fields = new HashMap<String, Object>();
fields.put(this.config.embeddingFieldName, embeddings.get(documents.indexOf(document)));
fields.put(this.config.contentFieldName, document.getContent());
fields.put(this.embeddingFieldName, embeddings.get(documents.indexOf(document)));
fields.put(this.contentFieldName, document.getContent());
fields.putAll(document.getMetadata());
pipeline.jsonSetWithEscape(key(document.getId()), JSON_SET_PATH, fields);
}
@@ -186,7 +310,7 @@ public class RedisVectorStore extends AbstractObservationVectorStore implements
}
private String key(String id) {
return this.config.prefix + id;
return this.prefix + id;
}
@Override
@@ -216,13 +340,13 @@ public class RedisVectorStore extends AbstractObservationVectorStore implements
String filter = nativeExpressionFilter(request);
String queryString = String.format(QUERY_FORMAT, filter, request.getTopK(), this.config.embeddingFieldName,
String queryString = String.format(QUERY_FORMAT, filter, request.getTopK(), this.embeddingFieldName,
EMBEDDING_PARAM_NAME, DISTANCE_FIELD_NAME);
List<String> returnFields = new ArrayList<>();
this.config.metadataFields.stream().map(MetadataField::name).forEach(returnFields::add);
returnFields.add(this.config.embeddingFieldName);
returnFields.add(this.config.contentFieldName);
this.metadataFields.stream().map(MetadataField::name).forEach(returnFields::add);
returnFields.add(this.embeddingFieldName);
returnFields.add(this.contentFieldName);
returnFields.add(DISTANCE_FIELD_NAME);
var embedding = this.embeddingModel.embed(request.getQuery());
Query query = new Query(queryString).addParam(EMBEDDING_PARAM_NAME, RediSearchUtil.toByteArray(embedding))
@@ -231,7 +355,7 @@ public class RedisVectorStore extends AbstractObservationVectorStore implements
.limit(0, request.getTopK())
.dialect(2);
SearchResult result = this.jedis.ftSearch(this.config.indexName, query);
SearchResult result = this.jedis.ftSearch(this.indexName, query);
return result.getDocuments()
.stream()
.filter(d -> similarityScore(d) >= request.getSimilarityThreshold())
@@ -240,14 +364,12 @@ public class RedisVectorStore extends AbstractObservationVectorStore implements
}
private Document toDocument(redis.clients.jedis.search.Document doc) {
var id = doc.getId().substring(this.config.prefix.length());
var content = doc.hasProperty(this.config.contentFieldName) ? doc.getString(this.config.contentFieldName) : "";
Map<String, Object> metadata = this.config.metadataFields.stream()
var id = doc.getId().substring(this.prefix.length());
var content = doc.hasProperty(this.contentFieldName) ? doc.getString(this.contentFieldName) : "";
Map<String, Object> metadata = this.metadataFields.stream()
.map(MetadataField::name)
.filter(doc::hasProperty)
.collect(Collectors.toMap(Function.identity(), doc::getString));
// TODO: this seems wrong. The key is named "vector_store", but the value is the
// distance. Can we remove this after standardizing the metadata?
metadata.put(DISTANCE_FIELD_NAME, 1 - similarityScore(doc));
metadata.put(DocumentMetadata.DISTANCE.value(), 1 - similarityScore(doc));
return Document.builder().id(id).text(content).metadata(metadata).score((double) similarityScore(doc)).build();
@@ -272,12 +394,12 @@ public class RedisVectorStore extends AbstractObservationVectorStore implements
}
// If index already exists don't do anything
if (this.jedis.ftList().contains(this.config.indexName)) {
if (this.jedis.ftList().contains(this.indexName)) {
return;
}
String response = this.jedis.ftCreate(this.config.indexName,
FTCreateParams.createParams().on(IndexDataType.JSON).addPrefix(this.config.prefix), schemaFields());
String response = this.jedis.ftCreate(this.indexName,
FTCreateParams.createParams().on(IndexDataType.JSON).addPrefix(this.prefix), schemaFields());
if (!RESPONSE_OK.test(response)) {
String message = MessageFormat.format("Could not create index: {0}", response);
throw new RuntimeException(message);
@@ -290,16 +412,16 @@ public class RedisVectorStore extends AbstractObservationVectorStore implements
vectorAttrs.put("DISTANCE_METRIC", DEFAULT_DISTANCE_METRIC);
vectorAttrs.put("TYPE", VECTOR_TYPE_FLOAT32);
List<SchemaField> fields = new ArrayList<>();
fields.add(TextField.of(jsonPath(this.config.contentFieldName)).as(this.config.contentFieldName).weight(1.0));
fields.add(TextField.of(jsonPath(this.contentFieldName)).as(this.contentFieldName).weight(1.0));
fields.add(VectorField.builder()
.fieldName(jsonPath(this.config.embeddingFieldName))
.fieldName(jsonPath(this.embeddingFieldName))
.algorithm(vectorAlgorithm())
.attributes(vectorAttrs)
.as(this.config.embeddingFieldName)
.as(this.embeddingFieldName)
.build());
if (!CollectionUtils.isEmpty(this.config.metadataFields)) {
for (MetadataField field : this.config.metadataFields) {
if (!CollectionUtils.isEmpty(this.metadataFields)) {
for (MetadataField field : this.metadataFields) {
fields.add(schemaField(field));
}
}
@@ -318,7 +440,7 @@ public class RedisVectorStore extends AbstractObservationVectorStore implements
}
private VectorAlgorithm vectorAlgorithm() {
if (this.config.vectorAlgorithm == Algorithm.HSNW) {
if (this.vectorAlgorithm == Algorithm.HSNW) {
return VectorAlgorithm.HNSW;
}
return VectorAlgorithm.FLAT;
@@ -332,9 +454,9 @@ public class RedisVectorStore extends AbstractObservationVectorStore implements
public VectorStoreObservationContext.Builder createObservationContextBuilder(String operationName) {
return VectorStoreObservationContext.builder(VectorStoreProvider.REDIS.value(), operationName)
.withCollectionName(this.config.indexName)
.withCollectionName(this.indexName)
.withDimensions(this.embeddingModel.dimensions())
.withFieldName(this.config.embeddingFieldName)
.withFieldName(this.embeddingFieldName)
.withSimilarityMetric(VectorStoreSimilarityMetric.COSINE.value());
}
@@ -361,9 +483,151 @@ public class RedisVectorStore extends AbstractObservationVectorStore implements
}
public static RedisBuilder builder() {
return new RedisBuilder();
}
public static class RedisBuilder extends AbstractVectorStoreBuilder<RedisBuilder> {
private JedisPooled jedis;
private String indexName = DEFAULT_INDEX_NAME;
private String prefix = DEFAULT_PREFIX;
private String contentFieldName = DEFAULT_CONTENT_FIELD_NAME;
private String embeddingFieldName = DEFAULT_EMBEDDING_FIELD_NAME;
private Algorithm vectorAlgorithm = DEFAULT_VECTOR_ALGORITHM;
private List<MetadataField> metadataFields = new ArrayList<>();
private boolean initializeSchema = false;
private BatchingStrategy batchingStrategy = new TokenCountBatchingStrategy();
public RedisBuilder jedis(JedisPooled jedis) {
Assert.notNull(jedis, "JedisPooled must not be null");
this.jedis = jedis;
return this;
}
/**
* Sets the Redis index name.
* @param indexName the index name to use
* @return the builder instance
*/
public RedisBuilder indexName(String indexName) {
if (StringUtils.hasText(indexName)) {
this.indexName = indexName;
}
return this;
}
/**
* Sets the Redis key prefix (default: "embedding:").
* @param prefix the prefix to use
* @return the builder instance
*/
public RedisBuilder prefix(String prefix) {
if (StringUtils.hasText(prefix)) {
this.prefix = prefix;
}
return this;
}
/**
* Sets the Redis content field name.
* @param fieldName the content field name to use
* @return the builder instance
*/
public RedisBuilder contentFieldName(String fieldName) {
if (StringUtils.hasText(fieldName)) {
this.contentFieldName = fieldName;
}
return this;
}
/**
* Sets the Redis embedding field name.
* @param fieldName the embedding field name to use
* @return the builder instance
*/
public RedisBuilder embeddingFieldName(String fieldName) {
if (StringUtils.hasText(fieldName)) {
this.embeddingFieldName = fieldName;
}
return this;
}
/**
* Sets the Redis vector algorithm.
* @param algorithm the vector algorithm to use
* @return the builder instance
*/
public RedisBuilder vectorAlgorithm(Algorithm algorithm) {
if (algorithm != null) {
this.vectorAlgorithm = algorithm;
}
return this;
}
/**
* Sets the metadata fields.
* @param fields the metadata fields to include
* @return the builder instance
*/
public RedisBuilder metadataFields(MetadataField... fields) {
return metadataFields(Arrays.asList(fields));
}
/**
* Sets the metadata fields.
* @param fields the list of metadata fields to include
* @return the builder instance
*/
public RedisBuilder metadataFields(List<MetadataField> fields) {
if (fields != null && !fields.isEmpty()) {
this.metadataFields = new ArrayList<>(fields);
}
return this;
}
/**
* Sets whether to initialize the schema.
* @param initializeSchema true to initialize schema, false otherwise
* @return the builder instance
*/
public RedisBuilder initializeSchema(boolean initializeSchema) {
this.initializeSchema = initializeSchema;
return this;
}
/**
* Sets the batching strategy.
* @param batchingStrategy the strategy to use
* @return the builder instance
* @throws IllegalArgumentException if batchingStrategy is null
*/
public RedisBuilder batchingStrategy(BatchingStrategy batchingStrategy) {
Assert.notNull(batchingStrategy, "BatchingStrategy must not be null");
this.batchingStrategy = batchingStrategy;
return this;
}
@Override
public RedisVectorStore build() {
validate();
return new RedisVectorStore(this);
}
}
/**
* Configuration for the Redis vector store.
*/
@Deprecated(since = "1.0.0-M5", forRemoval = true)
public static final class RedisVectorStoreConfig {
private final String indexName;
@@ -395,19 +659,20 @@ public class RedisVectorStore extends AbstractObservationVectorStore implements
* Start building a new configuration.
* @return The entry point for creating a new configuration.
*/
@Deprecated(since = "1.0.0-M5", forRemoval = true)
public static Builder builder() {
return new Builder();
}
/**
* {@return the default config}
*/
@Deprecated(since = "1.0.0-M5", forRemoval = true)
public static RedisVectorStoreConfig defaultConfig() {
return builder().build();
}
@Deprecated(since = "1.0.0-M5", forRemoval = true)
public static final class Builder {
private String indexName = DEFAULT_INDEX_NAME;

View File

@@ -14,14 +14,14 @@
* limitations under the License.
*/
package org.springframework.ai.vectorstore;
package org.springframework.ai.vectorstore.redis;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.ai.vectorstore.RedisVectorStore.MetadataField;
import org.springframework.ai.vectorstore.redis.RedisVectorStore.MetadataField;
import org.springframework.ai.vectorstore.filter.Filter.Expression;
import org.springframework.ai.vectorstore.filter.Filter.Group;
import org.springframework.ai.vectorstore.filter.Filter.Key;
@@ -49,7 +49,7 @@ class RedisFilterExpressionConverterTests {
@Test
void testEQ() {
// country == "BG"
String vectorExpr = converter(org.springframework.ai.vectorstore.RedisVectorStore.MetadataField.tag("country"))
String vectorExpr = converter(RedisVectorStore.MetadataField.tag("country"))
.convertExpression(new Expression(EQ, new Key("country"), new Value("BG")));
assertThat(vectorExpr).isEqualTo("@country:{BG}");
}
@@ -57,8 +57,8 @@ class RedisFilterExpressionConverterTests {
@Test
void tesEqAndGte() {
// genre == "drama" AND year >= 2020
String vectorExpr = converter(org.springframework.ai.vectorstore.RedisVectorStore.MetadataField.tag("genre"),
org.springframework.ai.vectorstore.RedisVectorStore.MetadataField.numeric("year"))
String vectorExpr = converter(RedisVectorStore.MetadataField.tag("genre"),
RedisVectorStore.MetadataField.numeric("year"))
.convertExpression(new Expression(AND, new Expression(EQ, new Key("genre"), new Value("drama")),
new Expression(GTE, new Key("year"), new Value(2020))));
assertThat(vectorExpr).isEqualTo("@genre:{drama} @year:[2020 inf]");
@@ -67,18 +67,16 @@ class RedisFilterExpressionConverterTests {
@Test
void tesIn() {
// genre in ["comedy", "documentary", "drama"]
String vectorExpr = converter(org.springframework.ai.vectorstore.RedisVectorStore.MetadataField.tag("genre"))
.convertExpression(
new Expression(IN, new Key("genre"), new Value(List.of("comedy", "documentary", "drama"))));
String vectorExpr = converter(RedisVectorStore.MetadataField.tag("genre")).convertExpression(
new Expression(IN, new Key("genre"), new Value(List.of("comedy", "documentary", "drama"))));
assertThat(vectorExpr).isEqualTo("@genre:{comedy | documentary | drama}");
}
@Test
void testNe() {
// year >= 2020 OR country == "BG" AND city != "Sofia"
String vectorExpr = converter(org.springframework.ai.vectorstore.RedisVectorStore.MetadataField.numeric("year"),
org.springframework.ai.vectorstore.RedisVectorStore.MetadataField.tag("country"),
org.springframework.ai.vectorstore.RedisVectorStore.MetadataField.tag("city"))
String vectorExpr = converter(RedisVectorStore.MetadataField.numeric("year"),
RedisVectorStore.MetadataField.tag("country"), RedisVectorStore.MetadataField.tag("city"))
.convertExpression(new Expression(OR, new Expression(GTE, new Key("year"), new Value(2020)),
new Group(new Expression(AND, new Expression(EQ, new Key("country"), new Value("BG")),
new Expression(NE, new Key("city"), new Value("Sofia"))))));
@@ -88,9 +86,8 @@ class RedisFilterExpressionConverterTests {
@Test
void testGroup() {
// (year >= 2020 OR country == "BG") AND city NIN ["Sofia", "Plovdiv"]
String vectorExpr = converter(org.springframework.ai.vectorstore.RedisVectorStore.MetadataField.numeric("year"),
org.springframework.ai.vectorstore.RedisVectorStore.MetadataField.tag("country"),
org.springframework.ai.vectorstore.RedisVectorStore.MetadataField.tag("city"))
String vectorExpr = converter(RedisVectorStore.MetadataField.numeric("year"),
RedisVectorStore.MetadataField.tag("country"), RedisVectorStore.MetadataField.tag("city"))
.convertExpression(new Expression(AND,
new Group(new Expression(OR, new Expression(GTE, new Key("year"), new Value(2020)),
new Expression(EQ, new Key("country"), new Value("BG")))),
@@ -101,9 +98,8 @@ class RedisFilterExpressionConverterTests {
@Test
void tesBoolean() {
// isOpen == true AND year >= 2020 AND country IN ["BG", "NL", "US"]
String vectorExpr = converter(org.springframework.ai.vectorstore.RedisVectorStore.MetadataField.numeric("year"),
org.springframework.ai.vectorstore.RedisVectorStore.MetadataField.tag("country"),
org.springframework.ai.vectorstore.RedisVectorStore.MetadataField.tag("isOpen"))
String vectorExpr = converter(RedisVectorStore.MetadataField.numeric("year"),
RedisVectorStore.MetadataField.tag("country"), RedisVectorStore.MetadataField.tag("isOpen"))
.convertExpression(new Expression(AND,
new Expression(AND, new Expression(EQ, new Key("isOpen"), new Value(true)),
new Expression(GTE, new Key("year"), new Value(2020))),
@@ -115,8 +111,7 @@ class RedisFilterExpressionConverterTests {
@Test
void testDecimal() {
// temperature >= -15.6 && temperature <= +20.13
String vectorExpr = converter(
org.springframework.ai.vectorstore.RedisVectorStore.MetadataField.numeric("temperature"))
String vectorExpr = converter(RedisVectorStore.MetadataField.numeric("temperature"))
.convertExpression(new Expression(AND, new Expression(GTE, new Key("temperature"), new Value(-15.6)),
new Expression(LTE, new Key("temperature"), new Value(20.13))));
@@ -125,12 +120,11 @@ class RedisFilterExpressionConverterTests {
@Test
void testComplexIdentifiers() {
String vectorExpr = converter(
org.springframework.ai.vectorstore.RedisVectorStore.MetadataField.tag("country 1 2 3"))
String vectorExpr = converter(RedisVectorStore.MetadataField.tag("country 1 2 3"))
.convertExpression(new Expression(EQ, new Key("\"country 1 2 3\""), new Value("BG")));
assertThat(vectorExpr).isEqualTo("@\"country 1 2 3\":{BG}");
vectorExpr = converter(org.springframework.ai.vectorstore.RedisVectorStore.MetadataField.tag("country 1 2 3"))
vectorExpr = converter(RedisVectorStore.MetadataField.tag("country 1 2 3"))
.convertExpression(new Expression(EQ, new Key("'country 1 2 3'"), new Value("BG")));
assertThat(vectorExpr).isEqualTo("@'country 1 2 3':{BG}");
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.vectorstore;
package org.springframework.ai.vectorstore.redis;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -34,8 +34,9 @@ import redis.clients.jedis.JedisPooled;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.transformers.TransformersEmbeddingModel;
import org.springframework.ai.vectorstore.RedisVectorStore.MetadataField;
import org.springframework.ai.vectorstore.RedisVectorStore.RedisVectorStoreConfig;
import org.springframework.ai.vectorstore.redis.RedisVectorStore.MetadataField;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -255,13 +256,13 @@ class RedisVectorStoreIT {
@Bean
public RedisVectorStore vectorStore(EmbeddingModel embeddingModel,
JedisConnectionFactory jedisConnectionFactory) {
return new RedisVectorStore(
RedisVectorStoreConfig.builder()
.withMetadataFields(MetadataField.tag("meta1"), MetadataField.tag("meta2"),
MetadataField.tag("country"), MetadataField.numeric("year"))
.build(),
embeddingModel,
new JedisPooled(jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort()), true);
return RedisVectorStore.builder()
.jedis(new JedisPooled(jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort()))
.embeddingModel(embeddingModel)
.metadataFields(MetadataField.tag("meta1"), MetadataField.tag("meta2"), MetadataField.tag("country"),
MetadataField.numeric("year"))
.initializeSchema(true)
.build();
}
@Bean

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.vectorstore;
package org.springframework.ai.vectorstore.redis;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -38,8 +38,9 @@ import org.springframework.ai.observation.conventions.SpringAiKind;
import org.springframework.ai.observation.conventions.VectorStoreProvider;
import org.springframework.ai.observation.conventions.VectorStoreSimilarityMetric;
import org.springframework.ai.transformers.TransformersEmbeddingModel;
import org.springframework.ai.vectorstore.RedisVectorStore.MetadataField;
import org.springframework.ai.vectorstore.RedisVectorStore.RedisVectorStoreConfig;
import org.springframework.ai.vectorstore.redis.RedisVectorStore.MetadataField;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservationConvention;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.HighCardinalityKeyNames;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.LowCardinalityKeyNames;
@@ -175,14 +176,16 @@ public class RedisVectorStoreObservationIT {
@Bean
public RedisVectorStore vectorStore(EmbeddingModel embeddingModel,
JedisConnectionFactory jedisConnectionFactory, ObservationRegistry observationRegistry) {
return new RedisVectorStore(
RedisVectorStoreConfig.builder()
.withMetadataFields(MetadataField.tag("meta1"), MetadataField.tag("meta2"),
MetadataField.tag("country"), MetadataField.numeric("year"))
.build(),
embeddingModel,
new JedisPooled(jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort()), true,
observationRegistry, null, new TokenCountBatchingStrategy());
return RedisVectorStore.builder()
.jedis(new JedisPooled(jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort()))
.embeddingModel(embeddingModel)
.observationRegistry(observationRegistry)
.customObservationConvention(null)
.initializeSchema(true)
.batchingStrategy(new TokenCountBatchingStrategy())
.metadataFields(MetadataField.tag("meta1"), MetadataField.tag("meta2"), MetadataField.tag("country"),
MetadataField.numeric("year"))
.build();
}
@Bean