Skip to content

Commit 37c222a

Browse files
eddumelendeztzolov
authored andcommitted
Add OpenSearch Service Connection support
Service Connection support for Docker Compose and Testcontainers.
1 parent 5844f9b commit 37c222a

File tree

15 files changed

+480
-5
lines changed

15 files changed

+480
-5
lines changed

spring-ai-docs/src/main/antora/modules/ROOT/pages/api/docker-compose.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ The following service connection factories are provided in the `spring-ai-spring
3737
| `OllamaConnectionDetails`
3838
| Containers named `ollama/ollama`
3939

40+
| `OpenSearchConnectionDetails`
41+
| Containers named `opensearchproject/opensearch`
42+
4043
| `QdrantConnectionDetails`
4144
| Containers named `qdrant/qdrant`
4245

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ The following service connection factories are provided in the `spring-ai-spring
4040
| `OllamaConnectionDetails`
4141
| Containers of type `OllamaContainer`
4242

43+
| `OpenSearchConnectionDetails`
44+
| Containers of type `OpensearchContainer`
45+
4346
| `QdrantConnectionDetails`
4447
| Containers of type `QdrantContainer`
4548

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2023 - 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.ai.autoconfigure.vectorstore.opensearch;
17+
18+
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
19+
20+
import java.util.List;
21+
22+
public interface OpenSearchConnectionDetails extends ConnectionDetails {
23+
24+
List<String> getUris();
25+
26+
String getUsername();
27+
28+
String getPassword();
29+
30+
}

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

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,19 @@
3030
import org.springframework.context.annotation.Bean;
3131

3232
import java.net.URISyntaxException;
33+
import java.util.List;
3334
import java.util.Optional;
3435

