From f329d71b7be5f8aec2490d659a234aeed76dbdf8 Mon Sep 17 00:00:00 2001 From: Mark Pollack Date: Tue, 6 May 2025 18:18:21 -0400 Subject: [PATCH] Improve OpenSearch AWS/non-AWS auto-configuration and document fallback logic - Add OpenSearchNonAwsCondition to robustly activate non-AWS OpenSearch auto-configuration if either: - AWS SDK classes are missing, OR - spring.ai.vectorstore.opensearch.aws.enabled=false - Update OpenSearchVectorStoreAutoConfiguration to use the new condition for non-AWS configuration. - Ensure AWS-specific auto-configuration is only active when AWS SDKs are present and the property is true (default). - Add Javadoc to OpenSearchNonAwsCondition and clarify Javadoc on AWS config. - Update integration test (OpenSearchVectorStoreNonAwsFallbackIT) to verify fallback logic works with AWS SDKs present. - Add and refine documentation in opensearch.adoc: - Explain the purpose and usage of the spring.ai.vectorstore.opensearch.aws.enabled property. - Document the fallback logic and how to explicitly select AWS or non-AWS OpenSearch support. Fixes #1901 Signed-off-by: Mark Pollack --- .../OpenSearchNonAwsCondition.java | 72 ++++++++++++++ ...penSearchVectorStoreAutoConfiguration.java | 16 ++- ...OpenSearchVectorStoreNonAwsFallbackIT.java | 98 +++++++++++++++++++ .../ROOT/pages/api/vectordbs/opensearch.adoc | 12 +++ 4 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/main/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchNonAwsCondition.java create mode 100644 auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/test/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchVectorStoreNonAwsFallbackIT.java diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/main/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchNonAwsCondition.java b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/main/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchNonAwsCondition.java new file mode 100644 index 000000000..88692e7e9 --- /dev/null +++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/main/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchNonAwsCondition.java @@ -0,0 +1,72 @@ +/* + * Copyright 2023-2024 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.opensearch.autoconfigure; + +import org.springframework.boot.autoconfigure.condition.ConditionMessage; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * Condition that matches if either: + * + *

