From e1d9bfc616509fb75151fb4f679b059c58610bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Mon, 30 Sep 2024 20:29:20 -0600 Subject: [PATCH] Add MongoDB Atlas support for TestContainers and Docker Compose Implement service connection support for MongoDB Atlas using both TestContainers and Docker Compose. This change enables easier integration testing and local development with MongoDB Atlas. - Leverage TestContainers 1.20.2 which introduces MongoDBAtlasLocalContainer - Add TestContainers support using MongoDBAtlasLocalContainer - Implement Docker Compose configuration for MongoDB Atlas - Create connection details factories for both TestContainers and Docker Compose - Update dependency management for MongoDB Atlas integration - Add integration tests for both TestContainers and Docker Compose setups - Update documentation to include MongoDB Atlas support --- .../ROOT/pages/api/docker-compose.adoc | 3 + .../ROOT/pages/api/testcontainers.adoc | 3 + spring-ai-spring-boot-docker-compose/pom.xml | 7 ++ ...DockerComposeConnectionDetailsFactory.java | 83 ++++++++++++++ .../main/resources/META-INF/spring.factories | 1 + ...rComposeConnectionDetailsFactoryTests.java | 22 ++++ .../connection/mongo/mongo-compose.yaml | 5 + spring-ai-spring-boot-testcontainers/pom.xml | 14 +++ ...ocalContainerConnectionDetailsFactory.java | 69 ++++++++++++ .../main/resources/META-INF/spring.factories | 1 + ...ContainerConnectionDetailsFactoryTest.java | 103 ++++++++++++++++++ 11 files changed, 311 insertions(+) create mode 100644 spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/mongo/MongoDbAtlasLocalDockerComposeConnectionDetailsFactory.java create mode 100644 spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/mongo/MongoDbAtlasLocalDockerComposeConnectionDetailsFactoryTests.java create mode 100644 spring-ai-spring-boot-docker-compose/src/test/resources/org/springframework/ai/docker/compose/service/connection/mongo/mongo-compose.yaml create mode 100644 spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/mongo/MongoDbAtlasLocalContainerConnectionDetailsFactory.java create mode 100644 spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/mongo/MongoDbAtlasLocalContainerConnectionDetailsFactoryTest.java 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 50745c1e3..51855e9fb 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 @@ -34,6 +34,9 @@ The following service connection factories are provided in the `spring-ai-spring | `ChromaConnectionDetails` | Containers named `chromadb/chroma`, `ghcr.io/chroma-core/chroma` +| `MongoConnectionDetails` +| Containers named `mongodb/mongodb-atlas-local` + | `OllamaConnectionDetails` | Containers named `ollama/ollama` 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 e646be203..006378d62 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 @@ -37,6 +37,9 @@ The following service connection factories are provided in the `spring-ai-spring | `MilvusServiceClientConnectionDetails` | Containers of type `MilvusContainer` +| `MongoConnectionDetails` +| Containers of type `MongoDBAtlasLocalContainer` + | `OllamaConnectionDetails` | Containers of type `OllamaContainer` diff --git a/spring-ai-spring-boot-docker-compose/pom.xml b/spring-ai-spring-boot-docker-compose/pom.xml index d3b530ca2..fb13e0efe 100644 --- a/spring-ai-spring-boot-docker-compose/pom.xml +++ b/spring-ai-spring-boot-docker-compose/pom.xml @@ -123,6 +123,13 @@ true + + org.springframework.ai + spring-ai-mongodb-atlas-store + ${project.parent.version} + true + + diff --git a/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/mongo/MongoDbAtlasLocalDockerComposeConnectionDetailsFactory.java b/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/mongo/MongoDbAtlasLocalDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000..8de8fb4ec --- /dev/null +++ b/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/mongo/MongoDbAtlasLocalDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,83 @@ +/* + * 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.mongo; + +import com.mongodb.ConnectionString; +import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails; +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; + +/** + * A {@link DockerComposeConnectionDetailsFactory} implementation that creates + * {@link MongoConnectionDetails} for a MongoDB Atlas Local instance running in a Docker + * container. + * + *

+ * This factory is designed to work with Docker Compose configurations that include a + * MongoDB Atlas Local container. It provides the necessary connection details for Spring + * Boot applications to connect to the MongoDB instance. + * + *

+ * The factory matches containers with the image name "mongodb/mongodb-atlas-local". + * + *

+ * Usage of this factory requires the presence of the Spring Boot Docker Compose support + * and the MongoDB driver on the classpath. + * + * @author Eddú Meléndez + * @see DockerComposeConnectionDetailsFactory + * @see MongoConnectionDetails + * @see org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource + * @since 1.0.0 + */ +class MongoDbAtlasLocalDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + private static final int MONGODB_PORT = 27017; + + protected MongoDbAtlasLocalDockerComposeConnectionDetailsFactory() { + super("mongodb/mongodb-atlas-local"); + } + + @Override + protected MongoConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new MongoDbAtlasLocalContainerConnectionDetails(source.getRunningService()); + } + + /** + * {@link MongoConnectionDetails} backed by a {@code MongoDB Atlas} + * {@link RunningService}. + */ + static class MongoDbAtlasLocalContainerConnectionDetails extends DockerComposeConnectionDetails + implements MongoConnectionDetails { + + private final String connectionString; + + MongoDbAtlasLocalContainerConnectionDetails(RunningService service) { + super(service); + this.connectionString = String.format("mongodb://%s:%d/?directConnection=true", service.host(), + service.ports().get(MONGODB_PORT)); + } + + @Override + public ConnectionString getConnectionString() { + return new ConnectionString(this.connectionString); + } + + } + +} 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 218716860..cf9041575 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,5 +1,6 @@ org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ org.springframework.ai.docker.compose.service.connection.chroma.ChromaDockerComposeConnectionDetailsFactory,\ +org.springframework.ai.docker.compose.service.connection.mongo.MongoDbAtlasLocalDockerComposeConnectionDetailsFactory,\ 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,\ diff --git a/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/mongo/MongoDbAtlasLocalDockerComposeConnectionDetailsFactoryTests.java b/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/mongo/MongoDbAtlasLocalDockerComposeConnectionDetailsFactoryTests.java new file mode 100644 index 000000000..b312d71b4 --- /dev/null +++ b/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/mongo/MongoDbAtlasLocalDockerComposeConnectionDetailsFactoryTests.java @@ -0,0 +1,22 @@ +package org.springframework.ai.docker.compose.service.connection.mongo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.testcontainers.utility.DockerImageName; + +import static org.assertj.core.api.Assertions.assertThat; + +class MongoDbAtlasLocalDockerComposeConnectionDetailsFactoryTests extends AbstractDockerComposeIntegrationTests { + + protected MongoDbAtlasLocalDockerComposeConnectionDetailsFactoryTests() { + super("mongo-compose.yaml", DockerImageName.parse("mongodb/mongodb-atlas-local")); + } + + @Test + void runCreatesConnectionDetails() { + MongoConnectionDetails connectionDetails = run(MongoConnectionDetails.class); + assertThat(connectionDetails.getConnectionString()).isNotNull(); + } + +} \ 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/mongo/mongo-compose.yaml b/spring-ai-spring-boot-docker-compose/src/test/resources/org/springframework/ai/docker/compose/service/connection/mongo/mongo-compose.yaml new file mode 100644 index 000000000..bcb7976b4 --- /dev/null +++ b/spring-ai-spring-boot-docker-compose/src/test/resources/org/springframework/ai/docker/compose/service/connection/mongo/mongo-compose.yaml @@ -0,0 +1,5 @@ +services: + mongo: + image: '{imageName}' + ports: + - '27017' diff --git a/spring-ai-spring-boot-testcontainers/pom.xml b/spring-ai-spring-boot-testcontainers/pom.xml index 91ea7f7b4..a56a7af7a 100644 --- a/spring-ai-spring-boot-testcontainers/pom.xml +++ b/spring-ai-spring-boot-testcontainers/pom.xml @@ -131,6 +131,13 @@ true + + org.springframework.ai + spring-ai-mongodb-atlas-store + ${project.parent.version} + true + + @@ -218,6 +225,13 @@ true + + org.testcontainers + mongodb + 1.20.2 + true + + org.testcontainers ollama diff --git a/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/mongo/MongoDbAtlasLocalContainerConnectionDetailsFactory.java b/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/mongo/MongoDbAtlasLocalContainerConnectionDetailsFactory.java new file mode 100644 index 000000000..8bc4b2021 --- /dev/null +++ b/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/mongo/MongoDbAtlasLocalContainerConnectionDetailsFactory.java @@ -0,0 +1,69 @@ +/* + * 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.mongo; + +import com.mongodb.ConnectionString; +import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; +import org.testcontainers.mongodb.MongoDBAtlasLocalContainer; + +/** + * A {@link ContainerConnectionDetailsFactory} implementation that provides + * {@link MongoConnectionDetails} for a {@link MongoDBAtlasLocalContainer}. + *

+ * This factory is used in conjunction with Spring Boot's auto-configuration for + * Testcontainers to automatically create and configure a connection to a MongoDB instance + * running in a testcontainer. + *

+ * It generates {@link MongoConnectionDetails} based on the connection information + * provided by the {@link MongoDBAtlasLocalContainer}, allowing integration of MongoDB + * testcontainers in Spring Boot applications. + * + * @author Eddú Meléndez + * @since 1.0.0 + * @see ContainerConnectionDetailsFactory + * @see MongoConnectionDetails + * @see MongoDBAtlasLocalContainer + */ +class MongoDbAtlasLocalContainerConnectionDetailsFactory + extends ContainerConnectionDetailsFactory { + + @Override + protected MongoConnectionDetails getContainerConnectionDetails( + ContainerConnectionSource source) { + return new MongoDbAtlasLocalContainerConnectionDetails(source); + } + + /** + * {@link MongoConnectionDetails} backed by a {@link ContainerConnectionSource}. + */ + private static final class MongoDbAtlasLocalContainerConnectionDetails + extends ContainerConnectionDetails implements MongoConnectionDetails { + + private MongoDbAtlasLocalContainerConnectionDetails( + ContainerConnectionSource source) { + super(source); + } + + @Override + public ConnectionString getConnectionString() { + return new ConnectionString(getContainer().getConnectionString()); + } + + } + +} 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 8fa2b068b..bf8370e68 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 @@ -1,6 +1,7 @@ org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ org.springframework.ai.testcontainers.service.connection.chroma.ChromaContainerConnectionDetailsFactory,\ org.springframework.ai.testcontainers.service.connection.milvus.MilvusContainerConnectionDetailsFactory,\ +org.springframework.ai.testcontainers.service.connection.mongo.MongoDbAtlasLocalContainerConnectionDetailsFactory,\ org.springframework.ai.testcontainers.service.connection.ollama.OllamaContainerConnectionDetailsFactory,\ org.springframework.ai.testcontainers.service.connection.opensearch.OpenSearchContainerConnectionDetailsFactory,\ org.springframework.ai.testcontainers.service.connection.qdrant.QdrantContainerConnectionDetailsFactory,\ diff --git a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/mongo/MongoDbAtlasLocalContainerConnectionDetailsFactoryTest.java b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/mongo/MongoDbAtlasLocalContainerConnectionDetailsFactoryTest.java new file mode 100644 index 000000000..0ee882bc5 --- /dev/null +++ b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/mongo/MongoDbAtlasLocalContainerConnectionDetailsFactoryTest.java @@ -0,0 +1,103 @@ +/* + * 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.mongo; + +import org.junit.jupiter.api.Test; +import org.springframework.ai.autoconfigure.vectorstore.mongo.MongoDBAtlasVectorStoreAutoConfiguration; +import org.springframework.ai.document.Document; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.openai.OpenAiEmbeddingModel; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; +import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.mongodb.MongoDBAtlasLocalContainer; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringJUnitConfig +@Testcontainers +@TestPropertySource(properties = { "spring.data.mongodb.database=simpleaidb", + "spring.ai.vectorstore.mongodb.initialize-schema=true", + "spring.ai.vectorstore.mongodb.collection-name=test_collection", + "spring.ai.vectorstore.mongodb.index-name=text_index" }) +class MongoDbAtlasLocalContainerConnectionDetailsFactoryTest { + + @Container + @ServiceConnection + private static MongoDBAtlasLocalContainer container = new MongoDBAtlasLocalContainer( + "mongodb/mongodb-atlas-local:7.0.9"); + + @Autowired + private VectorStore vectorStore; + + @Test + public void addAndSearch() throws InterruptedException { + List documents = List.of( + new Document( + "Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!!", + Collections.singletonMap("meta1", "meta1")), + new Document("Hello World Hello World Hello World Hello World Hello World Hello World Hello World"), + new Document( + "Great Depression Great Depression Great Depression Great Depression Great Depression Great Depression", + Collections.singletonMap("meta2", "meta2"))); + + vectorStore.add(documents); + Thread.sleep(5000); // Await a second for the document to be indexed + + List results = vectorStore.similaritySearch(SearchRequest.query("Great").withTopK(1)); + + assertThat(results).hasSize(1); + Document resultDoc = results.get(0); + assertThat(resultDoc.getId()).isEqualTo(documents.get(2).getId()); + assertThat(resultDoc.getContent()).isEqualTo( + "Great Depression Great Depression Great Depression Great Depression Great Depression Great Depression"); + assertThat(resultDoc.getMetadata()).containsEntry("meta2", "meta2"); + + // Remove all documents from the store + vectorStore.delete(documents.stream().map(Document::getId).collect(Collectors.toList())); + + List results2 = vectorStore.similaritySearch(SearchRequest.query("Great").withTopK(1)); + assertThat(results2).isEmpty(); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration({ MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, + MongoDBAtlasVectorStoreAutoConfiguration.class }) + static class Config { + + @Bean + public EmbeddingModel embeddingModel() { + return new OpenAiEmbeddingModel(new OpenAiApi(System.getenv("OPENAI_API_KEY"))); + } + + } + +} \ No newline at end of file