Skip to content

Commit d276d17

Browse files
eddumelendeztzolov
authored andcommitted
Add AWS OpenSearch AutoConfiguration
Currently, in order to use an OpenSearch instance provided by AWS, additional steps are needed. This commit introduces the required configuration. Add new starter and update docs
1 parent 84737ec commit d276d17

File tree

9 files changed

+378
-54
lines changed

9 files changed

+378
-54
lines changed

pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
<module>models/spring-ai-zhipuai</module>
7878
<module>models/spring-ai-moonshot</module>
7979
<module>spring-ai-spring-boot-starters/spring-ai-starter-anthropic</module>
80+
<module>spring-ai-spring-boot-starters/spring-ai-starter-aws-opensearch-store</module>
8081
<module>spring-ai-spring-boot-starters/spring-ai-starter-azure-openai</module>
8182
<module>spring-ai-spring-boot-starters/spring-ai-starter-bedrock-ai</module>
8283
<module>spring-ai-spring-boot-starters/spring-ai-starter-huggingface</module>
@@ -177,6 +178,7 @@
177178
<qdrant.version>1.9.1</qdrant.version>
178179
<typesense.version>0.5.0</typesense.version>
179180
<opensearch-client.version>2.10.1</opensearch-client.version>
181+
<awssdk.version>2.20.161</awssdk.version>
180182

181183
<!-- testing dependencies -->
182184
<httpclient5.version>5.3.1</httpclient5.version>

spring-ai-bom/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,12 @@
307307
<version>${project.version}</version>
308308
</dependency>
309309

310+
<dependency>
311+
<groupId>org.springframework.ai</groupId>
312+
<artifactId>spring-ai-aws-opensearch-store-spring-boot-starter</artifactId>
313+
<version>${project.version}</version>
314+
</dependency>
315+
310316
<dependency>
311317
<groupId>org.springframework.ai</groupId>
312318
<artifactId>spring-ai-opensearch-store-spring-boot-starter</artifactId>

spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/opensearch.adoc

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ List<Document> results = vectorStore.similaritySearch(SearchRequest.query("Sprin
114114

115115
=== Configuration properties
116116

117-
You can use the following properties in your Spring Boot configuration to customize the PGVector vector store.
117+
You can use the following properties in your Spring Boot configuration to customize the OpenSearch vector store.
118118

119119
[cols="2,5,1"]
120120
|===
@@ -134,6 +134,11 @@ fields are stored and indexed. |
134134
}
135135
}
136136
}
137+
|`spring.opensearch.aws.host`| Hostname of the OpenSearch instance. | -
138+
|`spring.opensearch.aws.service-name`| AWS service name for the OpenSearch instance. | -
139+
|`spring.opensearch.aws.access-key`| AWS access key for the OpenSearch instance. | -
140+
|`spring.opensearch.aws.secret-key`| AWS secret key for the OpenSearch instance. | -
141+
|`spring.opensearch.aws.region`| AWS region for the OpenSearch instance. | -
137142
|===
138143

139144
=== Customizing OpenSearch Client Configuration
@@ -148,9 +153,8 @@ To enable it, add the following dependency to your project's Maven `pom.xml` fil
148153
[source,xml]
149154
----
150155
<dependency>
151-
<groupId>software.amazon.awssdk</groupId>
152-
<artifactId>apache-client</artifactId>
153-
<version>2.25.40</version>
156+
<groupId>org.springframework.ai</groupId>
157+
<artifactId>spring-ai-aws-opensearch-store-spring-boot-starter</artifactId>
154158
</dependency>
155159
----
156160

@@ -159,24 +163,7 @@ or to your Gradle `build.gradle` build file.
159163
[source,groovy]
160164
----
161165
dependencies {
162-
implementation 'software.amazon.awssdk:apache-client:2.25.40'
163-
}
164-
----
165-
166-
Here is an example of the needed bean:
167-
168-
[source,java]
169-
----
170-
@Bean
171-
public OpenSearchClient openSearchClient() {
172-
return new OpenSearchClient(
173-
new AwsSdk2Transport(
174-
ApacheHttpClient.builder().build(),
175-
"search-...us-west-2.es.amazonaws.com", // OpenSearch endpoint, without https://
176-
"es",
177-
Region.US_WEST_2, // signing service region
178-
AwsSdk2TransportOptions.builder().build())
179-
);
166+
implementation 'org.springframework.ai:spring-ai-aws-opensearch-store-spring-boot-starter'
180167
}
181168
----
182169

spring-ai-spring-boot-autoconfigure/pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,13 @@
342342
<optional>true</optional>
343343
</dependency>
344344

345+
<dependency>
346+
<groupId>software.amazon.awssdk</groupId>
347+
<artifactId>apache-client</artifactId>
348+
<version>${awssdk.version}</version>
349+
<optional>true</optional>
350+
</dependency>
351+
345352
<!-- test dependencies -->
346353

