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 554cae00e..d77e6d795 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 @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-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. @@ -122,6 +122,10 @@ public class OpenSearchVectorStoreAutoConfiguration { httpClientBuilder.setDefaultRequestConfig(createRequestConfig(properties)); return httpClientBuilder; }); + String pathPrefix = properties.getPathPrefix(); + if (StringUtils.hasText(pathPrefix)) { + transportBuilder.setPathPrefix(pathPrefix); + } return new OpenSearchClient(transportBuilder.build()); } diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/main/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchVectorStoreProperties.java b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/main/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchVectorStoreProperties.java index 6d8bbc637..6f57e7f42 100644 --- a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/main/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchVectorStoreProperties.java +++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/main/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchVectorStoreProperties.java @@ -55,6 +55,13 @@ public class OpenSearchVectorStoreProperties extends CommonVectorStoreProperties */ private Duration readTimeout; + /** + * Path prefix for OpenSearch API endpoints. Used when OpenSearch is behind a reverse + * proxy with a non-root path. For example, if your OpenSearch instance is accessible + * at https://example.com/opensearch/, set this to "/opensearch". + */ + private String pathPrefix; + private Aws aws = new Aws(); public List getUris() { @@ -121,6 +128,14 @@ public class OpenSearchVectorStoreProperties extends CommonVectorStoreProperties this.readTimeout = readTimeout; } + public String getPathPrefix() { + return pathPrefix; + } + + public void setPathPrefix(String pathPrefix) { + this.pathPrefix = pathPrefix; + } + public Aws getAws() { return this.aws; } diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/test/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchVectorStoreAutoConfigurationIT.java b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/test/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchVectorStoreAutoConfigurationIT.java index 6ddc6f03c..afe7e1789 100644 --- a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/test/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchVectorStoreAutoConfigurationIT.java +++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-opensearch/src/test/java/org/springframework/ai/vectorstore/opensearch/autoconfigure/OpenSearchVectorStoreAutoConfigurationIT.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-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. @@ -25,6 +25,7 @@ import io.micrometer.observation.tck.TestObservationRegistry; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import org.opensearch.client.opensearch.OpenSearchClient; +import org.opensearch.client.transport.Transport; import org.opensearch.testcontainers.OpensearchContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -50,6 +51,7 @@ 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 org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.hasSize; @@ -89,7 +91,7 @@ class OpenSearchVectorStoreAutoConfigurationIT { new Document("3", getText("classpath:/test/data/great.depression.txt"), Map.of("meta2", "meta2"))); @Test - public void addAndSearchTest() { + void addAndSearchTest() { this.contextRunner.run(context -> { OpenSearchVectorStore vectorStore = context.getBean(OpenSearchVectorStore.class); @@ -148,7 +150,7 @@ class OpenSearchVectorStoreAutoConfigurationIT { } @Test - public void autoConfigurationDisabledWhenTypeIsNone() { + void autoConfigurationDisabledWhenTypeIsNone() { this.contextRunner.withPropertyValues("spring.ai.vectorstore.type=none").run(context -> { assertThat(context.getBeansOfType(OpenSearchVectorStoreProperties.class)).isEmpty(); assertThat(context.getBeansOfType(OpenSearchVectorStore.class)).isEmpty(); @@ -157,7 +159,7 @@ class OpenSearchVectorStoreAutoConfigurationIT { } @Test - public void autoConfigurationEnabledByDefault() { + void autoConfigurationEnabledByDefault() { this.contextRunner.run(context -> { assertThat(context.getBeansOfType(OpenSearchVectorStoreProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(VectorStore.class)).isNotEmpty(); @@ -166,7 +168,7 @@ class OpenSearchVectorStoreAutoConfigurationIT { } @Test - public void autoConfigurationEnabledWhenTypeIsOpensearch() { + void autoConfigurationEnabledWhenTypeIsOpensearch() { this.contextRunner.withPropertyValues("spring.ai.vectorstore.type=opensearch").run(context -> { assertThat(context.getBeansOfType(OpenSearchVectorStoreProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(VectorStore.class)).isNotEmpty(); @@ -175,7 +177,7 @@ class OpenSearchVectorStoreAutoConfigurationIT { } @Test - public void autoConfigurationWithSslBundles() { + void autoConfigurationWithSslBundles() { this.contextRunner.withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class)).run(context -> { assertThat(context.getBeansOfType(SslBundles.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenSearchClient.class)).isNotEmpty(); @@ -185,6 +187,27 @@ class OpenSearchVectorStoreAutoConfigurationIT { }); } + @Test + void testPathPrefixIsConfigured() { + this.contextRunner + .withPropertyValues(OpenSearchVectorStoreProperties.CONFIG_PREFIX + ".pathPrefix=/custom-path", + "spring.ai.vectorstore.opensearch.initialize-schema=false" // Prevent + // schema + // initialization + ) + .run(context -> { + // Verify the property is correctly set in the properties bean + OpenSearchVectorStoreProperties properties = context.getBean(OpenSearchVectorStoreProperties.class); + assertThat(properties.getPathPrefix()).isEqualTo("/custom-path"); + + // Verify the OpenSearchClient was configured with the correct pathPrefix + OpenSearchClient client = context.getBean(OpenSearchClient.class); + Transport transport = (Transport) ReflectionTestUtils.getField(client, "transport"); + String configuredPathPrefix = (String) ReflectionTestUtils.getField(transport, "pathPrefix"); + assertThat(configuredPathPrefix).isEqualTo("/custom-path"); + }); + } + private String getText(String uri) { var resource = new DefaultResourceLoader().getResource(uri); try { 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 21f2bf4f1..ea5977574 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 @@ -105,6 +105,7 @@ spring: similarity-function: cosinesimil read-timeout: