diff --git a/document-readers/pdf-reader/pom.xml b/document-readers/pdf-reader/pom.xml
index c870c9176c..37ebc83dd5 100644
--- a/document-readers/pdf-reader/pom.xml
+++ b/document-readers/pdf-reader/pom.xml
@@ -55,6 +55,7 @@
org.testcontainers
junit-jupiter
+ ${testcontainers.version}
test
diff --git a/models/spring-ai-ollama/pom.xml b/models/spring-ai-ollama/pom.xml
index e8f04527e7..b72cf88f94 100644
--- a/models/spring-ai-ollama/pom.xml
+++ b/models/spring-ai-ollama/pom.xml
@@ -71,6 +71,7 @@
org.testcontainers
junit-jupiter
+ ${testcontainers.version}
test
diff --git a/models/spring-ai-postgresml/pom.xml b/models/spring-ai-postgresml/pom.xml
index 78ed1e8f2c..aac3aee315 100644
--- a/models/spring-ai-postgresml/pom.xml
+++ b/models/spring-ai-postgresml/pom.xml
@@ -54,12 +54,14 @@
org.testcontainers
junit-jupiter
+ ${testcontainers.version}
test
org.testcontainers
postgresql
+ ${testcontainers.version}
test
diff --git a/models/spring-ai-transformers/pom.xml b/models/spring-ai-transformers/pom.xml
index fdc8f69035..bffd8123d5 100644
--- a/models/spring-ai-transformers/pom.xml
+++ b/models/spring-ai-transformers/pom.xml
@@ -82,6 +82,7 @@
org.testcontainers
junit-jupiter
+ ${testcontainers.version}
test
diff --git a/pom.xml b/pom.xml
index 83bd0e1ff5..97071095ad 100644
--- a/pom.xml
+++ b/pom.xml
@@ -60,6 +60,7 @@
spring-ai-spring-boot-starters/spring-ai-starter-bedrock-ai
spring-ai-spring-boot-starters/spring-ai-starter-mistral-ai
spring-ai-retry
+ spring-ai-spring-boot-testcontainers
@@ -133,7 +134,7 @@
1.7.1
- 1.19.6
+ 1.19.7
0.0.4
diff --git a/spring-ai-bom/pom.xml b/spring-ai-bom/pom.xml
index 03655b644e..ac3d40e9d2 100644
--- a/spring-ai-bom/pom.xml
+++ b/spring-ai-bom/pom.xml
@@ -301,6 +301,12 @@
spring-ai-qdrant-store-spring-boot-starter
${project.version}
+
+
+ org.springframework.ai
+ spring-ai-spring-boot-testcontainers
+ ${project.version}
+
diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc
index 7fa84af400..4891400b84 100644
--- a/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc
@@ -52,6 +52,7 @@
** xref:api/etl-pipeline.adoc[]
** xref:api/testing.adoc[]
** xref:api/generic-model.adoc[]
+* xref:api/testcontainers.adoc[Testcontainers]
* xref:contribution-guidelines.adoc[Contribution Guidelines]
* Appendices
** xref:upgrade-notes.adoc[]
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
new file mode 100644
index 0000000000..f43fa75d87
--- /dev/null
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/testcontainers.adoc
@@ -0,0 +1,28 @@
+[[testcontainers]]
+= Testcontainers
+
+== Service Connections
+
+The following service connection factories are provided in the spring-ai-spring-boot-testcontainers jar:
+
+[cols="|,|"]
+|====
+| Connection Details | Matched on
+| `ChromaConnectionDetails`
+| Containers of type `ChromaDBContainer`
+
+| `MilvusServiceClientConnectionDetails`
+| Containers of type `MilvusContainer`
+
+| `OllamaConnectionDetails`
+| Containers of type `OllamaContainer`
+
+| `QdrantConnectionDetails`
+| Containers of type `QdrantContainer`
+
+| `RedisConnectionDetails`
+| Containers of type `RedisStackContainer`
+
+| `WeaviateConnectionDetails`
+| Containers of type `WeaviateContainer`
+|====
diff --git a/spring-ai-spring-boot-autoconfigure/pom.xml b/spring-ai-spring-boot-autoconfigure/pom.xml
index 80c6aacf12..8b5df9d983 100644
--- a/spring-ai-spring-boot-autoconfigure/pom.xml
+++ b/spring-ai-spring-boot-autoconfigure/pom.xml
@@ -265,6 +265,7 @@
org.testcontainers
postgresql
+ ${testcontainers.version}
test
@@ -298,7 +299,7 @@
org.testcontainers
qdrant
- 1.19.6
+ ${testcontainers.version}
test
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfiguration.java
index aa454fb838..d3378ec88e 100644
--- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfiguration.java
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfiguration.java
@@ -31,6 +31,7 @@
* {@link AutoConfiguration Auto-configuration} for Ollama Chat Client.
*
* @author Christian Tzolov
+ * @author Eddú Meléndez
* @since 0.8.0
*/
@AutoConfiguration(after = RestClientAutoConfiguration.class)
@@ -39,10 +40,16 @@
OllamaConnectionProperties.class })
public class OllamaAutoConfiguration {
+ @Bean
+ @ConditionalOnMissingBean(OllamaConnectionDetails.class)
+ public PropertiesOllamaConnectionDetails ollamaConnectionDetails(OllamaConnectionProperties properties) {
+ return new PropertiesOllamaConnectionDetails(properties);
+ }
+
@Bean
@ConditionalOnMissingBean
- public OllamaApi ollamaApi(OllamaConnectionProperties properties, RestClient.Builder restClientBuilder) {
- return new OllamaApi(properties.getBaseUrl(), restClientBuilder);
+ public OllamaApi ollamaApi(OllamaConnectionDetails connectionDetails, RestClient.Builder restClientBuilder) {
+ return new OllamaApi(connectionDetails.getBaseUrl(), restClientBuilder);
}
@Bean
@@ -65,4 +72,19 @@ public OllamaEmbeddingClient ollamaEmbeddingClient(OllamaApi ollamaApi, OllamaEm
.withDefaultOptions(properties.getOptions());
}
+ private static class PropertiesOllamaConnectionDetails implements OllamaConnectionDetails {
+
+ private final OllamaConnectionProperties properties;
+
+ PropertiesOllamaConnectionDetails(OllamaConnectionProperties properties) {
+ this.properties = properties;
+ }
+
+ @Override
+ public String getBaseUrl() {
+ return this.properties.getBaseUrl();
+ }
+
+ }
+
}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaConnectionDetails.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaConnectionDetails.java
new file mode 100644
index 0000000000..6981097c3e
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaConnectionDetails.java
@@ -0,0 +1,27 @@
+/*
+ * 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.ollama;
+
+import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
+
+/**
+ * @author Eddú Meléndez
+ */
+public interface OllamaConnectionDetails extends ConnectionDetails {
+
+ String getBaseUrl();
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaConnectionDetails.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaConnectionDetails.java
new file mode 100644
index 0000000000..4877fdca2c
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaConnectionDetails.java
@@ -0,0 +1,29 @@
+/*
+ * 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.chroma;
+
+import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
+
+/**
+ * @author Eddú Meléndez
+ */
+public interface ChromaConnectionDetails extends ConnectionDetails {
+
+ String getHost();
+
+ int getPort();
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaVectorStoreAutoConfiguration.java
index 392ce9160a..a780313b7a 100644
--- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaVectorStoreAutoConfiguration.java
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaVectorStoreAutoConfiguration.java
@@ -30,12 +30,19 @@
/**
* @author Christian Tzolov
+ * @author Eddú Meléndez
*/
@AutoConfiguration
@ConditionalOnClass({ EmbeddingClient.class, RestTemplate.class, ChromaVectorStore.class, ObjectMapper.class })
@EnableConfigurationProperties({ ChromaApiProperties.class, ChromaVectorStoreProperties.class })
public class ChromaVectorStoreAutoConfiguration {
+ @Bean
+ @ConditionalOnMissingBean(ChromaConnectionDetails.class)
+ PropertiesChromaConnectionDetails chromaConnectionDetails(ChromaApiProperties properties) {
+ return new PropertiesChromaConnectionDetails(properties);
+ }
+
@Bean
@ConditionalOnMissingBean
public RestTemplate restTemplate() {
@@ -44,9 +51,10 @@ public RestTemplate restTemplate() {
@Bean
@ConditionalOnMissingBean
- public ChromaApi chromaApi(ChromaApiProperties apiProperties, RestTemplate restTemplate) {
+ public ChromaApi chromaApi(ChromaApiProperties apiProperties, RestTemplate restTemplate,
+ ChromaConnectionDetails connectionDetails) {
- String chromaUrl = String.format("%s:%s", apiProperties.getHost(), apiProperties.getPort());
+ String chromaUrl = String.format("%s:%s", connectionDetails.getHost(), connectionDetails.getPort());
var chromaApi = new ChromaApi(chromaUrl, restTemplate, new ObjectMapper());
@@ -67,4 +75,24 @@ public ChromaVectorStore vectorStore(EmbeddingClient embeddingClient, ChromaApi
return new ChromaVectorStore(embeddingClient, chromaApi, storeProperties.getCollectionName());
}
+ private static class PropertiesChromaConnectionDetails implements ChromaConnectionDetails {
+
+ private final ChromaApiProperties properties;
+
+ PropertiesChromaConnectionDetails(ChromaApiProperties properties) {
+ this.properties = properties;
+ }
+
+ @Override
+ public String getHost() {
+ return this.properties.getHost();
+ }
+
+ @Override
+ public int getPort() {
+ return this.properties.getPort();
+ }
+
+ }
+
}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/milvus/MilvusServiceClientConnectionDetails.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/milvus/MilvusServiceClientConnectionDetails.java
new file mode 100644
index 0000000000..b6d015630b
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/milvus/MilvusServiceClientConnectionDetails.java
@@ -0,0 +1,29 @@
+/*
+ * 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.milvus;
+
+import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
+
+/**
+ * @author Eddú Meléndez
+ */
+public interface MilvusServiceClientConnectionDetails extends ConnectionDetails {
+
+ String getHost();
+
+ int getPort();
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/milvus/MilvusVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/milvus/MilvusVectorStoreAutoConfiguration.java
index dd744cd6ac..c1b2ab70a9 100644
--- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/milvus/MilvusVectorStoreAutoConfiguration.java
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/milvus/MilvusVectorStoreAutoConfiguration.java
@@ -34,12 +34,20 @@
/**
* @author Christian Tzolov
+ * @author Eddú Meléndez
*/
@AutoConfiguration
@ConditionalOnClass({ MilvusVectorStore.class, EmbeddingClient.class })
@EnableConfigurationProperties({ MilvusServiceClientProperties.class, MilvusVectorStoreProperties.class })
public class MilvusVectorStoreAutoConfiguration {
+ @Bean
+ @ConditionalOnMissingBean(MilvusServiceClientConnectionDetails.class)
+ PropertiesMilvusServiceClientConnectionDetails milvusServiceClientConnectionDetails(
+ MilvusServiceClientProperties properties) {
+ return new PropertiesMilvusServiceClientConnectionDetails(properties);
+ }
+
@Bean
@ConditionalOnMissingBean
public MilvusVectorStore vectorStore(MilvusServiceClient milvusClient, EmbeddingClient embeddingClient,
@@ -60,11 +68,11 @@ public MilvusVectorStore vectorStore(MilvusServiceClient milvusClient, Embedding
@Bean
@ConditionalOnMissingBean
public MilvusServiceClient milvusClient(MilvusVectorStoreProperties serverProperties,
- MilvusServiceClientProperties clientProperties) {
+ MilvusServiceClientProperties clientProperties, MilvusServiceClientConnectionDetails connectionDetails) {
var builder = ConnectParam.newBuilder()
- .withHost(clientProperties.getHost())
- .withPort(clientProperties.getPort())
+ .withHost(connectionDetails.getHost())
+ .withPort(connectionDetails.getPort())
.withDatabaseName(serverProperties.getDatabaseName())
.withConnectTimeout(clientProperties.getConnectTimeoutMs(), TimeUnit.MILLISECONDS)
.withKeepAliveTime(clientProperties.getKeepAliveTimeMs(), TimeUnit.MILLISECONDS)
@@ -105,4 +113,25 @@ public MilvusServiceClient milvusClient(MilvusVectorStoreProperties serverProper
return new MilvusServiceClient(builder.build());
}
+ private static class PropertiesMilvusServiceClientConnectionDetails
+ implements MilvusServiceClientConnectionDetails {
+
+ private final MilvusServiceClientProperties properties;
+
+ PropertiesMilvusServiceClientConnectionDetails(MilvusServiceClientProperties properties) {
+ this.properties = properties;
+ }
+
+ @Override
+ public String getHost() {
+ return this.properties.getHost();
+ }
+
+ @Override
+ public int getPort() {
+ return this.properties.getPort();
+ }
+
+ }
+
}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/qdrant/QdrantConnectionDetails.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/qdrant/QdrantConnectionDetails.java
new file mode 100644
index 0000000000..f07a701e2b
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/qdrant/QdrantConnectionDetails.java
@@ -0,0 +1,29 @@
+/*
+ * 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.qdrant;
+
+import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
+
+/**
+ * @author Eddú Meléndez
+ */
+public interface QdrantConnectionDetails extends ConnectionDetails {
+
+ String getHost();
+
+ int getPort();
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/qdrant/QdrantVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/qdrant/QdrantVectorStoreAutoConfiguration.java
index d887d9731a..5540f29776 100644
--- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/qdrant/QdrantVectorStoreAutoConfiguration.java
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/qdrant/QdrantVectorStoreAutoConfiguration.java
@@ -26,6 +26,7 @@
/**
* @author Anush Shetty
+ * @author Eddú Meléndez
* @since 0.8.1
*/
@AutoConfiguration
@@ -33,14 +34,21 @@
@EnableConfigurationProperties(QdrantVectorStoreProperties.class)
public class QdrantVectorStoreAutoConfiguration {
+ @Bean
+ @ConditionalOnMissingBean(QdrantConnectionDetails.class)
+ PropertiesQdrantConnectionDetails qdrantConnectionDetails(QdrantVectorStoreProperties properties) {
+ return new PropertiesQdrantConnectionDetails(properties);
+ }
+
@Bean
@ConditionalOnMissingBean
- public QdrantVectorStore vectorStore(EmbeddingClient embeddingClient, QdrantVectorStoreProperties properties) {
+ public QdrantVectorStore vectorStore(EmbeddingClient embeddingClient, QdrantVectorStoreProperties properties,
+ QdrantConnectionDetails connectionDetails) {
var config = QdrantVectorStoreConfig.builder()
.withCollectionName(properties.getCollectionName())
- .withHost(properties.getHost())
- .withPort(properties.getPort())
+ .withHost(connectionDetails.getHost())
+ .withPort(connectionDetails.getPort())
.withTls(properties.isUseTls())
.withApiKey(properties.getApiKey())
.build();
@@ -48,4 +56,24 @@ public QdrantVectorStore vectorStore(EmbeddingClient embeddingClient, QdrantVect
return new QdrantVectorStore(config, embeddingClient);
}
+ private static class PropertiesQdrantConnectionDetails implements QdrantConnectionDetails {
+
+ private final QdrantVectorStoreProperties properties;
+
+ PropertiesQdrantConnectionDetails(QdrantVectorStoreProperties properties) {
+ this.properties = properties;
+ }
+
+ @Override
+ public String getHost() {
+ return this.properties.getHost();
+ }
+
+ @Override
+ public int getPort() {
+ return this.properties.getPort();
+ }
+
+ }
+
}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisConnectionDetails.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisConnectionDetails.java
new file mode 100644
index 0000000000..df98d95227
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisConnectionDetails.java
@@ -0,0 +1,27 @@
+/*
+ * 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.redis;
+
+import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
+
+/**
+ * @author Eddú Meléndez
+ */
+public interface RedisConnectionDetails extends ConnectionDetails {
+
+ String getUri();
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java
index 67e807d82e..e17d76e7da 100644
--- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java
@@ -26,18 +26,26 @@
/**
* @author Christian Tzolov
+ * @author Eddú Meléndez
*/
@AutoConfiguration
@ConditionalOnClass({ RedisVectorStore.class, EmbeddingClient.class })
@EnableConfigurationProperties(RedisVectorStoreProperties.class)
public class RedisVectorStoreAutoConfiguration {
+ @Bean
+ @ConditionalOnMissingBean(RedisConnectionDetails.class)
+ public PropertiesRedisConnectionDetails redisConnectionDetails(RedisVectorStoreProperties properties) {
+ return new PropertiesRedisConnectionDetails(properties);
+ }
+
@Bean
@ConditionalOnMissingBean
- public RedisVectorStore vectorStore(EmbeddingClient embeddingClient, RedisVectorStoreProperties properties) {
+ public RedisVectorStore vectorStore(EmbeddingClient embeddingClient, RedisVectorStoreProperties properties,
+ RedisConnectionDetails redisConnectionDetails) {
var config = RedisVectorStoreConfig.builder()
- .withURI(properties.getUri())
+ .withURI(redisConnectionDetails.getUri())
.withIndexName(properties.getIndex())
.withPrefix(properties.getPrefix())
.build();
@@ -45,4 +53,19 @@ public RedisVectorStore vectorStore(EmbeddingClient embeddingClient, RedisVector
return new RedisVectorStore(config, embeddingClient);
}
+ private static class PropertiesRedisConnectionDetails implements RedisConnectionDetails {
+
+ private final RedisVectorStoreProperties properties;
+
+ public PropertiesRedisConnectionDetails(RedisVectorStoreProperties properties) {
+ this.properties = properties;
+ }
+
+ @Override
+ public String getUri() {
+ return this.properties.getUri();
+ }
+
+ }
+
}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/weaviate/WeaviateConnectionDetails.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/weaviate/WeaviateConnectionDetails.java
new file mode 100644
index 0000000000..154271c6f1
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/weaviate/WeaviateConnectionDetails.java
@@ -0,0 +1,24 @@
+/*
+ * 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.weaviate;
+
+import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
+
+public interface WeaviateConnectionDetails extends ConnectionDetails {
+
+ String getHost();
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/weaviate/WeaviateVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/weaviate/WeaviateVectorStoreAutoConfiguration.java
index 3f3e706060..3d5c84534f 100644
--- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/weaviate/WeaviateVectorStoreAutoConfiguration.java
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/weaviate/WeaviateVectorStoreAutoConfiguration.java
@@ -27,20 +27,28 @@
/**
* @author Christian Tzolov
+ * @author Eddú Meléndez
*/
@AutoConfiguration
@ConditionalOnClass({ EmbeddingClient.class, WeaviateVectorStore.class })
@EnableConfigurationProperties({ WeaviateVectorStoreProperties.class })
public class WeaviateVectorStoreAutoConfiguration {
+ @Bean
+ @ConditionalOnMissingBean(WeaviateConnectionDetails.class)
+ public PropertiesWeaviateConnectionDetails weaviateConnectionDetails(WeaviateVectorStoreProperties properties) {
+ return new PropertiesWeaviateConnectionDetails(properties);
+ }
+
@Bean
@ConditionalOnMissingBean
- public WeaviateVectorStore vectorStore(EmbeddingClient embeddingClient, WeaviateVectorStoreProperties properties) {
+ public WeaviateVectorStore vectorStore(EmbeddingClient embeddingClient, WeaviateVectorStoreProperties properties,
+ WeaviateConnectionDetails connectionDetails) {
WeaviateVectorStoreConfig.Builder configBuilder = WeaviateVectorStore.WeaviateVectorStoreConfig.builder()
.withScheme(properties.getScheme())
.withApiKey(properties.getApiKey())
- .withHost(properties.getHost())
+ .withHost(connectionDetails.getHost())
.withHeaders(properties.getHeaders())
.withObjectClass(properties.getObjectClass())
.withFilterableMetadataFields(properties.getFilterField()
@@ -53,4 +61,19 @@ public WeaviateVectorStore vectorStore(EmbeddingClient embeddingClient, Weaviate
return new WeaviateVectorStore(configBuilder.build(), embeddingClient);
}
+ private static class PropertiesWeaviateConnectionDetails implements WeaviateConnectionDetails {
+
+ private final WeaviateVectorStoreProperties properties;
+
+ PropertiesWeaviateConnectionDetails(WeaviateVectorStoreProperties properties) {
+ this.properties = properties;
+ }
+
+ @Override
+ public String getHost() {
+ return this.properties.getHost();
+ }
+
+ }
+
}
diff --git a/spring-ai-spring-boot-testcontainers/pom.xml b/spring-ai-spring-boot-testcontainers/pom.xml
new file mode 100644
index 0000000000..fca68b9112
--- /dev/null
+++ b/spring-ai-spring-boot-testcontainers/pom.xml
@@ -0,0 +1,206 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+
+ spring-ai-spring-boot-testcontainers
+ jar
+ Spring AI Testcontainers
+ Spring AI Testcontainers
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-spring-boot-autoconfigure
+ ${project.parent.version}
+
+
+
+ com.google.protobuf
+ protobuf-java
+ ${protobuf-java.version}
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-testcontainers
+
+
+
+ org.springframework.ai
+ spring-ai-openai
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-ollama
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-transformers
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-milvus-store
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-chroma-store
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-weaviate-store
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-redis
+ ${project.parent.version}
+ true
+
+
+
+
+ redis.clients
+ jedis
+ 5.1.0
+
+
+
+
+ org.springframework.ai
+ spring-ai-qdrant
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+ test
+
+
+ org.postgresql
+ postgresql
+ ${postgresql.version}
+ test
+
+
+
+ org.testcontainers
+ testcontainers
+ ${testcontainers.version}
+ true
+
+
+
+ org.testcontainers
+ junit-jupiter
+ ${testcontainers.version}
+ test
+
+
+
+ com.redis
+ testcontainers-redis
+ 2.0.1
+
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+
+ org.testcontainers
+ qdrant
+ ${testcontainers.version}
+ true
+
+
+
+ org.testcontainers
+ weaviate
+ ${testcontainers.version}
+ true
+
+
+
+ org.testcontainers
+ chromadb
+ ${testcontainers.version}
+ true
+
+
+
+ org.testcontainers
+ milvus
+ ${testcontainers.version}
+ true
+
+
+
+ org.testcontainers
+ ollama
+ ${testcontainers.version}
+ true
+
+
+
+
+
diff --git a/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaContainerConnectionDetailsFactory.java b/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaContainerConnectionDetailsFactory.java
new file mode 100644
index 0000000000..cb859f400a
--- /dev/null
+++ b/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaContainerConnectionDetailsFactory.java
@@ -0,0 +1,56 @@
+/*
+ * 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.chroma;
+
+import org.springframework.ai.autoconfigure.vectorstore.chroma.ChromaConnectionDetails;
+import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
+import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
+import org.testcontainers.chromadb.ChromaDBContainer;
+
+/**
+ * @author Eddú Meléndez
+ */
+class ChromaContainerConnectionDetailsFactory
+ extends ContainerConnectionDetailsFactory {
+
+ @Override
+ public ChromaConnectionDetails getContainerConnectionDetails(ContainerConnectionSource source) {
+ return new ChromaDBContainerConnectionDetails(source);
+ }
+
+ /**
+ * {@link ChromaConnectionDetails} backed by a {@link ContainerConnectionSource}.
+ */
+ private static final class ChromaDBContainerConnectionDetails extends ContainerConnectionDetails
+ implements ChromaConnectionDetails {
+
+ private ChromaDBContainerConnectionDetails(ContainerConnectionSource source) {
+ super(source);
+ }
+
+ @Override
+ public String getHost() {
+ return "http://%s".formatted(getContainer().getHost());
+ }
+
+ @Override
+ public int getPort() {
+ return getContainer().getMappedPort(8000);
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/milvus/MilvusContainerConnectionDetailsFactory.java b/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/milvus/MilvusContainerConnectionDetailsFactory.java
new file mode 100644
index 0000000000..a44643c382
--- /dev/null
+++ b/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/milvus/MilvusContainerConnectionDetailsFactory.java
@@ -0,0 +1,58 @@
+/*
+ * 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.milvus;
+
+import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientConnectionDetails;
+import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
+import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
+import org.testcontainers.milvus.MilvusContainer;
+
+/**
+ * @author Eddú Meléndez
+ */
+class MilvusContainerConnectionDetailsFactory
+ extends ContainerConnectionDetailsFactory {
+
+ @Override
+ public MilvusServiceClientConnectionDetails getContainerConnectionDetails(
+ ContainerConnectionSource source) {
+ return new MilvusContainerConnectionDetails(source);
+ }
+
+ /**
+ * {@link MilvusServiceClientConnectionDetails} backed by a
+ * {@link ContainerConnectionSource}.
+ */
+ private static final class MilvusContainerConnectionDetails extends ContainerConnectionDetails
+ implements MilvusServiceClientConnectionDetails {
+
+ private MilvusContainerConnectionDetails(ContainerConnectionSource source) {
+ super(source);
+ }
+
+ @Override
+ public String getHost() {
+ return getContainer().getHost();
+ }
+
+ @Override
+ public int getPort() {
+ return getContainer().getMappedPort(19530);
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/ollama/OllamaContainerConnectionDetailsFactory.java b/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/ollama/OllamaContainerConnectionDetailsFactory.java
new file mode 100644
index 0000000000..46174bc36f
--- /dev/null
+++ b/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/ollama/OllamaContainerConnectionDetailsFactory.java
@@ -0,0 +1,51 @@
+/*
+ * 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.ollama;
+
+import org.springframework.ai.autoconfigure.ollama.OllamaConnectionDetails;
+import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
+import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
+import org.testcontainers.ollama.OllamaContainer;
+
+/**
+ * @author Eddú Meléndez
+ */
+class OllamaContainerConnectionDetailsFactory
+ extends ContainerConnectionDetailsFactory {
+
+ @Override
+ public OllamaConnectionDetails getContainerConnectionDetails(ContainerConnectionSource source) {
+ return new OllamaContainerConnectionDetails(source);
+ }
+
+ /**
+ * {@link OllamaConnectionDetails} backed by a {@link ContainerConnectionSource}.
+ */
+ private static final class OllamaContainerConnectionDetails extends ContainerConnectionDetails
+ implements OllamaConnectionDetails {
+
+ private OllamaContainerConnectionDetails(ContainerConnectionSource source) {
+ super(source);
+ }
+
+ @Override
+ public String getBaseUrl() {
+ return getContainer().getEndpoint();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/qdrant/QdrantContainerConnectionDetailsFactory.java b/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/qdrant/QdrantContainerConnectionDetailsFactory.java
new file mode 100644
index 0000000000..2b410c5161
--- /dev/null
+++ b/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/qdrant/QdrantContainerConnectionDetailsFactory.java
@@ -0,0 +1,56 @@
+/*
+ * 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.qdrant;
+
+import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantConnectionDetails;
+import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
+import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
+import org.testcontainers.qdrant.QdrantContainer;
+
+/**
+ * @author Eddú Meléndez
+ */
+class QdrantContainerConnectionDetailsFactory
+ extends ContainerConnectionDetailsFactory {
+
+ @Override
+ public QdrantConnectionDetails getContainerConnectionDetails(ContainerConnectionSource source) {
+ return new QdrantContainerConnectionDetails(source);
+ }
+
+ /**
+ * {@link QdrantConnectionDetails} backed by a {@link ContainerConnectionSource}.
+ */
+ private static final class QdrantContainerConnectionDetails extends ContainerConnectionDetails
+ implements QdrantConnectionDetails {
+
+ private QdrantContainerConnectionDetails(ContainerConnectionSource source) {
+ super(source);
+ }
+
+ @Override
+ public String getHost() {
+ return getContainer().getHost();
+ }
+
+ @Override
+ public int getPort() {
+ return getContainer().getMappedPort(6334);
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/redis/RedisContainerConnectionDetailsFactory.java b/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/redis/RedisContainerConnectionDetailsFactory.java
new file mode 100644
index 0000000000..72007c4e6d
--- /dev/null
+++ b/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/redis/RedisContainerConnectionDetailsFactory.java
@@ -0,0 +1,51 @@
+/*
+ * 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.redis;
+
+import com.redis.testcontainers.RedisStackContainer;
+import org.springframework.ai.autoconfigure.vectorstore.redis.RedisConnectionDetails;
+import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
+import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
+
+/**
+ * @author Eddú Meléndez
+ */
+class RedisContainerConnectionDetailsFactory
+ extends ContainerConnectionDetailsFactory {
+
+ @Override
+ public RedisConnectionDetails getContainerConnectionDetails(ContainerConnectionSource source) {
+ return new RedisContainerConnectionDetails(source);
+ }
+
+ /**
+ * {@link RedisConnectionDetails} backed by a {@link ContainerConnectionSource}.
+ */
+ private static final class RedisContainerConnectionDetails extends ContainerConnectionDetails
+ implements RedisConnectionDetails {
+
+ private RedisContainerConnectionDetails(ContainerConnectionSource source) {
+ super(source);
+ }
+
+ @Override
+ public String getUri() {
+ return getContainer().getRedisURI();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/weaviate/WeaviateContainerConnectionDetailsFactory.java b/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/weaviate/WeaviateContainerConnectionDetailsFactory.java
new file mode 100644
index 0000000000..601fb6244d
--- /dev/null
+++ b/spring-ai-spring-boot-testcontainers/src/main/java/org/springframework/ai/testcontainers/service/connection/weaviate/WeaviateContainerConnectionDetailsFactory.java
@@ -0,0 +1,52 @@
+/*
+ * 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.weaviate;
+
+import org.springframework.ai.autoconfigure.vectorstore.weaviate.WeaviateConnectionDetails;
+import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
+import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
+import org.testcontainers.weaviate.WeaviateContainer;
+
+/**
+ * @author Eddú Meléndez
+ */
+class WeaviateContainerConnectionDetailsFactory
+ extends ContainerConnectionDetailsFactory {
+
+ @Override
+ public WeaviateConnectionDetails getContainerConnectionDetails(
+ ContainerConnectionSource source) {
+ return new WeaviateContainerConnectionDetails(source);
+ }
+
+ /**
+ * {@link WeaviateConnectionDetails} backed by a {@link ContainerConnectionSource}.
+ */
+ private static final class WeaviateContainerConnectionDetails extends ContainerConnectionDetails
+ implements WeaviateConnectionDetails {
+
+ private WeaviateContainerConnectionDetails(ContainerConnectionSource source) {
+ super(source);
+ }
+
+ @Override
+ public String getHost() {
+ return getContainer().getHttpHostAddress();
+ }
+
+ }
+
+}
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
new file mode 100644
index 0000000000..32f6c2d2c4
--- /dev/null
+++ b/spring-ai-spring-boot-testcontainers/src/main/resources/META-INF/spring.factories
@@ -0,0 +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.ollama.OllamaContainerConnectionDetailsFactory,\
+org.springframework.ai.testcontainers.service.connection.qdrant.QdrantContainerConnectionDetailsFactory,\
+org.springframework.ai.testcontainers.service.connection.redis.RedisContainerConnectionDetailsFactory,\
+org.springframework.ai.testcontainers.service.connection.weaviate.WeaviateContainerConnectionDetailsFactory
diff --git a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaContainerConnectionDetailsFactoryTest.java b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaContainerConnectionDetailsFactoryTest.java
new file mode 100644
index 0000000000..1a9db7bc8c
--- /dev/null
+++ b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaContainerConnectionDetailsFactoryTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.chroma;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.autoconfigure.vectorstore.chroma.ChromaVectorStoreAutoConfiguration;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingClient;
+import org.springframework.ai.transformers.TransformersEmbeddingClient;
+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.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.chromadb.ChromaDBContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringJUnitConfig
+@Testcontainers
+@TestPropertySource(properties = "spring.ai.vectorstore.chroma.store.collectionName=TestCollection")
+class ChromaContainerConnectionDetailsFactoryTest {
+
+ @Container
+ @ServiceConnection
+ static ChromaDBContainer chroma = new ChromaDBContainer("ghcr.io/chroma-core/chroma:0.4.15");
+
+ @Autowired
+ private VectorStore vectorStore;
+
+ @Test
+ public void addAndSearchWithFilters() {
+ var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
+ Map.of("country", "Bulgaria"));
+ var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
+ Map.of("country", "Netherlands"));
+
+ vectorStore.add(List.of(bgDocument, nlDocument));
+
+ var request = SearchRequest.query("The World").withTopK(5);
+
+ List results = vectorStore.similaritySearch(request);
+ assertThat(results).hasSize(2);
+
+ results = vectorStore
+ .similaritySearch(request.withSimilarityThresholdAll().withFilterExpression("country == 'Bulgaria'"));
+ assertThat(results).hasSize(1);
+ assertThat(results.get(0).getId()).isEqualTo(bgDocument.getId());
+
+ results = vectorStore
+ .similaritySearch(request.withSimilarityThresholdAll().withFilterExpression("country == 'Netherlands'"));
+ assertThat(results).hasSize(1);
+ assertThat(results.get(0).getId()).isEqualTo(nlDocument.getId());
+
+ // Remove all documents from the store
+ vectorStore.delete(List.of(bgDocument, nlDocument).stream().map(doc -> doc.getId()).toList());
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ImportAutoConfiguration(ChromaVectorStoreAutoConfiguration.class)
+ static class Config {
+
+ @Bean
+ public EmbeddingClient embeddingClient() {
+ return new TransformersEmbeddingClient();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/milvus/MilvusContainerConnectionDetailsFactoryTest.java b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/milvus/MilvusContainerConnectionDetailsFactoryTest.java
new file mode 100644
index 0000000000..ee43cf0019
--- /dev/null
+++ b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/milvus/MilvusContainerConnectionDetailsFactoryTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.milvus;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.ResourceUtils;
+import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingClient;
+import org.springframework.ai.transformers.TransformersEmbeddingClient;
+import org.springframework.ai.vectorstore.SearchRequest;
+import org.springframework.ai.vectorstore.VectorStore;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+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.milvus.MilvusContainer;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringJUnitConfig
+@Testcontainers
+@TestPropertySource(properties = { "spring.ai.vectorstore.milvus.metricType=COSINE",
+ "spring.ai.vectorstore.milvus.indexType=IVF_FLAT", "spring.ai.vectorstore.milvus.embeddingDimension=384",
+ "spring.ai.vectorstore.milvus.collectionName=myTestCollection" })
+class MilvusContainerConnectionDetailsFactoryTest {
+
+ @Container
+ @ServiceConnection
+ static MilvusContainer milvusContainer = new MilvusContainer("milvusdb/milvus:v2.3.8");
+
+ List documents = List.of(
+ new Document(ResourceUtils.getText("classpath:/test/data/spring.ai.txt"), Map.of("spring", "great")),
+ new Document(ResourceUtils.getText("classpath:/test/data/time.shelter.txt")), new Document(
+ ResourceUtils.getText("classpath:/test/data/great.depression.txt"), Map.of("depression", "bad")));
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(MilvusVectorStoreAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ @Autowired
+ private VectorStore vectorStore;
+
+ @Test
+ public void addAndSearch() {
+ vectorStore.add(documents);
+
+ List results = vectorStore.similaritySearch(SearchRequest.query("Spring").withTopK(1));
+
+ assertThat(results).hasSize(1);
+ Document resultDoc = results.get(0);
+ assertThat(resultDoc.getId()).isEqualTo(documents.get(0).getId());
+ assertThat(resultDoc.getContent())
+ .contains("Spring AI provides abstractions that serve as the foundation for developing AI applications.");
+ assertThat(resultDoc.getMetadata()).hasSize(2);
+ assertThat(resultDoc.getMetadata()).containsKeys("spring", "distance");
+
+ // Remove all documents from the store
+ vectorStore.delete(documents.stream().map(doc -> doc.getId()).toList());
+
+ results = vectorStore.similaritySearch(SearchRequest.query("Spring").withTopK(1));
+ assertThat(results).hasSize(0);
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ImportAutoConfiguration(MilvusVectorStoreAutoConfiguration.class)
+ static class Config {
+
+ @Bean
+ public EmbeddingClient embeddingClient() {
+ return new TransformersEmbeddingClient();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/ollama/OllamaContainerConnectionDetailsFactoryTest.java b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/ollama/OllamaContainerConnectionDetailsFactoryTest.java
new file mode 100644
index 0000000000..5b79e662f2
--- /dev/null
+++ b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/ollama/OllamaContainerConnectionDetailsFactoryTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.ollama;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration;
+import org.springframework.ai.embedding.EmbeddingResponse;
+import org.springframework.ai.ollama.OllamaEmbeddingClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+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.ollama.OllamaContainer;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Eddú Meléndez
+ */
+@SpringJUnitConfig
+@Testcontainers
+@TestPropertySource(properties = "spring.ai.ollama.embedding.options.model="
+ + OllamaContainerConnectionDetailsFactoryTest.MODEL_NAME)
+class OllamaContainerConnectionDetailsFactoryTest {
+
+ private static final Log logger = LogFactory.getLog(OllamaContainerConnectionDetailsFactoryTest.class);
+
+ static final String MODEL_NAME = "orca-mini";
+
+ @Container
+ @ServiceConnection
+ static OllamaContainer ollama = new OllamaContainer("ollama/ollama:0.1.23");
+
+ @Autowired
+ private OllamaEmbeddingClient embeddingClient;
+
+ @BeforeAll
+ public static void beforeAll() throws IOException, InterruptedException {
+ logger.info("Start pulling the '" + MODEL_NAME + " ' generative ... would take several minutes ...");
+ ollama.execInContainer("ollama", "pull", MODEL_NAME);
+ logger.info(MODEL_NAME + " pulling competed!");
+ }
+
+ @Test
+ public void singleTextEmbedding() {
+ EmbeddingResponse embeddingResponse = this.embeddingClient.embedForResponse(List.of("Hello World"));
+ assertThat(embeddingResponse.getResults()).hasSize(1);
+ assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
+ assertThat(this.embeddingClient.dimensions()).isEqualTo(3200);
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ImportAutoConfiguration({ RestClientAutoConfiguration.class, OllamaAutoConfiguration.class })
+ static class Config {
+
+ }
+
+}
\ No newline at end of file
diff --git a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/qdrant/QdrantContainerConnectionDetailsFactoryTest.java b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/qdrant/QdrantContainerConnectionDetailsFactoryTest.java
new file mode 100644
index 0000000000..09f3c1498d
--- /dev/null
+++ b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/qdrant/QdrantContainerConnectionDetailsFactoryTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.qdrant;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingClient;
+import org.springframework.ai.transformers.TransformersEmbeddingClient;
+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.testcontainers.service.connection.ServiceConnection;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.DefaultResourceLoader;
+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.qdrant.QdrantContainer;
+
+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;
+
+@SpringJUnitConfig
+@Testcontainers
+@TestPropertySource(properties = "spring.ai.vectorstore.qdrant.collectionName=test_collection")
+public class QdrantContainerConnectionDetailsFactoryTest {
+
+ @Container
+ @ServiceConnection
+ static QdrantContainer qdrantContainer = new QdrantContainer("qdrant/qdrant:v1.7.4");
+
+ List documents = List.of(
+ new Document(getText("classpath:/test/data/spring.ai.txt"), Map.of("spring", "great")),
+ new Document(getText("classpath:/test/data/time.shelter.txt")),
+ new Document(getText("classpath:/test/data/great.depression.txt"), Map.of("depression", "bad")));
+
+ @Autowired
+ private VectorStore vectorStore;
+
+ @Test
+ public void addAndSearch() {
+ vectorStore.add(documents);
+
+ List results = vectorStore
+ .similaritySearch(SearchRequest.query("What is Great Depression?").withTopK(1));
+
+ assertThat(results).hasSize(1);
+ Document resultDoc = results.get(0);
+ assertThat(resultDoc.getId()).isEqualTo(documents.get(2).getId());
+ assertThat(resultDoc.getMetadata()).containsKeys("depression", "distance");
+
+ // Remove all documents from the store
+ vectorStore.delete(documents.stream().map(doc -> doc.getId()).toList());
+ results = vectorStore.similaritySearch(SearchRequest.query("Great Depression").withTopK(1));
+ assertThat(results).hasSize(0);
+ }
+
+ public static 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(QdrantVectorStoreAutoConfiguration.class)
+ static class Config {
+
+ @Bean
+ public EmbeddingClient embeddingClient() {
+ return new TransformersEmbeddingClient();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/redis/RedisContainerConnectionDetailsFactoryTest.java b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/redis/RedisContainerConnectionDetailsFactoryTest.java
new file mode 100644
index 0000000000..b0888689e7
--- /dev/null
+++ b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/redis/RedisContainerConnectionDetailsFactoryTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.redis;
+
+import com.redis.testcontainers.RedisStackContainer;
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.ResourceUtils;
+import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreAutoConfiguration;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingClient;
+import org.springframework.ai.transformers.TransformersEmbeddingClient;
+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.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 java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringJUnitConfig
+@Testcontainers
+@TestPropertySource(
+ properties = { "spring.ai.vectorstore.redis.index=myIdx", "spring.ai.vectorstore.redis.prefix=doc:" })
+class RedisContainerConnectionDetailsFactoryTest {
+
+ @Container
+ @ServiceConnection
+ static RedisStackContainer redisContainer = new RedisStackContainer(
+ RedisStackContainer.DEFAULT_IMAGE_NAME.withTag(RedisStackContainer.DEFAULT_TAG));
+
+ private List documents = List.of(
+ new Document(ResourceUtils.getText("classpath:/test/data/spring.ai.txt"), Map.of("spring", "great")),
+ new Document(ResourceUtils.getText("classpath:/test/data/time.shelter.txt")), new Document(
+ ResourceUtils.getText("classpath:/test/data/great.depression.txt"), Map.of("depression", "bad")));
+
+ @Autowired
+ private VectorStore vectorStore;
+
+ @Test
+ void addAndSearch() {
+ vectorStore.add(documents);
+
+ List results = vectorStore.similaritySearch(SearchRequest.query("Spring").withTopK(1));
+
+ assertThat(results).hasSize(1);
+ Document resultDoc = results.get(0);
+ assertThat(resultDoc.getId()).isEqualTo(documents.get(0).getId());
+ assertThat(resultDoc.getContent())
+ .contains("Spring AI provides abstractions that serve as the foundation for developing AI applications.");
+
+ // Remove all documents from the store
+ vectorStore.delete(documents.stream().map(doc -> doc.getId()).toList());
+
+ results = vectorStore.similaritySearch(SearchRequest.query("Spring").withTopK(1));
+ assertThat(results).isEmpty();
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ImportAutoConfiguration(RedisVectorStoreAutoConfiguration.class)
+ static class Config {
+
+ @Bean
+ public EmbeddingClient embeddingClient() {
+ return new TransformersEmbeddingClient();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/weaviate/WeaviateContainerConnectionDetailsFactoryTest.java b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/weaviate/WeaviateContainerConnectionDetailsFactoryTest.java
new file mode 100644
index 0000000000..685bc1b794
--- /dev/null
+++ b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/weaviate/WeaviateContainerConnectionDetailsFactoryTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.weaviate;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.autoconfigure.vectorstore.weaviate.WeaviateVectorStoreAutoConfiguration;
+import org.springframework.ai.autoconfigure.vectorstore.weaviate.WeaviateVectorStoreProperties;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingClient;
+import org.springframework.ai.transformers.TransformersEmbeddingClient;
+import org.springframework.ai.vectorstore.SearchRequest;
+import org.springframework.ai.vectorstore.VectorStore;
+import org.springframework.ai.vectorstore.WeaviateVectorStore;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
+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.weaviate.WeaviateContainer;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringJUnitConfig
+@Testcontainers
+@TestPropertySource(properties = { "spring.ai.vectorstore.weaviate.filter-field.country=TEXT",
+ "spring.ai.vectorstore.weaviate.filter-field.year=NUMBER",
+ "spring.ai.vectorstore.weaviate.filter-field.active=BOOLEAN",
+ "spring.ai.vectorstore.weaviate.filter-field.price=NUMBER" })
+class WeaviateContainerConnectionDetailsFactoryTest {
+
+ @Container
+ @ServiceConnection
+ static WeaviateContainer weaviateContainer = new WeaviateContainer("semitechnologies/weaviate:1.22.4");
+
+ @Autowired
+ private WeaviateVectorStoreProperties properties;
+
+ @Autowired
+ private VectorStore vectorStore;
+
+ @Test
+ public void addAndSearchWithFilters() {
+ assertThat(properties.getFilterField()).hasSize(4);
+
+ assertThat(properties.getFilterField().get("country"))
+ .isEqualTo(WeaviateVectorStore.WeaviateVectorStoreConfig.MetadataField.Type.TEXT);
+ assertThat(properties.getFilterField().get("year"))
+ .isEqualTo(WeaviateVectorStore.WeaviateVectorStoreConfig.MetadataField.Type.NUMBER);
+ assertThat(properties.getFilterField().get("active"))
+ .isEqualTo(WeaviateVectorStore.WeaviateVectorStoreConfig.MetadataField.Type.BOOLEAN);
+ assertThat(properties.getFilterField().get("price"))
+ .isEqualTo(WeaviateVectorStore.WeaviateVectorStoreConfig.MetadataField.Type.NUMBER);
+
+ var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
+ Map.of("country", "Bulgaria", "price", 3.14, "active", true, "year", 2020));
+ var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
+ Map.of("country", "Netherlands", "price", 1.57, "active", false, "year", 2023));
+
+ vectorStore.add(List.of(bgDocument, nlDocument));
+
+ var request = SearchRequest.query("The World").withTopK(5);
+
+ List results = vectorStore.similaritySearch(request);
+ assertThat(results).hasSize(2);
+
+ results = vectorStore
+ .similaritySearch(request.withSimilarityThresholdAll().withFilterExpression("country == 'Bulgaria'"));
+ assertThat(results).hasSize(1);
+ assertThat(results.get(0).getId()).isEqualTo(bgDocument.getId());
+
+ results = vectorStore
+ .similaritySearch(request.withSimilarityThresholdAll().withFilterExpression("country == 'Netherlands'"));
+ assertThat(results).hasSize(1);
+ assertThat(results.get(0).getId()).isEqualTo(nlDocument.getId());
+
+ results = vectorStore.similaritySearch(
+ request.withSimilarityThresholdAll().withFilterExpression("price > 1.57 && active == true"));
+ assertThat(results).hasSize(1);
+ assertThat(results.get(0).getId()).isEqualTo(bgDocument.getId());
+
+ results = vectorStore
+ .similaritySearch(request.withSimilarityThresholdAll().withFilterExpression("year in [2020, 2023]"));
+ assertThat(results).hasSize(2);
+
+ results = vectorStore
+ .similaritySearch(request.withSimilarityThresholdAll().withFilterExpression("year > 2020 && year <= 2023"));
+ assertThat(results).hasSize(1);
+ assertThat(results.get(0).getId()).isEqualTo(nlDocument.getId());
+
+ // Remove all documents from the store
+ vectorStore.delete(List.of(bgDocument, nlDocument).stream().map(doc -> doc.getId()).toList());
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ImportAutoConfiguration(WeaviateVectorStoreAutoConfiguration.class)
+ static class Config {
+
+ @Bean
+ public EmbeddingClient embeddingClient() {
+ return new TransformersEmbeddingClient();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/vector-stores/spring-ai-qdrant/pom.xml b/vector-stores/spring-ai-qdrant/pom.xml
index 8b635c4c17..f5c4c36cd2 100644
--- a/vector-stores/spring-ai-qdrant/pom.xml
+++ b/vector-stores/spring-ai-qdrant/pom.xml
@@ -72,7 +72,7 @@
org.testcontainers
qdrant
- 1.19.6
+ ${testcontainers.version}
test
diff --git a/vector-stores/spring-ai-redis/pom.xml b/vector-stores/spring-ai-redis/pom.xml
index 8bf9b348e8..b735ded987 100644
--- a/vector-stores/spring-ai-redis/pom.xml
+++ b/vector-stores/spring-ai-redis/pom.xml
@@ -71,6 +71,7 @@
org.testcontainers
junit-jupiter
+ ${testcontainers.version}
test