347354
<dependency>
@@ -435,6 +442,12 @@
435442
<scope>test</scope>
436443
</dependency>
437444

445+
<dependency>
446+
<groupId>org.testcontainers</groupId>
447+
<artifactId>localstack</artifactId>
448+
<scope>test</scope>
449+
</dependency>
450+
438451
<dependency>
439452
<groupId>org.testcontainers</groupId>
440453
<artifactId>milvus</artifactId>

spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/opensearch/OpenSearchVectorStoreAutoConfiguration.java

Lines changed: 73 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,24 @@
2020
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
2121
import org.apache.hc.core5.http.HttpHost;
2222
import org.opensearch.client.opensearch.OpenSearchClient;
23+
import org.opensearch.client.transport.OpenSearchTransport;
24+
import org.opensearch.client.transport.aws.AwsSdk2Transport;
25+
import org.opensearch.client.transport.aws.AwsSdk2TransportOptions;
2326
import org.opensearch.client.transport.httpclient5.ApacheHttpClient5TransportBuilder;
2427
import org.springframework.ai.embedding.EmbeddingModel;
2528
import org.springframework.ai.vectorstore.OpenSearchVectorStore;
2629
import org.springframework.boot.autoconfigure.AutoConfiguration;
2730
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2831
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
32+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
2933
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3034
import org.springframework.context.annotation.Bean;
35+
import org.springframework.context.annotation.Configuration;
36+
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
37+
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
38+
import software.amazon.awssdk.http.SdkHttpClient;
39+
import software.amazon.awssdk.http.apache.ApacheHttpClient;
40+
import software.amazon.awssdk.regions.Region;
3141

