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 index 4877fdca2..465086d34 100644 --- 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 @@ -26,4 +26,6 @@ public interface ChromaConnectionDetails extends ConnectionDetails { int getPort(); + String getKeyToken(); + } 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 592f99421..5bc669f8b 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 @@ -59,8 +59,8 @@ public class ChromaVectorStoreAutoConfiguration { var chromaApi = new ChromaApi(chromaUrl, restClientBuilder, new ObjectMapper()); - if (StringUtils.hasText(apiProperties.getKeyToken())) { - chromaApi.withKeyToken(apiProperties.getKeyToken()); + if (StringUtils.hasText(connectionDetails.getKeyToken())) { + chromaApi.withKeyToken(connectionDetails.getKeyToken()); } else if (StringUtils.hasText(apiProperties.getUsername()) && StringUtils.hasText(apiProperties.getPassword())) { chromaApi.withBasicAuthCredentials(apiProperties.getUsername(), apiProperties.getPassword()); @@ -95,6 +95,11 @@ public class ChromaVectorStoreAutoConfiguration { return this.properties.getPort(); } + @Override + public String getKeyToken() { + return this.properties.getKeyToken(); + } + } } diff --git a/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/chroma/ChromaDockerComposeConnectionDetailsFactory.java b/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/chroma/ChromaDockerComposeConnectionDetailsFactory.java index f0eaf3762..e8f544201 100644 --- a/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/chroma/ChromaDockerComposeConnectionDetailsFactory.java +++ b/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/chroma/ChromaDockerComposeConnectionDetailsFactory.java @@ -45,12 +45,15 @@ public class ChromaDockerComposeConnectionDetailsFactory static class ChromaDockerComposeConnectionDetails extends DockerComposeConnectionDetails implements ChromaConnectionDetails { + private final ChromaEnvironment environment; + private final String host; private final int port; ChromaDockerComposeConnectionDetails(RunningService service) { super(service); + this.environment = new ChromaEnvironment(service.env()); this.host = service.host(); this.port = service.ports().get(CHROMA_PORT); } @@ -65,6 +68,11 @@ public class ChromaDockerComposeConnectionDetailsFactory return this.port; } + @Override + public String getKeyToken() { + return this.environment.getKeyToken(); + } + } } diff --git a/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/chroma/ChromaEnvironment.java b/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/chroma/ChromaEnvironment.java new file mode 100644 index 000000000..6e02dfa20 --- /dev/null +++ b/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/chroma/ChromaEnvironment.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 - 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.docker.compose.service.connection.chroma; + +import java.util.Map; + +class ChromaEnvironment { + + private final String keyToken; + + ChromaEnvironment(Map env) { + this.keyToken = env.get("CHROMA_SERVER_AUTHN_CREDENTIALS"); + } + + public String getKeyToken() { + return this.keyToken; + } + +} diff --git a/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/chroma/ChromaEnvironmentTests.java b/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/chroma/ChromaEnvironmentTests.java new file mode 100644 index 000000000..8f4eac436 --- /dev/null +++ b/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/chroma/ChromaEnvironmentTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2023 - 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.docker.compose.service.connection.chroma; + +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class ChromaEnvironmentTests { + + @Test + void getKeyTokenWhenNoCredential() { + ChromaEnvironment environment = new ChromaEnvironment(Collections.emptyMap()); + assertThat(environment.getKeyToken()).isNull(); + } + + @Test + void getKeyTokenWhenHasCredential() { + ChromaEnvironment environment = new ChromaEnvironment(Map.of("CHROMA_SERVER_AUTHN_CREDENTIALS", "secret")); + assertThat(environment.getKeyToken()).isEqualTo("secret"); + } + +} diff --git a/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/chroma/ChromaWithTokenDockerComposeConnectionDetailsFactoryTests.java b/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/chroma/ChromaWithTokenDockerComposeConnectionDetailsFactoryTests.java new file mode 100644 index 000000000..493f8ca54 --- /dev/null +++ b/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/chroma/ChromaWithTokenDockerComposeConnectionDetailsFactoryTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2023 - 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.docker.compose.service.connection.chroma; + +import org.junit.jupiter.api.Test; +import org.springframework.ai.autoconfigure.vectorstore.chroma.ChromaConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.testcontainers.utility.DockerImageName; + +import static org.assertj.core.api.Assertions.assertThat; + +class ChromaWithTokenDockerComposeConnectionDetailsFactoryTests extends AbstractDockerComposeIntegrationTests { + + ChromaWithTokenDockerComposeConnectionDetailsFactoryTests() { + super("chroma-with-token-compose.yaml", DockerImageName.parse("chromadb/chroma")); + } + + @Test + void runCreatesConnectionDetails() { + ChromaConnectionDetails connectionDetails = run(ChromaConnectionDetails.class); + assertThat(connectionDetails.getHost()).isNotNull(); + assertThat(connectionDetails.getPort()).isGreaterThan(0); + assertThat(connectionDetails.getKeyToken()).isEqualTo("secret"); + } + +} \ No newline at end of file diff --git a/spring-ai-spring-boot-docker-compose/src/test/resources/org/springframework/ai/docker/compose/service/connection/chroma/chroma-with-token-compose.yaml b/spring-ai-spring-boot-docker-compose/src/test/resources/org/springframework/ai/docker/compose/service/connection/chroma/chroma-with-token-compose.yaml new file mode 100644 index 000000000..5e304ac13 --- /dev/null +++ b/spring-ai-spring-boot-docker-compose/src/test/resources/org/springframework/ai/docker/compose/service/connection/chroma/chroma-with-token-compose.yaml @@ -0,0 +1,8 @@ +services: + chroma: + image: '{imageName}' + ports: + - '8000' + environment: + - CHROMA_SERVER_AUTHN_CREDENTIALS=secret + - CHROMA_SERVER_AUTHN_PROVIDER=chromadb.auth.token_authn.TokenAuthenticationServerProvider 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 index cb859f400..e60b7bebe 100644 --- 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 @@ -51,6 +51,11 @@ class ChromaContainerConnectionDetailsFactory return getContainer().getMappedPort(8000); } + @Override + public String getKeyToken() { + return getContainer().getEnvMap().get("CHROMA_SERVER_AUTHN_CREDENTIALS"); + } + } } 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 index 2977ef7bd..436770db4 100644 --- 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 @@ -45,7 +45,7 @@ class ChromaContainerConnectionDetailsFactoryTest { @Container @ServiceConnection - static ChromaDBContainer chroma = new ChromaDBContainer("ghcr.io/chroma-core/chroma:0.4.15"); + static ChromaDBContainer chroma = new ChromaDBContainer("ghcr.io/chroma-core/chroma:0.5.0"); @Autowired private VectorStore vectorStore; diff --git a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithTokenContainerConnectionDetailsFactoryTest.java b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithTokenContainerConnectionDetailsFactoryTest.java new file mode 100644 index 000000000..3f330cdfa --- /dev/null +++ b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/chroma/ChromaWithTokenContainerConnectionDetailsFactoryTest.java @@ -0,0 +1,94 @@ +/* + * 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.EmbeddingModel; +import org.springframework.ai.transformers.TransformersEmbeddingModel; +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 ChromaWithTokenContainerConnectionDetailsFactoryTest { + + @Container + @ServiceConnection + static ChromaDBContainer chroma = new ChromaDBContainer("ghcr.io/chroma-core/chroma:0.5.0") + .withEnv("CHROMA_SERVER_AUTHN_CREDENTIALS", "token") + .withEnv("CHROMA_SERVER_AUTHN_PROVIDER", "chromadb.auth.token_authn.TokenAuthenticationServerProvider"); + + @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 EmbeddingModel embeddingModel() { + return new TransformersEmbeddingModel(); + } + + } + +} diff --git a/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/chroma/ChromaApiIT.java b/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/chroma/ChromaApiIT.java index 0565cf16b..638c2a1f2 100644 --- a/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/chroma/ChromaApiIT.java +++ b/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/chroma/ChromaApiIT.java @@ -43,7 +43,7 @@ import org.testcontainers.junit.jupiter.Testcontainers; public class ChromaApiIT { @Container - static ChromaDBContainer chromaContainer = new ChromaDBContainer("ghcr.io/chroma-core/chroma:0.4.12"); + static ChromaDBContainer chromaContainer = new ChromaDBContainer("ghcr.io/chroma-core/chroma:0.5.0"); @Autowired ChromaApi chroma; diff --git a/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/vectorstore/BasicAuthChromaWhereIT.java b/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/vectorstore/BasicAuthChromaWhereIT.java index 5780f7258..fef21848b 100644 --- a/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/vectorstore/BasicAuthChromaWhereIT.java +++ b/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/vectorstore/BasicAuthChromaWhereIT.java @@ -55,10 +55,8 @@ public class BasicAuthChromaWhereIT { */ @Container static ChromaDBContainer chromaContainer = new ChromaDBContainer("ghcr.io/chroma-core/chroma:0.5.0") - .withEnv("CHROMA_SERVER_AUTH_CREDENTIALS_FILE", "/chroma/server.htpasswd") - .withEnv("CHROMA_SERVER_AUTH_CREDENTIALS_PROVIDER", - "chromadb.auth.providers.HtpasswdFileServerAuthCredentialsProvider") - .withEnv("CHROMA_SERVER_AUTH_PROVIDER", "chromadb.auth.basic.BasicAuthServerProvider") + .withEnv("CHROMA_SERVER_AUTHN_CREDENTIALS_FILE", "/chroma/server.htpasswd") + .withEnv("CHROMA_SERVER_AUTHN_PROVIDER", "chromadb.auth.basic_authn.BasicAuthenticationServerProvider") .withCopyToContainer(MountableFile.forClasspathResource("server.htpasswd"), "/chroma/server.htpasswd"); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() diff --git a/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/vectorstore/TokenSecuredChromaWhereIT.java b/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/vectorstore/TokenSecuredChromaWhereIT.java index 7b6f5e8dd..fc567c46e 100644 --- a/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/vectorstore/TokenSecuredChromaWhereIT.java +++ b/vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/vectorstore/TokenSecuredChromaWhereIT.java @@ -37,7 +37,7 @@ import org.testcontainers.junit.jupiter.Testcontainers; /** * ChromaDB with static API Token Authentication: - * https://docs.trychroma.com/usage-guide#static-api-token-authentication + * https://docs.trychroma.com/deployment/auth * * Test cases are based on the Chroma: * https://docs.trychroma.com/usage-guide#using-where-filters and the related @@ -53,14 +53,12 @@ public class TokenSecuredChromaWhereIT { /** * ChromaDB with static API Token Authentication: - * https://docs.trychroma.com/usage-guide#static-api-token-authentication + * https://docs.trychroma.com/deployment/auth */ @Container static ChromaDBContainer chromaContainer = new ChromaDBContainer("ghcr.io/chroma-core/chroma:0.5.0") - .withEnv("CHROMA_SERVER_AUTH_CREDENTIALS", CHROMA_SERVER_AUTH_CREDENTIALS) - .withEnv("CHROMA_SERVER_AUTH_CREDENTIALS_PROVIDER", - "chromadb.auth.token.TokenConfigServerAuthCredentialsProvider") - .withEnv("CHROMA_SERVER_AUTH_PROVIDER", "chromadb.auth.token.TokenAuthServerProvider"); + .withEnv("CHROMA_SERVER_AUTHN_CREDENTIALS", CHROMA_SERVER_AUTH_CREDENTIALS) + .withEnv("CHROMA_SERVER_AUTHN_PROVIDER", "chromadb.auth.token_authn.TokenAuthenticationServerProvider"); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withUserConfiguration(TestApplication.class)