+ * This enables the non-AWS OpenSearch auto-configuration to be activated when the user + * disables AWS support via property or when AWS SDKs are not present, ensuring correct + * fallback behavior for non-AWS OpenSearch usage. + */ +public class OpenSearchNonAwsCondition extends SpringBootCondition { + + private static final String AWS_ENABLED_PROPERTY = "spring.ai.vectorstore.opensearch.aws.enabled"; + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + // 1. If AWS property is set to false, match + String awsEnabled = context.getEnvironment().getProperty(AWS_ENABLED_PROPERTY); + if ("false".equalsIgnoreCase(awsEnabled)) { + return ConditionOutcome.match(ConditionMessage.forCondition("OpenSearchNonAwsCondition") + .because("Property 'spring.ai.vectorstore.opensearch.aws.enabled' is false")); + } + // 2. If AWS SDK classes are missing, match + boolean awsClassesPresent = isPresent("software.amazon.awssdk.auth.credentials.AwsCredentialsProvider") + && isPresent("software.amazon.awssdk.regions.Region") + && isPresent("software.amazon.awssdk.http.apache.ApacheHttpClient"); + if (!awsClassesPresent) { + return ConditionOutcome.match( + ConditionMessage.forCondition("OpenSearchNonAwsCondition").because("AWS SDK classes are missing")); + } + // 3. Otherwise, do not match + return ConditionOutcome.noMatch(ConditionMessage.forCondition("OpenSearchNonAwsCondition") + .because("AWS SDK classes are present and property is not false")); + } + + private boolean isPresent(String className) { + try { + Class.forName(className, false, getClass().getClassLoader()); + return true; + } + catch (ClassNotFoundException ex) { + return false; + } + } + +} diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/main/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchVectorStoreAutoConfiguration.java b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/main/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchVectorStoreAutoConfiguration.java index a41dee55e..74597d1dd 100644 --- a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/main/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchVectorStoreAutoConfiguration.java +++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/main/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchVectorStoreAutoConfiguration.java @@ -94,8 +94,7 @@ public class OpenSearchVectorStoreAutoConfiguration { } @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingClass({ "software.amazon.awssdk.regions.Region", - "software.amazon.awssdk.http.apache.ApacheHttpClient" }) + @org.springframework.context.annotation.Conditional(OpenSearchNonAwsCondition.class) static class OpenSearchConfiguration { @Bean @@ -134,8 +133,21 @@ public class OpenSearchVectorStoreAutoConfiguration { } + /** + * AWS OpenSearch configuration. + *

+ * This configuration is only enabled if AWS SDK classes are present on the classpath + * and the property {@code spring.ai.vectorstore.opensearch.aws.enabled} is set + * to {@code true} (default: true). + *

+ * Set {@code spring.ai.vectorstore.opensearch.aws.enabled=false} to disable + * AWS-specific OpenSearch configuration when AWS SDK is present for other services + * (e.g., S3). + */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ AwsCredentialsProvider.class, Region.class, ApacheHttpClient.class }) + @ConditionalOnProperty(name = "spring.ai.vectorstore.opensearch.aws.enabled", havingValue = "true", + matchIfMissing = true) static class AwsOpenSearchConfiguration { @Bean diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/test/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchVectorStoreNonAwsFallbackIT.java b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/test/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchVectorStoreNonAwsFallbackIT.java new file mode 100644 index 000000000..ea51b6d0f --- /dev/null +++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/test/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchVectorStoreNonAwsFallbackIT.java @@ -0,0 +1,98 @@ +/* + * Copyright 2023-2024 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.opensearch.autoconfigure; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.opensearch.testcontainers.OpensearchContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import org.springframework.ai.document.Document; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; +import org.springframework.ai.transformers.TransformersEmbeddingModel; +import org.springframework.ai.vectorstore.opensearch.OpenSearchVectorStore; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.DefaultResourceLoader; + +import static org.assertj.core.api.Assertions.assertThat; + +@Testcontainers +class OpenSearchVectorStoreNonAwsFallbackIT { + + @Container + private static final OpensearchContainer opensearchContainer = new OpensearchContainer<>( + DockerImageName.parse("opensearchproject/opensearch:2.13.0")); + + private static final String DOCUMENT_INDEX = "nonaws-spring-ai-document-index"; + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OpenSearchVectorStoreAutoConfiguration.class, + SpringAiRetryAutoConfiguration.class)) + .withUserConfiguration(Config.class) + .withPropertyValues("spring.ai.vectorstore.opensearch.aws.enabled=false", + "spring.ai.vectorstore.opensearch.uris=" + opensearchContainer.getHttpHostAddress(), + "spring.ai.vectorstore.opensearch.indexName=" + DOCUMENT_INDEX, + "spring.ai.vectorstore.opensearch.mappingJson={\"properties\":{\"embedding\":{\"type\":\"knn_vector\",\"dimension\":384}}}"); + + private List documents = List.of( + new Document("1", getText("classpath:/test/data/spring.ai.txt"), Map.of("meta1", "meta1")), + new Document("2", getText("classpath:/test/data/time.shelter.txt"), Map.of()), + new Document("3", getText("classpath:/test/data/great.depression.txt"), Map.of("meta2", "meta2"))); + + @Test + void nonAwsFallbackConfigurationWorks() { + this.contextRunner.run(context -> { + // AWS-specific bean should NOT be present + assertThat(context.containsBeanDefinition("awsOpenSearchConnectionDetails")).isFalse(); + // Standard OpenSearch bean should be present + assertThat(context.getBeansOfType(OpenSearchConnectionDetails.class)).isNotEmpty(); + // OpenSearchVectorStore should still be present + assertThat(context.getBeansOfType(OpenSearchVectorStore.class)).isNotEmpty(); + }); + } + + private String getText(String uri) { + var resource = new DefaultResourceLoader().getResource(uri); + try { + return resource.getContentAsString(StandardCharsets.UTF_8); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + public EmbeddingModel embeddingModel() { + return new TransformersEmbeddingModel(); + } + + } + +} diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/opensearch.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/opensearch.adoc index f0c8c48a4..ceff602a9 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/opensearch.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/opensearch.adoc @@ -130,6 +130,18 @@ Properties starting with `spring.ai.vectorstore.opensearch.*` are used to config |`spring.ai.vectorstore.opensearch.aws.region`| AWS region | - |=== +[NOTE] +==== +You can control whether the AWS-specific OpenSearch auto-configuration is enabled using the `spring.ai.vectorstore.opensearch.aws.enabled` property. + +- If this property is set to `false`, the non-AWS OpenSearch configuration is activated, even if AWS SDK classes are present on the classpath. This allows you to use self-managed or third-party OpenSearch clusters in environments where AWS SDKs are present for other services. +- If AWS SDK classes are not present, the non-AWS configuration is always used. +- If AWS SDK classes are present and the property is not set or set to `true`, the AWS-specific configuration is used by default. + +This fallback logic ensures that users have explicit control over the type of OpenSearch integration, preventing accidental activation of AWS-specific logic when not desired. +==== + + The following similarity functions are available: * `cosinesimil` - Default, suitable for most use cases. Measures cosine similarity between vectors.