3242
import java.net.URISyntaxException;
3343
import java.util.List;
@@ -48,45 +58,76 @@ PropertiesOpenSearchConnectionDetails openSearchConnectionDetails(OpenSearchVect
4858
@ConditionalOnMissingBean
4959
OpenSearchVectorStore vectorStore(OpenSearchVectorStoreProperties properties, OpenSearchClient openSearchClient,
5060
EmbeddingModel embeddingModel) {
51-
return new OpenSearchVectorStore(
52-
Optional.ofNullable(properties.getIndexName()).orElse(OpenSearchVectorStore.DEFAULT_INDEX_NAME),
53-
openSearchClient, embeddingModel, Optional.ofNullable(properties.getMappingJson())
54-
.orElse(OpenSearchVectorStore.DEFAULT_MAPPING_EMBEDDING_TYPE_KNN_VECTOR_DIMENSION_1536));
61+
var indexName = Optional.ofNullable(properties.getIndexName()).orElse(OpenSearchVectorStore.DEFAULT_INDEX_NAME);
62+
var mappingJson = Optional.ofNullable(properties.getMappingJson())
63+
.orElse(OpenSearchVectorStore.DEFAULT_MAPPING_EMBEDDING_TYPE_KNN_VECTOR_DIMENSION_1536);
64+
return new OpenSearchVectorStore(indexName, openSearchClient, embeddingModel, mappingJson);
5565
}
5666

57-
@Bean
58-
@ConditionalOnMissingBean
59-
OpenSearchClient openSearchClient(OpenSearchConnectionDetails connectionDetails) {
60-
HttpHost[] httpHosts = connectionDetails.getUris()
61-
.stream()
62-
.map(s -> createHttpHost(s))
63-
.toArray(HttpHost[]::new);
64-
ApacheHttpClient5TransportBuilder transportBuilder = ApacheHttpClient5TransportBuilder.builder(httpHosts);
65-
66-
Optional.ofNullable(connectionDetails.getUsername())
67-
.map(username -> createBasicCredentialsProvider(httpHosts[0], username, connectionDetails.getPassword()))
68-
.ifPresent(basicCredentialsProvider -> transportBuilder
69-
.setHttpClientConfigCallback(httpAsyncClientBuilder -> httpAsyncClientBuilder
70-
.setDefaultCredentialsProvider(basicCredentialsProvider)));
71-
72-
return new OpenSearchClient(transportBuilder.build());
73-
}
67+
@Configuration(proxyBeanMethods = false)
68+
@ConditionalOnMissingClass({ "software.amazon.awssdk.regions.Region",
69+
"software.amazon.awssdk.http.apache.ApacheHttpClient" })
70+
static class OpenSearchConfiguration {
71+
72+
@Bean
73+
@ConditionalOnMissingBean
74+
OpenSearchClient openSearchClient(OpenSearchVectorStoreProperties properties) {
75+
HttpHost[] httpHosts = properties.getUris().stream().map(s -> createHttpHost(s)).toArray(HttpHost[]::new);
76+
ApacheHttpClient5TransportBuilder transportBuilder = ApacheHttpClient5TransportBuilder.builder(httpHosts);
77+
78+
Optional.ofNullable(properties.getUsername())
79+
.map(username -> createBasicCredentialsProvider(httpHosts[0], username, properties.getPassword()))
80+
.ifPresent(basicCredentialsProvider -> transportBuilder
81+
.setHttpClientConfigCallback(httpAsyncClientBuilder -> httpAsyncClientBuilder
82+
.setDefaultCredentialsProvider(basicCredentialsProvider)));
83+
return new OpenSearchClient(transportBuilder.build());
84+
}
85+
86+
private BasicCredentialsProvider createBasicCredentialsProvider(HttpHost httpHost, String username,
87+
String password) {
88+
BasicCredentialsProvider basicCredentialsProvider = new BasicCredentialsProvider();
89+
basicCredentialsProvider.setCredentials(new AuthScope(httpHost),
90+
new UsernamePasswordCredentials(username, password.toCharArray()));
91+
return basicCredentialsProvider;
92+
}
93+
94+
private HttpHost createHttpHost(String s) {
95+
try {
96+
return HttpHost.create(s);
97+
}
98+
catch (URISyntaxException e) {
99+
throw new RuntimeException(e);
100+
}
101+
}
74102

75-
private BasicCredentialsProvider createBasicCredentialsProvider(HttpHost httpHost, String username,
76-
String password) {
77-
BasicCredentialsProvider basicCredentialsProvider = new BasicCredentialsProvider();
78-
basicCredentialsProvider.setCredentials(new AuthScope(httpHost),
79-
new UsernamePasswordCredentials(username, password.toCharArray()));
80-
return basicCredentialsProvider;
81103
}
82104

83-
private HttpHost createHttpHost(String s) {
84-
try {
85-
return HttpHost.create(s);
105+
@Configuration(proxyBeanMethods = false)
106+
@ConditionalOnClass({ Region.class, ApacheHttpClient.class })
107+
static class AwsOpenSearchConfiguration {
108+
109+
@Bean
110+
@ConditionalOnMissingBean
111+
OpenSearchClient openSearchClient(OpenSearchVectorStoreProperties properties, AwsSdk2TransportOptions options) {
112+
OpenSearchVectorStoreProperties.Aws aws = properties.getAws();
113+
Region region = Region.of(aws.getRegion());
114+
115+
SdkHttpClient httpClient = ApacheHttpClient.builder().build();
116+
OpenSearchTransport transport = new AwsSdk2Transport(httpClient, aws.getHost(), aws.getServiceName(),
117+
region, options);
118+
return new OpenSearchClient(transport);
86119
}
87-
catch (URISyntaxException e) {
88-
throw new RuntimeException(e);
120+
121+
@Bean
122+
@ConditionalOnMissingBean
123+
AwsSdk2TransportOptions options(OpenSearchVectorStoreProperties properties) {
124+
OpenSearchVectorStoreProperties.Aws aws = properties.getAws();
125+
return AwsSdk2TransportOptions.builder()
126+
.setCredentials(StaticCredentialsProvider
127+
.create(AwsBasicCredentials.create(aws.getAccessKey(), aws.getSecretKey())))
128+
.build();
89129
}
130+
90131
}
91132

92133
static class PropertiesOpenSearchConnectionDetails implements OpenSearchConnectionDetails {

spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/opensearch/OpenSearchVectorStoreProperties.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public class OpenSearchVectorStoreProperties {
3737

3838
private String mappingJson;
3939

40+
private Aws aws = new Aws();
41+
4042
public List<String> getUris() {
4143
return uris;
4244
}
@@ -77,4 +79,66 @@ public void setMappingJson(String mappingJson) {
7779
this.mappingJson = mappingJson;
7880
}
7981

82+
public Aws getAws() {
83+
return this.aws;
84+
}
85+
86+
public void setAws(Aws aws) {
87+
this.aws = aws;
88+
}
89+
90+
static class Aws {
91+
92+
private String host;
93+
94+
private String serviceName;
95+
96+
private String accessKey;
97+
98+
private String secretKey;
99+
100+
private String region;
101+
102+
public String getHost() {
103+
return this.host;
104+
}
105+
106+
public void setHost(String host) {
107+
this.host = host;
108+
}
109+
110+
public String getServiceName() {
111+
return this.serviceName;
112+
}
113+
114+
public void setServiceName(String serviceName) {
115+
this.serviceName = serviceName;
116+
}
117+
118+
public String getAccessKey() {
119+
return this.accessKey;
120+
}
121+
122+
public void setAccessKey(String accessKey) {
123+
this.accessKey = accessKey;
124+
}
125+
126+
public String getSecretKey() {
127+
return this.secretKey;
128+
}
129+
130+
public void setSecretKey(String secretKey) {
131+
this.secretKey = secretKey;
132+
}
133+
134+
public String getRegion() {
135+
return this.region;
136+
}
137+
138+
public void setRegion(String region) {
139+
this.region = region;
140+
}
141+
142+
}
143+
80144
}

0 commit comments

Comments
 (0)