diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/docker-compose.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/docker-compose.adoc index 7d19d0eda..432a06e4c 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/docker-compose.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/docker-compose.adoc @@ -37,6 +37,9 @@ The following service connection factories are provided in the `spring-ai-spring | `OllamaConnectionDetails` | Containers named `ollama/ollama` +| `OpenSearchConnectionDetails` +| Containers named `opensearchproject/opensearch` + | `QdrantConnectionDetails` | Containers named `qdrant/qdrant` diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/testcontainers.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/testcontainers.adoc index 5812e5af7..c1966544a 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/testcontainers.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/testcontainers.adoc @@ -40,6 +40,9 @@ The following service connection factories are provided in the `spring-ai-spring | `OllamaConnectionDetails` | Containers of type `OllamaContainer` +| `OpenSearchConnectionDetails` +| Containers of type `OpensearchContainer` + | `QdrantConnectionDetails` | Containers of type `QdrantContainer` diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/opensearch/OpenSearchConnectionDetails.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/opensearch/OpenSearchConnectionDetails.java new file mode 100644 index 000000000..39c3b4c34 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/opensearch/OpenSearchConnectionDetails.java @@ -0,0 +1,30 @@ +/* + * 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.autoconfigure.vectorstore.opensearch; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +import java.util.List; + +public interface OpenSearchConnectionDetails extends ConnectionDetails { + + List getUris(); + + String getUsername(); + + String getPassword(); + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/opensearch/OpenSearchVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/opensearch/OpenSearchVectorStoreAutoConfiguration.java index b005a8400..1763aabc0 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/opensearch/OpenSearchVectorStoreAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/opensearch/OpenSearchVectorStoreAutoConfiguration.java @@ -30,12 +30,19 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean; import java.net.URISyntaxException; +import java.util.List; import java.util.Optional; @AutoConfiguration @ConditionalOnClass({ OpenSearchVectorStore.class, EmbeddingModel.class, OpenSearchClient.class }) @EnableConfigurationProperties(OpenSearchVectorStoreProperties.class) -class OpenSearchVectorStoreAutoConfiguration { +public class OpenSearchVectorStoreAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(OpenSearchConnectionDetails.class) + PropertiesOpenSearchConnectionDetails openSearchConnectionDetails(OpenSearchVectorStoreProperties properties) { + return new PropertiesOpenSearchConnectionDetails(properties); + } @Bean @ConditionalOnMissingBean @@ -49,12 +56,15 @@ class OpenSearchVectorStoreAutoConfiguration { @Bean @ConditionalOnMissingBean - OpenSearchClient openSearchClient(OpenSearchVectorStoreProperties properties) { - HttpHost[] httpHosts = properties.getUris().stream().map(s -> createHttpHost(s)).toArray(HttpHost[]::new); + OpenSearchClient openSearchClient(OpenSearchConnectionDetails connectionDetails) { + HttpHost[] httpHosts = connectionDetails.getUris() + .stream() + .map(s -> createHttpHost(s)) + .toArray(HttpHost[]::new); ApacheHttpClient5TransportBuilder transportBuilder = ApacheHttpClient5TransportBuilder.builder(httpHosts); - Optional.ofNullable(properties.getUsername()) - .map(username -> createBasicCredentialsProvider(httpHosts[0], username, properties.getPassword())) + Optional.ofNullable(connectionDetails.getUsername()) + .map(username -> createBasicCredentialsProvider(httpHosts[0], username, connectionDetails.getPassword())) .ifPresent(basicCredentialsProvider -> transportBuilder .setHttpClientConfigCallback(httpAsyncClientBuilder -> httpAsyncClientBuilder .setDefaultCredentialsProvider(basicCredentialsProvider))); @@ -79,4 +89,29 @@ class OpenSearchVectorStoreAutoConfiguration { } } + static class PropertiesOpenSearchConnectionDetails implements OpenSearchConnectionDetails { + + private final OpenSearchVectorStoreProperties properties; + + PropertiesOpenSearchConnectionDetails(OpenSearchVectorStoreProperties properties) { + this.properties = properties; + } + + @Override + public List getUris() { + return this.properties.getUris(); + } + + @Override + public String getUsername() { + return this.properties.getUsername(); + } + + @Override + public String getPassword() { + return this.properties.getPassword(); + } + + } + } diff --git a/spring-ai-spring-boot-docker-compose/pom.xml b/spring-ai-spring-boot-docker-compose/pom.xml index 1728041dd..d3b530ca2 100644 --- a/spring-ai-spring-boot-docker-compose/pom.xml +++ b/spring-ai-spring-boot-docker-compose/pom.xml @@ -115,6 +115,14 @@ true + + + org.springframework.ai + spring-ai-opensearch-store + ${project.parent.version} + true + + diff --git a/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/opensearch/OpenSearchDockerComposeConnectionDetailsFactory.java b/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/opensearch/OpenSearchDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000..718112524 --- /dev/null +++ b/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/opensearch/OpenSearchDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,76 @@ +/* + * 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.docker.compose.service.connection.opensearch; + +import org.springframework.ai.autoconfigure.vectorstore.opensearch.OpenSearchConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; + +import java.util.List; + +/** + * @author Eddú Meléndez + */ +class OpenSearchDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + private static final int OPENSEARCH_PORT = 9200; + + protected OpenSearchDockerComposeConnectionDetailsFactory() { + super("opensearchproject/opensearch"); + } + + @Override + protected OpenSearchConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new OpenSearchDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link OpenSearchConnectionDetails} backed by a {@code OpenSearch} + * {@link RunningService}. + */ + static class OpenSearchDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements OpenSearchConnectionDetails { + + private final OpenSearchEnvironment environment; + + private final String uri; + + OpenSearchDockerComposeConnectionDetails(RunningService service) { + super(service); + this.environment = new OpenSearchEnvironment(service.env()); + this.uri = "http://" + service.host() + ":" + service.ports().get(OPENSEARCH_PORT); + } + + @Override + public List getUris() { + return List.of(this.uri); + } + + @Override + public String getUsername() { + return "admin"; + } + + @Override + public String getPassword() { + return this.environment.getPassword(); + } + + } + +} diff --git a/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/opensearch/OpenSearchEnvironment.java b/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/opensearch/OpenSearchEnvironment.java new file mode 100644 index 000000000..56adc0afe --- /dev/null +++ b/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/opensearch/OpenSearchEnvironment.java @@ -0,0 +1,32 @@ +/* + * 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.docker.compose.service.connection.opensearch; + +import java.util.Map; + +class OpenSearchEnvironment { + + private final String password; + + OpenSearchEnvironment(Map env) { + this.password = env.get("OPENSEARCH_INITIAL_ADMIN_PASSWORD"); + } + + String getPassword() { + return this.password; + } + +} diff --git a/spring-ai-spring-boot-docker-compose/src/main/resources/META-INF/spring.factories b/spring-ai-spring-boot-docker-compose/src/main/resources/META-INF/spring.factories index 27068f889..c0ee2af6b 100644 --- a/spring-ai-spring-boot-docker-compose/src/main/resources/META-INF/spring.factories +++ b/spring-ai-spring-boot-docker-compose/src/main/resources/META-INF/spring.factories @@ -1,6 +1,7 @@ org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ org.springframework.ai.docker.compose.service.connection.chroma.ChromaDockerComposeConnectionDetailsFactory,\ org.springframework.ai.docker.compose.service.connection.ollama.OllamaDockerComposeConnectionDetailsFactory,\ +org.springframework.ai.docker.compose.service.connection.opensearch.OpenSearchDockerComposeConnectionDetailsFactory,\ org.springframework.ai.docker.compose.service.connection.qdrant.QdrantDockerComposeConnectionDetailsFactory,\ org.springframework.ai.docker.compose.service.connection.redis.RedisDockerComposeConnectionDetailsFactory,\ org.springframework.ai.docker.compose.service.connection.typesense.TypesenseDockerComposeConnectionDetailsFactory,\ diff --git a/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/opensearch/OpenSearchDockerComposeConnectionDetailsFactoryTests.java b/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/opensearch/OpenSearchDockerComposeConnectionDetailsFactoryTests.java new file mode 100644 index 000000000..e50a8655d --- /dev/null +++ b/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/opensearch/OpenSearchDockerComposeConnectionDetailsFactoryTests.java @@ -0,0 +1,39 @@ +/* + * 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.docker.compose.service.connection.opensearch; + +import org.junit.jupiter.api.Test; +import org.springframework.ai.autoconfigure.vectorstore.opensearch.OpenSearchConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.testcontainers.utility.DockerImageName; + +import static org.assertj.core.api.Assertions.assertThat; + +class OpenSearchDockerComposeConnectionDetailsFactoryTests extends AbstractDockerComposeIntegrationTests { + + OpenSearchDockerComposeConnectionDetailsFactoryTests() { + super("opensearch-compose.yaml", DockerImageName.parse("opensearchproject/opensearch")); + } + + @Test + void runCreatesConnectionDetails() { + OpenSearchConnectionDetails connectionDetails = run(OpenSearchConnectionDetails.class); + assertThat(connectionDetails.getUris()).isNotNull(); + assertThat(connectionDetails.getUsername()).isEqualTo("admin"); + assertThat(connectionDetails.getPassword()).isEqualTo("D3v3l0p-ment"); + } + +} \ No newline at end of file diff --git a/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/opensearch/OpenSearchEnvironmentTests.java b/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/opensearch/OpenSearchEnvironmentTests.java new file mode 100644 index 000000000..7e7ba6c42 --- /dev/null +++ b/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/opensearch/OpenSearchEnvironmentTests.java @@ -0,0 +1,40 @@ +/* + * 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.docker.compose.service.connection.opensearch; + +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class OpenSearchEnvironmentTests { + + @Test + void getPasswordWhenNoPassword() { + OpenSearchEnvironment environment = new OpenSearchEnvironment(Collections.emptyMap()); + assertThat(environment.getPassword()).isNull(); + } + + @Test + void getPasswordWhenHasPassword() { + OpenSearchEnvironment environment = new OpenSearchEnvironment( + Map.of("OPENSEARCH_INITIAL_ADMIN_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + +} \ No newline at end of file diff --git a/spring-ai-spring-boot-docker-compose/src/test/resources/org/springframework/ai/docker/compose/service/connection/opensearch/opensearch-compose.yaml b/spring-ai-spring-boot-docker-compose/src/test/resources/org/springframework/ai/docker/compose/service/connection/opensearch/opensearch-compose.yaml new file mode 100644 index 000000000..c3e28a5c6 --- /dev/null +++ b/spring-ai-spring-boot-docker-compose/src/test/resources/org/springframework/ai/docker/compose/service/connection/opensearch/opensearch-compose.yaml @@ -0,0 +1,8 @@ +services: + opensearch: + image: '{imageName}' + ports: + - '9200' + environment: + - OPENSEARCH_INITIAL_ADMIN_PASSWORD=D3v3l0p-ment + - discovery.type=single-node diff --git a/spring-ai-spring-boot-testcontainers/pom.xml b/spring-ai-spring-boot-testcontainers/pom.xml index 80593b13d..91ea7f7b4 100644 --- a/spring-ai-spring-boot-testcontainers/pom.xml +++ b/spring-ai-spring-boot-testcontainers/pom.xml @@ -123,6 +123,14 @@ true + + + org.springframework.ai + spring-ai-opensearch-store + ${project.parent.version} + true + + @@ -216,6 +224,13 @@ true + + org.opensearch + opensearch-testcontainers + 2.0.1 + true + + diff --git a/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/opensearch/OpenSearchContainerConnectionDetailsFactory.java b/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/opensearch/OpenSearchContainerConnectionDetailsFactory.java new file mode 100644 index 000000000..22898785f --- /dev/null +++ b/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/opensearch/OpenSearchContainerConnectionDetailsFactory.java @@ -0,0 +1,64 @@ +/* + * 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.testcontainers.service.connection.opensearch; + +import org.opensearch.testcontainers.OpensearchContainer; +import org.springframework.ai.autoconfigure.vectorstore.opensearch.OpenSearchConnectionDetails; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; + +import java.util.List; + +/** + * @author Eddú Meléndez + */ +class OpenSearchContainerConnectionDetailsFactory + extends ContainerConnectionDetailsFactory, OpenSearchConnectionDetails> { + + @Override + public OpenSearchConnectionDetails getContainerConnectionDetails( + ContainerConnectionSource> source) { + return new OpenSearchContainerConnectionDetails(source); + } + + /** + * {@link OpenSearchConnectionDetails} backed by a {@link ContainerConnectionSource}. + */ + private static final class OpenSearchContainerConnectionDetails + extends ContainerConnectionDetails> implements OpenSearchConnectionDetails { + + private OpenSearchContainerConnectionDetails(ContainerConnectionSource> source) { + super(source); + } + + @Override + public List getUris() { + return List.of(getContainer().getHttpHostAddress()); + } + + @Override + public String getUsername() { + return getContainer().isSecurityEnabled() ? getContainer().getUsername() : null; + } + + @Override + public String getPassword() { + return getContainer().isSecurityEnabled() ? getContainer().getPassword() : null; + } + + } + +} diff --git a/spring-ai-spring-boot-testcontainers/src/main/resources/META-INF/spring.factories b/spring-ai-spring-boot-testcontainers/src/main/resources/META-INF/spring.factories index 805211684..2eab8182b 100644 --- a/spring-ai-spring-boot-testcontainers/src/main/resources/META-INF/spring.factories +++ b/spring-ai-spring-boot-testcontainers/src/main/resources/META-INF/spring.factories @@ -2,6 +2,7 @@ org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFacto org.springframework.ai.testcontainers.service.connection.chroma.ChromaContainerConnectionDetailsFactory,\ org.springframework.ai.testcontainers.service.connection.milvus.MilvusContainerConnectionDetailsFactory,\ org.springframework.ai.testcontainers.service.connection.ollama.OllamaContainerConnectionDetailsFactory,\ +org.springframework.ai.testcontainers.service.connection.opensearch.OpenSearchContainerConnectionDetailsFactory,\ org.springframework.ai.testcontainers.service.connection.qdrant.QdrantContainerConnectionDetailsFactory,\ org.springframework.ai.testcontainers.service.connection.redis.RedisContainerConnectionDetailsFactory,\ org.springframework.ai.testcontainers.service.connection.typesense.TypesenseContainerConnectionDetailsFactory,\ diff --git a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/opensearch/OpenSearchContainerConnectionDetailsFactoryTest.java b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/opensearch/OpenSearchContainerConnectionDetailsFactoryTest.java new file mode 100644 index 000000000..a2b2f3ba1 --- /dev/null +++ b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/opensearch/OpenSearchContainerConnectionDetailsFactoryTest.java @@ -0,0 +1,120 @@ +/* + * 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.testcontainers.service.connection.opensearch; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.opensearch.testcontainers.OpensearchContainer; +import org.springframework.ai.autoconfigure.vectorstore.opensearch.OpenSearchVectorStoreAutoConfiguration; +import org.springframework.ai.document.Document; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.transformers.TransformersEmbeddingModel; +import org.springframework.ai.vectorstore.OpenSearchVectorStore; +import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.DefaultResourceLoader; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.hasSize; + +@SpringBootTest(properties = { + "spring.ai.vectorstore.opensearch.index-name=" + OpenSearchContainerConnectionDetailsFactoryTest.DOCUMENT_INDEX, + "spring.ai.vectorstore.opensearch.mapping-json=" + + OpenSearchContainerConnectionDetailsFactoryTest.MAPPING_JSON }) +@Testcontainers +class OpenSearchContainerConnectionDetailsFactoryTest { + + @Container + @ServiceConnection + private static final OpensearchContainer opensearch = new OpensearchContainer<>( + "opensearchproject/opensearch:2.12.0"); + + static final String DOCUMENT_INDEX = "auto-spring-ai-document-index"; + + static final String MAPPING_JSON = "{\"properties\":{\"embedding\":{\"type\":\"knn_vector\",\"dimension\":384}}}"; + + private final 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"))); + + @Autowired + private OpenSearchVectorStore vectorStore; + + @Test + public void addAndSearchTest() { + + vectorStore.add(documents); + + Awaitility.await() + .until(() -> vectorStore + .similaritySearch(SearchRequest.query("Great Depression").withTopK(1).withSimilarityThreshold(0)), + hasSize(1)); + + List results = vectorStore + .similaritySearch(SearchRequest.query("Great Depression").withTopK(1).withSimilarityThreshold(0)); + + assertThat(results).hasSize(1); + Document resultDoc = results.get(0); + assertThat(resultDoc.getId()).isEqualTo(documents.get(2).getId()); + assertThat(resultDoc.getContent()).contains("The Great Depression (1929–1939) was an economic shock"); + assertThat(resultDoc.getMetadata()).hasSize(2); + assertThat(resultDoc.getMetadata()).containsKey("meta2"); + assertThat(resultDoc.getMetadata()).containsKey("distance"); + + // Remove all documents from the store + vectorStore.delete(documents.stream().map(Document::getId).toList()); + + Awaitility.await() + .until(() -> vectorStore + .similaritySearch(SearchRequest.query("Great Depression").withTopK(1).withSimilarityThreshold(0)), + hasSize(0)); + } + + 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) + @ImportAutoConfiguration(OpenSearchVectorStoreAutoConfiguration.class) + static class Config { + + @Bean + public EmbeddingModel embeddingModel() { + return new TransformersEmbeddingModel(); + } + + } + +}