diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-redis/src/main/java/org/springframework/ai/vectorstore/redis/autoconfigure/RedisVectorStoreAutoConfiguration.java b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-redis/src/main/java/org/springframework/ai/vectorstore/redis/autoconfigure/RedisVectorStoreAutoConfiguration.java index f332752faa1..21b77187a78 100644 --- a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-redis/src/main/java/org/springframework/ai/vectorstore/redis/autoconfigure/RedisVectorStoreAutoConfiguration.java +++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-redis/src/main/java/org/springframework/ai/vectorstore/redis/autoconfigure/RedisVectorStoreAutoConfiguration.java @@ -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; @@ -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 }) @@ -63,20 +65,29 @@ BatchingStrategy batchingStrategy() { @Bean @ConditionalOnMissingBean - public RedisVectorStore vectorStore(EmbeddingModel embeddingModel, RedisVectorStoreProperties properties, - JedisConnectionFactory jedisConnectionFactory, ObjectProvider observationRegistry, + public RedisVectorStore.Builder vectorStoreBuilder(EmbeddingModel embeddingModel, + RedisVectorStoreProperties properties, JedisConnectionFactory jedisConnectionFactory, + ObjectProvider observationRegistry, ObjectProvider customObservationConvention, - BatchingStrategy batchingStrategy) { + BatchingStrategy batchingStrategy, + ObjectProvider 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) { diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-redis/src/test/java/org/springframework/ai/vectorstore/redis/autoconfigure/RedisVectorStoreAutoConfigurationIT.java b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-redis/src/test/java/org/springframework/ai/vectorstore/redis/autoconfigure/RedisVectorStoreAutoConfigurationIT.java index 40d3bce6e93..04912a30b04 100644 --- a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-redis/src/test/java/org/springframework/ai/vectorstore/redis/autoconfigure/RedisVectorStoreAutoConfigurationIT.java +++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-redis/src/test/java/org/springframework/ai/vectorstore/redis/autoconfigure/RedisVectorStoreAutoConfigurationIT.java @@ -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; @@ -49,6 +50,7 @@ * @author Soby Chacko * @author Christian Tzolov * @author Thomas Vitale + * @author Dongha Koo */ @Testcontainers class RedisVectorStoreAutoConfigurationIT { @@ -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 metadataFields = vectorStore.getMetadataFields(); + assertThat(metadataFields).extracting(RedisVectorStore.MetadataField::name).contains("customField"); + }); + } + @Configuration(proxyBeanMethods = false) static class Config { diff --git a/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/VectorStoreBuilderCustomizer.java b/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/VectorStoreBuilderCustomizer.java new file mode 100644 index 00000000000..959e8939fd6 --- /dev/null +++ b/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/VectorStoreBuilderCustomizer.java @@ -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. + *

+ * 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. + * + *

Typical Use Cases

+ *
    + *
  • Adding custom metadata fields to vector stores
  • + *
  • Configuring observation behaviors
  • + *
+ * + * @param 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> { + + /** + * Customizes the given {@link VectorStore.Builder} instance. + * @param builder the builder to configure (never {@code null}) + */ + void customize(T builder); + +} diff --git a/vector-stores/spring-ai-redis-store/src/main/java/org/springframework/ai/vectorstore/redis/RedisVectorStore.java b/vector-stores/spring-ai-redis-store/src/main/java/org/springframework/ai/vectorstore/redis/RedisVectorStore.java index 67d033fb2cf..1c442652d36 100644 --- a/vector-stores/spring-ai-redis-store/src/main/java/org/springframework/ai/vectorstore/redis/RedisVectorStore.java +++ b/vector-stores/spring-ai-redis-store/src/main/java/org/springframework/ai/vectorstore/redis/RedisVectorStore.java @@ -169,12 +169,21 @@ *
  • NUMERIC: For range queries on numerical data
  • * * + *

    + * Introspection: + *

    + *
      + *
    • {@link #getMetadataFields()} can be used to inspect which metadata fields are + * registered in the store.
    • + *
    + * * @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 @@ -471,6 +480,14 @@ public Optional getNativeClient() { return Optional.of(client); } + /** + * Returns the metadata fields used by this RedisVectorStore. + * @return list of configured metadata fields + */ + public List getMetadataFields() { + return this.metadataFields; + } + public static Builder builder(JedisPooled jedis, EmbeddingModel embeddingModel) { return new Builder(jedis, embeddingModel); } diff --git a/vector-stores/spring-ai-redis-store/src/main/java/org/springframework/ai/vectorstore/redis/RedisVectorStoreBuilderCustomizer.java b/vector-stores/spring-ai-redis-store/src/main/java/org/springframework/ai/vectorstore/redis/RedisVectorStoreBuilderCustomizer.java new file mode 100644 index 00000000000..c1594a6fb6a --- /dev/null +++ b/vector-stores/spring-ai-redis-store/src/main/java/org/springframework/ai/vectorstore/redis/RedisVectorStoreBuilderCustomizer.java @@ -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. + *

    + * 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. + * + *

    Usage Example

    The following example shows how to add custom metadata fields: + *
    {@code
    + * @Bean
    + * public RedisVectorStoreBuilderCustomizer metadataCustomizer() {
    + *     return builder -> builder.metadataFields(
    + *         List.of(RedisVectorStore.MetadataField.tag("conversationId"))
    + *     );
    + * }
    + * }
    + * + * @author Pengfei Lan + * @see RedisVectorStore + * @see VectorStore + */ +public interface RedisVectorStoreBuilderCustomizer extends VectorStoreBuilderCustomizer { + +} diff --git a/vector-stores/spring-ai-redis-store/src/test/java/org/springframework/ai/vectorstore/redis/RedisVectorStoreIT.java b/vector-stores/spring-ai-redis-store/src/test/java/org/springframework/ai/vectorstore/redis/RedisVectorStoreIT.java index 80b2b304614..d602941826e 100644 --- a/vector-stores/spring-ai-redis-store/src/test/java/org/springframework/ai/vectorstore/redis/RedisVectorStoreIT.java +++ b/vector-stores/spring-ai-redis-store/src/test/java/org/springframework/ai/vectorstore/redis/RedisVectorStoreIT.java @@ -59,6 +59,7 @@ * @author Eddú Meléndez * @author Thomas Vitale * @author Soby Chacko + * @author Dongha Koo */ @Testcontainers class RedisVectorStoreIT extends BaseVectorStoreTests { @@ -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 metadataFields = vectorStore.getMetadataFields(); + assertThat(metadataFields).extracting(MetadataField::name).contains("customField"); + }); + } + @SpringBootConfiguration @EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class }) public static class TestApplication {