3536
@AutoConfiguration
3637
@ConditionalOnClass({ OpenSearchVectorStore.class, EmbeddingModel.class, OpenSearchClient.class })
3738
@EnableConfigurationProperties(OpenSearchVectorStoreProperties.class)
38-
class OpenSearchVectorStoreAutoConfiguration {
39+
public class OpenSearchVectorStoreAutoConfiguration {
40+
41+
@Bean
42+
@ConditionalOnMissingBean(OpenSearchConnectionDetails.class)
43+
PropertiesOpenSearchConnectionDetails openSearchConnectionDetails(OpenSearchVectorStoreProperties properties) {
44+
return new PropertiesOpenSearchConnectionDetails(properties);
45+
}
3946

4047
@Bean
4148
@ConditionalOnMissingBean
@@ -49,12 +56,15 @@ OpenSearchVectorStore vectorStore(OpenSearchVectorStoreProperties properties, Op
4956

5057
@Bean
5158
@ConditionalOnMissingBean
52-
OpenSearchClient openSearchClient(OpenSearchVectorStoreProperties properties) {
53-
HttpHost[] httpHosts = properties.getUris().stream().map(s -> createHttpHost(s)).toArray(HttpHost[]::new);
59+
OpenSearchClient openSearchClient(OpenSearchConnectionDetails connectionDetails) {
60+
HttpHost[] httpHosts = connectionDetails.getUris()
61+
.stream()
62+
.map(s -> createHttpHost(s))
63+
.toArray(HttpHost[]::new);
5464
ApacheHttpClient5TransportBuilder transportBuilder = ApacheHttpClient5TransportBuilder.builder(httpHosts);
5565

56-
Optional.ofNullable(properties.getUsername())
57-
.map(username -> createBasicCredentialsProvider(httpHosts[0], username, properties.getPassword()))
66+
Optional.ofNullable(connectionDetails.getUsername())
67+
.map(username -> createBasicCredentialsProvider(httpHosts[0], username, connectionDetails.getPassword()))
5868
.ifPresent(basicCredentialsProvider -> transportBuilder
5969
.setHttpClientConfigCallback(httpAsyncClientBuilder -> httpAsyncClientBuilder
6070
.setDefaultCredentialsProvider(basicCredentialsProvider)));
@@ -79,4 +89,29 @@ private HttpHost createHttpHost(String s) {
7989
}
8090
}
8191

92+
static class PropertiesOpenSearchConnectionDetails implements OpenSearchConnectionDetails {
93+
94+
private final OpenSearchVectorStoreProperties properties;
95+
96+
PropertiesOpenSearchConnectionDetails(OpenSearchVectorStoreProperties properties) {
97+
this.properties = properties;
98+
}
99+
100+
@Override
101+
public List<String> getUris() {
102+
return this.properties.getUris();
103+
}
104+
105+
@Override
106+
public String getUsername() {
107+
return this.properties.getUsername();
108+
}
109+
110+
@Override
111+
public String getPassword() {
112+
return this.properties.getPassword();
113+
}
114+
115+
}
116+
82117
}

spring-ai-spring-boot-docker-compose/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,14 @@
115115
<optional>true</optional>
116116
</dependency>
117117

118+
<!-- OpenSearch Vector Store-->
119+
<dependency>
120+
<groupId>org.springframework.ai</groupId>
121+
<artifactId>spring-ai-opensearch-store</artifactId>
122+
<version>${project.parent.version}</version>
123+
<optional>true</optional>
124+
</dependency>
125+
118126
<!-- test dependencies -->
119127

120128
<dependency>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2023 - 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.ai.docker.compose.service.connection.opensearch;
17+
18+
import org.springframework.ai.autoconfigure.vectorstore.opensearch.OpenSearchConnectionDetails;
19+
import org.springframework.boot.docker.compose.core.RunningService;
20+
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
21+
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
22+
23+
import java.util.List;
24+
25+
/**
26+
* @author Eddú Meléndez
27+
*/
28+
class OpenSearchDockerComposeConnectionDetailsFactory
29+
extends DockerComposeConnectionDetailsFactory<OpenSearchConnectionDetails> {
30+
31+
private static final int OPENSEARCH_PORT = 9200;
32+
33+
protected OpenSearchDockerComposeConnectionDetailsFactory() {
34+
super("opensearchproject/opensearch");
35+
}
36+
37+
@Override
38+
protected OpenSearchConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
39+
return new OpenSearchDockerComposeConnectionDetails(source.getRunningService());
40+
}
41+
42+
/**
43+
* {@link OpenSearchConnectionDetails} backed by a {@code OpenSearch}
44+
* {@link RunningService}.
45+
*/
46+
static class OpenSearchDockerComposeConnectionDetails extends DockerComposeConnectionDetails
47+
implements OpenSearchConnectionDetails {
48+
49+
private final OpenSearchEnvironment environment;
50+
51+
private final String uri;
52+
53+
OpenSearchDockerComposeConnectionDetails(RunningService service) {
54+
super(service);
55+
this.environment = new OpenSearchEnvironment(service.env());
56+
this.uri = "http://" + service.host() + ":" + service.ports().get(OPENSEARCH_PORT);
57+
}
58+
59+
@Override
60+
public List<String> getUris() {
61+
return List.of(this.uri);
62+
}
63+
64+
@Override
65+
public String getUsername() {
66+
return "admin";
67+
}
68+
69+
@Override
70+
public String getPassword() {
71+
return this.environment.getPassword();
72+
}
73+
74+
}
75+
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2023 - 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.ai.docker.compose.service.connection.opensearch;
17+
18+
import java.util.Map;
19+
20+
class OpenSearchEnvironment {
21+
22+
private final String password;
23+
24+
OpenSearchEnvironment(Map<String, String> env) {
25+
this.password = env.get("OPENSEARCH_INITIAL_ADMIN_PASSWORD");
26+
}
27+
28+
String getPassword() {
29+
return this.password;
30+
}
31+
32+
}

spring-ai-spring-boot-docker-compose/src/main/resources/META-INF/spring.factories

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\
22
org.springframework.ai.docker.compose.service.connection.chroma.ChromaDockerComposeConnectionDetailsFactory,\
33
org.springframework.ai.docker.compose.service.connection.ollama.OllamaDockerComposeConnectionDetailsFactory,\
4+
org.springframework.ai.docker.compose.service.connection.opensearch.OpenSearchDockerComposeConnectionDetailsFactory,\
45
org.springframework.ai.docker.compose.service.connection.qdrant.QdrantDockerComposeConnectionDetailsFactory,\
56
org.springframework.ai.docker.compose.service.connection.redis.RedisDockerComposeConnectionDetailsFactory,\
67
org.springframework.ai.docker.compose.service.connection.typesense.TypesenseDockerComposeConnectionDetailsFactory,\
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2023 - 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.ai.docker.compose.service.connection.opensearch;
17+
18+
import org.junit.jupiter.api.Test;
19+
import org.springframework.ai.autoconfigure.vectorstore.opensearch.OpenSearchConnectionDetails;
20+
import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests;
21+
import org.testcontainers.utility.DockerImageName;
22+
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
25+
class OpenSearchDockerComposeConnectionDetailsFactoryTests extends AbstractDockerComposeIntegrationTests {
26+
27+
OpenSearchDockerComposeConnectionDetailsFactoryTests() {
28+
super("opensearch-compose.yaml", DockerImageName.parse("opensearchproject/opensearch"));
29+
}
30+
31+
@Test
32+
void runCreatesConnectionDetails() {
33+
OpenSearchConnectionDetails connectionDetails = run(OpenSearchConnectionDetails.class);
34+
assertThat(connectionDetails.getUris()).isNotNull();
35+
assertThat(connectionDetails.getUsername()).isEqualTo("admin");
36+
assertThat(connectionDetails.getPassword()).isEqualTo("D3v3l0p-ment");
37+
}
38+
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2023 - 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.ai.docker.compose.service.connection.opensearch;
17+
18+
import org.junit.jupiter.api.Test;
19+
20+
import java.util.Collections;
21+
import java.util.Map;
22+
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
25+
class OpenSearchEnvironmentTests {
26+
27+
@Test
28+
void getPasswordWhenNoPassword() {
29+
OpenSearchEnvironment environment = new OpenSearchEnvironment(Collections.emptyMap());
30+
assertThat(environment.getPassword()).isNull();
31+
}
32+
33+
@Test
34+
void getPasswordWhenHasPassword() {
35+
OpenSearchEnvironment environment = new OpenSearchEnvironment(
36+
Map.of("OPENSEARCH_INITIAL_ADMIN_PASSWORD", "secret"));
37+
assertThat(environment.getPassword()).isEqualTo("secret");
38+
}
39+
40+
}

0 commit comments

Comments
 (0)