diff --git a/spring-ai-spring-boot-autoconfigure/pom.xml b/spring-ai-spring-boot-autoconfigure/pom.xml index 84bd54a188..894ae1e83a 100644 --- a/spring-ai-spring-boot-autoconfigure/pom.xml +++ b/spring-ai-spring-boot-autoconfigure/pom.xml @@ -281,6 +281,13 @@ true + + org.springframework.ai + spring-ai-gemfire-store + ${project.parent.version} + true + + diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/gemfire/GemFireConnectionDetails.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/gemfire/GemFireConnectionDetails.java new file mode 100644 index 0000000000..b74eb61e2f --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/gemfire/GemFireConnectionDetails.java @@ -0,0 +1,29 @@ +/* + * Copyright 2024 - 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.gemfire; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * @author Philipp Kessler + */ +public interface GemFireConnectionDetails extends ConnectionDetails { + + String getHost(); + + int getPort(); + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/gemfire/GemFireVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/gemfire/GemFireVectorStoreAutoConfiguration.java new file mode 100644 index 0000000000..ddbd384c10 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/gemfire/GemFireVectorStoreAutoConfiguration.java @@ -0,0 +1,84 @@ +/* + * Copyright 2024 - 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.gemfire; + +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.vectorstore.GemFireVectorStore; +import org.springframework.ai.vectorstore.GemFireVectorStore.GemFireVectorStoreConfig; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * @author Philipp Kessler + */ +@AutoConfiguration +@ConditionalOnClass({ GemFireVectorStore.class, EmbeddingModel.class }) +@EnableConfigurationProperties({ GemFireVectorStoreProperties.class }) +public class GemFireVectorStoreAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(GemFireConnectionDetails.class) + public PropertiesGemFireConnectionDetails gemFireConnectionDetails(GemFireVectorStoreProperties properties) { + return new PropertiesGemFireConnectionDetails(properties); + } + + @Bean + @ConditionalOnMissingBean + public GemFireVectorStore vectorStore(EmbeddingModel embeddingModel, GemFireConnectionDetails connectionDetails, + GemFireVectorStoreProperties properties) { + var vectoreStoreConfig = GemFireVectorStoreConfig.builder() + .withHost(connectionDetails.getHost()) + .withPort(connectionDetails.getPort()) + .withIndex(properties.getIndex()) + .withDocumentField(properties.getDocumentField()) + .withTopK(properties.getTopK()) + .withTopKPerBucket(properties.getTopKPerBucket()) + .withSslEnabled(properties.isSslEnabled()) + .withConnectionTimeout(properties.getConnectionTimeout()) + .withRequestTimeout(properties.getRequestTimeout()) + .build(); + + var vectorStore = new GemFireVectorStore(vectoreStoreConfig, embeddingModel); + + vectorStore.setIndexName(properties.getIndex()); + + return vectorStore; + } + + private static class PropertiesGemFireConnectionDetails implements GemFireConnectionDetails { + + private final GemFireVectorStoreProperties properties; + + public PropertiesGemFireConnectionDetails(GemFireVectorStoreProperties properties) { + this.properties = properties; + } + + @Override + public String getHost() { + return properties.getHost(); + } + + @Override + public int getPort() { + return properties.getPort(); + } + + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/gemfire/GemFireVectorStoreProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/gemfire/GemFireVectorStoreProperties.java new file mode 100644 index 0000000000..f0c9efe43c --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/gemfire/GemFireVectorStoreProperties.java @@ -0,0 +1,119 @@ +/* + * Copyright 2024 - 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.gemfire; + +import org.springframework.ai.vectorstore.GemFireVectorStore; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Philipp Kessler + */ +@ConfigurationProperties(GemFireVectorStoreProperties.CONFIG_PREFIX) +public class GemFireVectorStoreProperties { + + public static final String CONFIG_PREFIX = "spring.ai.vectorstore.gemfire"; + + private String host; + + private int port = GemFireVectorStore.DEFAULT_PORT; + + private boolean sslEnabled; + + private long connectionTimeout; + + private long requestTimeout; + + private String index; + + private int topK = GemFireVectorStore.DEFAULT_TOP_K; + + private int topKPerBucket = GemFireVectorStore.DEFAULT_TOP_K_PER_BUCKET; + + private String documentField = GemFireVectorStore.DEFAULT_DOCUMENT_FIELD; + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public boolean isSslEnabled() { + return sslEnabled; + } + + public void setSslEnabled(boolean sslEnabled) { + this.sslEnabled = sslEnabled; + } + + public long getConnectionTimeout() { + return connectionTimeout; + } + + public void setConnectionTimeout(long connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + public long getRequestTimeout() { + return requestTimeout; + } + + public void setRequestTimeout(long requestTimeout) { + this.requestTimeout = requestTimeout; + } + + public String getIndex() { + return index; + } + + public void setIndex(String index) { + this.index = index; + } + + public int getTopKPerBucket() { + return topKPerBucket; + } + + public void setTopKPerBucket(int topKPerBucket) { + this.topKPerBucket = topKPerBucket; + } + + public int getTopK() { + return topK; + } + + public void setTopK(int topK) { + this.topK = topK; + } + + public String getDocumentField() { + return documentField; + } + + public void setDocumentField(String documentField) { + this.documentField = documentField; + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/gemfire/GemFireVectorStoreAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/gemfire/GemFireVectorStoreAutoConfigurationIT.java new file mode 100644 index 0000000000..7dce84ffb4 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/gemfire/GemFireVectorStoreAutoConfigurationIT.java @@ -0,0 +1,113 @@ +/* + * Copyright 2024 - 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.gemfire; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.ai.ResourceUtils; +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.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Testcontainers; + +/** + * @author Philipp Kessler + */ +@Testcontainers +class GemFireVectorStoreAutoConfigurationIT { + + private static DockerComposeContainer gemFireContainer; + + @BeforeAll + public static void beforeAll() { + + gemFireContainer = new DockerComposeContainer(new File("src/test/resources/gemfire/docker-compose.yml")) + .withExposedService("gemfire", 7070, + Wait.forHttp("/gemfire-api/v1/ping") + .forStatusCode(200) + .withStartupTimeout(Duration.ofSeconds(100))); + gemFireContainer.start(); + } + + @AfterAll + public static void afterAll() { + gemFireContainer.stop(); + } + + 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(GemFireVectorStoreAutoConfiguration.class)) + .withUserConfiguration(Config.class) + .withPropertyValues("spring.ai.vectorstore.gemfire.index=test_index", + "spring.ai.vectorstore.gemfire.documentField=doc_chunk", + "spring.ai.vectorstore.gemfire.host=" + gemFireContainer.getServiceHost("gemfire", 7070), + "spring.ai.vectorstore.gemfire.port=" + gemFireContainer.getServicePort("gemfire", 7070)); + + @Test + void addAndSearch() { + contextRunner + + .run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + 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) + static class Config { + + @Bean + public EmbeddingModel embeddingModel() { + return new TransformersEmbeddingModel(); + } + + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/gemfire/GemFireVectorStorePropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/gemfire/GemFireVectorStorePropertiesTests.java new file mode 100644 index 0000000000..97fb8367c2 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/gemfire/GemFireVectorStorePropertiesTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2024 - 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.gemfire; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.ai.vectorstore.GemFireVectorStore; + +/** + * @author Philipp Kessler + */ +class GemFireVectorStorePropertiesTests { + + @Test + void defaultValues() { + var props = new GemFireVectorStoreProperties(); + assertThat(props.getHost()).isNull(); + assertThat(props.getPort()).isEqualTo(GemFireVectorStore.DEFAULT_PORT); + assertThat(props.isSslEnabled()).isEqualTo(false); + assertThat(props.getConnectionTimeout()).isEqualTo(0); + assertThat(props.getRequestTimeout()).isEqualTo(0); + assertThat(props.getIndex()).isNull(); + assertThat(props.getTopK()).isEqualTo(GemFireVectorStore.DEFAULT_TOP_K); + assertThat(props.getTopKPerBucket()).isEqualTo(GemFireVectorStore.DEFAULT_TOP_K_PER_BUCKET); + assertThat(props.getDocumentField()).isEqualTo(GemFireVectorStore.DEFAULT_DOCUMENT_FIELD); + } + + @Test + void customValues() { + var props = new GemFireVectorStoreProperties(); + props.setHost("127.0.0.1"); + props.setPort(9043); + props.setSslEnabled(true); + props.setConnectionTimeout(100); + props.setRequestTimeout(200); + props.setIndex("index"); + props.setTopK(10); + props.setTopKPerBucket(20); + props.setDocumentField("document"); + + assertThat(props.getHost()).isEqualTo("127.0.0.1"); + assertThat(props.getPort()).isEqualTo(9043); + assertThat(props.isSslEnabled()).isTrue(); + assertThat(props.getConnectionTimeout()).isEqualTo(100); + assertThat(props.getRequestTimeout()).isEqualTo(200); + assertThat(props.getIndex()).isEqualTo("index"); + assertThat(props.getTopK()).isEqualTo(10); + assertThat(props.getTopKPerBucket()).isEqualTo(20); + assertThat(props.getDocumentField()).isEqualTo("document"); + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/resources/gemfire/docker-compose.yml b/spring-ai-spring-boot-autoconfigure/src/test/resources/gemfire/docker-compose.yml new file mode 100644 index 0000000000..c62e75a2ac --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/test/resources/gemfire/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3.5' + +services: + gemfire: + image: gemfire/gemfire:9.15.11 + ports: + - "7070:7070" + environment: + - ACCEPT_TERMS=y + volumes: + - ./extensions:/gemfire/extensions + command: gfsh start server --start-rest-api + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:7070/gemfire-api/v1/ping"] + interval: 5s + timeout: 5s + retries: 5 + +networks: + default: + name: gemfire diff --git a/spring-ai-spring-boot-autoconfigure/src/test/resources/gemfire/extensions/vmware-gemfire-vectordb-1.1.0.gfm b/spring-ai-spring-boot-autoconfigure/src/test/resources/gemfire/extensions/vmware-gemfire-vectordb-1.1.0.gfm new file mode 100644 index 0000000000..b61b188d47 Binary files /dev/null and b/spring-ai-spring-boot-autoconfigure/src/test/resources/gemfire/extensions/vmware-gemfire-vectordb-1.1.0.gfm differ diff --git a/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/GemFireVectorStore.java b/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/GemFireVectorStore.java index c91fff95df..959c9d8edb 100644 --- a/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/GemFireVectorStore.java +++ b/vector-stores/spring-ai-gemfire-store/src/main/java/org/springframework/ai/vectorstore/GemFireVectorStore.java @@ -176,15 +176,15 @@ public GemFireVectorStoreConfig build() { } - private static final int DEFAULT_PORT = 9090; + public static final int DEFAULT_PORT = 9090; - public static final String DEFAULT_URI = "http{ssl}://{host}:{port}/gemfire-vectordb/v1/indexes"; + public static final int DEFAULT_TOP_K_PER_BUCKET = 10; - private static final int DEFAULT_TOP_K_PER_BUCKET = 10; + public static final int DEFAULT_TOP_K = 10; - private static final int DEFAULT_TOP_K = 10; + public static final String DEFAULT_DOCUMENT_FIELD = "document"; - private static final String DEFAULT_DOCUMENT_FIELD = "document"; + private static final String DEFAULT_URI = "http{ssl}://{host}:{port}/gemfire-vectordb/v1/indexes"; public String indexName;