GH-886: feat(opensearch): Add path prefix configuration support

Fixes: #886

Add ability to configure path prefix for OpenSearch API endpoints via properties.
This allows connecting to OpenSearch instances running behind a reverse proxy
with a non-root path, similar to Spring Elasticsearch's path-prefix property.

- Add pathPrefix property to OpenSearchVectorStoreProperties
- Apply pathPrefix to OpenSearchClient when configured
- Add documentation and unit tests

Signed-off-by: Soby Chacko <soby.chacko@broadcom.com>
This commit is contained in:
Soby Chacko
2025-05-16 20:38:05 -04:00
committed by Ilayaperumal Gopinathan
parent 0bbccf8ca0
commit 8b8fb4f02d
4 changed files with 56 additions and 7 deletions

View File

@@ -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());
}

View File

@@ -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<String> 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;
}

View File

@@ -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 {

View File

@@ -105,6 +105,7 @@ spring:
similarity-function: cosinesimil
read-timeout: <time to wait for response>
connect-timeout: <time to wait until connection established>
path-prefix: <custom path prefix>
ssl-bundle: <name of SSL bundle>
aws: # Only for Amazon OpenSearch Service
host: <aws opensearch host>
@@ -128,6 +129,7 @@ Properties starting with `spring.ai.vectorstore.opensearch.*` are used to config
|`spring.ai.vectorstore.opensearch.similarity-function`| The similarity function to use | `cosinesimil`
|`spring.ai.vectorstore.opensearch.read-timeout`| Time to wait for response from the opposite endpoint. 0 - infinity. | -
|`spring.ai.vectorstore.opensearch.connect-timeout`| Time to wait until connection established. 0 - infinity. | -
|`spring.ai.vectorstore.opensearch.path-prefix`| Path prefix for OpenSearch API endpoints. Useful when OpenSearch is behind a reverse proxy with a non-root path. | -
|`spring.ai.vectorstore.opensearch.ssl-bundle`| Name of the SSL Bundle to use in case of SSL connection | -
|`spring.ai.vectorstore.opensearch.aws.host`| Hostname of the OpenSearch instance | -
|`spring.ai.vectorstore.opensearch.aws.service-name`| AWS service name | -
@@ -147,6 +149,11 @@ You can control whether the AWS-specific OpenSearch auto-configuration is enable
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.
====
[NOTE]
====
The `path-prefix` property allows you to specify a custom path prefix when OpenSearch is running behind a reverse proxy that uses a non-root path.
For example, if your OpenSearch instance is accessible at `https://example.com/opensearch/` instead of `https://example.com/`, you would set `path-prefix: /opensearch`.
====
The following similarity functions are available: