Skip to content

fix: GH-3690, enhance RedisVectorStore with RedisVectorStoreBuilderCustomizer #3809

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.springframework.ai.vectorstore.SpringAIVectorStoreTypes;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention;
import org.springframework.ai.vectorstore.redis.RedisVectorStore;
import org.springframework.ai.vectorstore.redis.RedisVectorStoreBuilderCustomizer;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
Expand All @@ -46,6 +47,7 @@
* @author Eddú Meléndez
* @author Soby Chacko
* @author Jihoon Kim
* @author Pengfei Lan
*/
@AutoConfiguration(after = RedisAutoConfiguration.class)
@ConditionalOnClass({ JedisPooled.class, JedisConnectionFactory.class, RedisVectorStore.class, EmbeddingModel.class })
Expand All @@ -63,20 +65,29 @@ BatchingStrategy batchingStrategy() {

@Bean
@ConditionalOnMissingBean
public RedisVectorStore vectorStore(EmbeddingModel embeddingModel, RedisVectorStoreProperties properties,
JedisConnectionFactory jedisConnectionFactory, ObjectProvider<ObservationRegistry> observationRegistry,
public RedisVectorStore.Builder vectorStoreBuilder(EmbeddingModel embeddingModel,
RedisVectorStoreProperties properties, JedisConnectionFactory jedisConnectionFactory,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<VectorStoreObservationConvention> customObservationConvention,
BatchingStrategy batchingStrategy) {
BatchingStrategy batchingStrategy,
ObjectProvider<RedisVectorStoreBuilderCustomizer> vectorStoreBuilderCustomizers) {

JedisPooled jedisPooled = this.jedisPooled(jedisConnectionFactory);
return RedisVectorStore.builder(jedisPooled, embeddingModel)
RedisVectorStore.Builder builder = RedisVectorStore.builder(jedisPooled, embeddingModel)
.initializeSchema(properties.isInitializeSchema())
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
.customObservationConvention(customObservationConvention.getIfAvailable(() -> null))
.batchingStrategy(batchingStrategy)
.indexName(properties.getIndexName())
.prefix(properties.getPrefix())
.build();
.prefix(properties.getPrefix());
vectorStoreBuilderCustomizers.orderedStream().forEach(customizer -> customizer.customize(builder));
return builder;
}

@Bean
@ConditionalOnMissingBean
public RedisVectorStore vectorStore(RedisVectorStore.Builder vectorStoreBuilder) {
return vectorStoreBuilder.build();
}

private JedisPooled jedisPooled(JedisConnectionFactory jedisConnectionFactory) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
import org.springframework.ai.vectorstore.redis.RedisVectorStore;
import org.springframework.ai.vectorstore.redis.RedisVectorStoreBuilderCustomizer;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
Expand All @@ -49,6 +50,7 @@
* @author Soby Chacko
* @author Christian Tzolov
* @author Thomas Vitale
* @author Dongha Koo
*/
@Testcontainers
class RedisVectorStoreAutoConfigurationIT {
Expand Down Expand Up @@ -134,6 +136,18 @@ public void autoConfigurationEnabledWhenTypeIsRedis() {
});
}

@Test
void customizerShouldApplyMetadataField() {
this.contextRunner
.withBean(RedisVectorStoreBuilderCustomizer.class,
() -> builder -> builder.metadataFields(RedisVectorStore.MetadataField.tag("customField")))
.run(context -> {
RedisVectorStore vectorStore = context.getBean(RedisVectorStore.class);
List<RedisVectorStore.MetadataField> metadataFields = vectorStore.getMetadataFields();
assertThat(metadataFields).extracting(RedisVectorStore.MetadataField::name).contains("customField");
});
}

@Configuration(proxyBeanMethods = false)
static class Config {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.ai.vectorstore;

/**
* Callback interface for customizing {@link VectorStore.Builder} instances.
* <p>
* Implemented by Spring beans that wish to configure {@code VectorStore} builders before
* they are constructed. Customizers are applied in declaration order (or priority order
* if {@link org.springframework.core.Ordered} is implemented), allowing incremental
* builder configuration while preserving auto-configuration.
*
* <h3>Typical Use Cases</h3>
* <ul>
* <li>Adding custom metadata fields to vector stores</li>
* <li>Configuring observation behaviors</li>
* </ul>
*
* @param <T> the specific type of {@link VectorStore.Builder} being customized
* @author Pengfei Lan
* @see VectorStore
* @see VectorStore.Builder
* @see org.springframework.core.Ordered
*/
@FunctionalInterface
public interface VectorStoreBuilderCustomizer<T extends VectorStore.Builder<T>> {

/**
* Customizes the given {@link VectorStore.Builder} instance.
* @param builder the builder to configure (never {@code null})
*/
void customize(T builder);

}
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,21 @@
* <li>NUMERIC: For range queries on numerical data</li>
* </ul>
*
* <p>
* Introspection:
* </p>
* <ul>
* <li>{@link #getMetadataFields()} can be used to inspect which metadata fields are
* registered in the store.</li>
* </ul>
*
* @author Julien Ruaux
* @author Christian Tzolov
* @author Eddú Meléndez
* @author Thomas Vitale
* @author Soby Chacko
* @author Jihoon Kim
* @author Dongha Koo
* @see VectorStore
* @see EmbeddingModel
* @since 1.0.0
Expand Down Expand Up @@ -471,6 +480,14 @@ public <T> Optional<T> getNativeClient() {
return Optional.of(client);
}

/**
* Returns the metadata fields used by this RedisVectorStore.
* @return list of configured metadata fields
*/
public List<MetadataField> getMetadataFields() {
return this.metadataFields;
}

public static Builder builder(JedisPooled jedis, EmbeddingModel embeddingModel) {
return new Builder(jedis, embeddingModel);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.ai.vectorstore.redis;

import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.VectorStoreBuilderCustomizer;

/**
* Customizer interface for {@link RedisVectorStore.Builder} configuration.
* <p>
* Allows customization of Redis-based {@link VectorStore} configuration while preserving
* default auto-configuration. Implementations can add additional settings or modify
* existing ones without overriding the entire configuration.
*
* <h3>Usage Example</h3> The following example shows how to add custom metadata fields:
* <pre>{@code
* &#64;Bean
* public RedisVectorStoreBuilderCustomizer metadataCustomizer() {
* return builder -> builder.metadataFields(
* List.of(RedisVectorStore.MetadataField.tag("conversationId"))
* );
* }
* }</pre>
*
* @author Pengfei Lan
* @see RedisVectorStore
* @see VectorStore
*/
public interface RedisVectorStoreBuilderCustomizer extends VectorStoreBuilderCustomizer<RedisVectorStore.Builder> {

}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
* @author Eddú Meléndez
* @author Thomas Vitale
* @author Soby Chacko
* @author Dongha Koo
*/
@Testcontainers
class RedisVectorStoreIT extends BaseVectorStoreTests {
Expand Down Expand Up @@ -316,6 +317,18 @@ void getNativeClientTest() {
});
}

@Test
void customizerShouldBeAppliedToBuilder() {
this.contextRunner
.withBean(RedisVectorStoreBuilderCustomizer.class,
() -> builder -> builder.metadataFields(MetadataField.tag("customField")))
.run(context -> {
RedisVectorStore vectorStore = context.getBean(RedisVectorStore.class);
List<MetadataField> metadataFields = vectorStore.getMetadataFields();
assertThat(metadataFields).extracting(MetadataField::name).contains("customField");
});
}

@SpringBootConfiguration
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })
public static class TestApplication {
Expand Down