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 50745c1e355..51855e9fb31 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 e646be20390..006378d6261 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 d3b530ca28f..fb13e0efec7 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 00000000000..b7814c68260 --- /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,63 @@ +/* + * 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; + +/** + * @author Eddú Meléndez + */ +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 218716860b4..cf904157592 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 00000000000..b312d71b4d9 --- /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 00000000000..bcb7976b4ba --- /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 91ea7f7b448..a56a7af7aac 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 00000000000..594c3627c3f --- /dev/null +++ b/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/mongo/MongoDbAtlasLocalContainerConnectionDetailsFactory.java @@ -0,0 +1,54 @@ +/* + * 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; + +/** + * @author Eddú Meléndez + */ +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 8fa2b068b6e..bf8370e68ae 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 00000000000..0ee882bc53b --- /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