From bb7bee984b887ea778ee383eea9540212244d893 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sat, 6 Apr 2024 19:06:35 +0200 Subject: [PATCH 01/35] feat: typesense vectorstore setup --- pom.xml | 2 + vector-stores/spring-ai-typesense/pom.xml | 60 +++++++++++++++++++ .../ai/vectorstore/TypesenseVectorStore.java | 23 +++++++ 3 files changed, 85 insertions(+) create mode 100644 vector-stores/spring-ai-typesense/pom.xml create mode 100644 vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java diff --git a/pom.xml b/pom.xml index 6a91121b88..feaf8dfa26 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,7 @@ vector-stores/spring-ai-elasticsearch-store spring-ai-spring-boot-starters/spring-ai-starter-watsonx-ai spring-ai-spring-boot-starters/spring-ai-starter-elasticsearch-store + vector-stores/spring-ai-typesense @@ -142,6 +143,7 @@ 11.6.1 4.5.1 1.7.1 + 0.5.0 0.0.4 diff --git a/vector-stores/spring-ai-typesense/pom.xml b/vector-stores/spring-ai-typesense/pom.xml new file mode 100644 index 0000000000..c2c8fdbd0d --- /dev/null +++ b/vector-stores/spring-ai-typesense/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + org.springframework.ai + spring-ai + 1.0.0-SNAPSHOT + ../../pom.xml + + + spring-ai-typesense + jar + Spring AI Typesense Vector Store + Spring AI Typesense Vector Store + 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-core + ${parent.version} + + + + org.typesense + typesense-java + ${typesense.version} + + + + + org.springframework.ai + spring-ai-test + ${parent.version} + test + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.testcontainers + junit-jupiter + test + + + + + \ No newline at end of file diff --git a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java new file mode 100644 index 0000000000..1e0b2f5668 --- /dev/null +++ b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java @@ -0,0 +1,23 @@ +package org.springframework.ai.vectorstore; + +import org.springframework.ai.document.Document; + +import java.util.List; +import java.util.Optional; + +public class TypesenseVectorStore implements VectorStore { + @Override + public void add(List documents) { + + } + + @Override + public Optional delete(List idList) { + return Optional.empty(); + } + + @Override + public List similaritySearch(SearchRequest request) { + return null; + } +} From 6e993a99a0e903dd73c839dd3dea348220d559a1 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sat, 6 Apr 2024 20:02:54 +0200 Subject: [PATCH 02/35] feat: typesense autoconfigure setup --- .../typesense/TypesenseConnectionDetails.java | 13 +++++ ...TypesenseVectorStoreAutoConfiguration.java | 46 +++++++++++++++ .../TypesenseVectorStoreProperties.java | 57 +++++++++++++++++++ .../ai/vectorstore/TypesenseVectorStore.java | 24 ++++---- 4 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseConnectionDetails.java create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreProperties.java diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseConnectionDetails.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseConnectionDetails.java new file mode 100644 index 0000000000..bdf03a2afe --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseConnectionDetails.java @@ -0,0 +1,13 @@ +package org.springframework.ai.autoconfigure.vectorstore.typesense; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +public interface TypesenseConnectionDetails extends ConnectionDetails { + + String getHost(); + + String getProtocol(); + + String getPort(); + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java new file mode 100644 index 0000000000..50e423db2b --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java @@ -0,0 +1,46 @@ +package org.springframework.ai.autoconfigure.vectorstore.typesense; + +import org.springframework.ai.embedding.EmbeddingClient; +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; + +@AutoConfiguration +@ConditionalOnClass({ EmbeddingClient.class }) +@EnableConfigurationProperties({ TypesenseVectorStoreProperties.class }) +public class TypesenseVectorStoreAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(TypesenseConnectionDetails.class) + public PropertiesTypesenseConnectionDetails typesenseConnectionDetails(TypesenseVectorStoreProperties properties) { + return new PropertiesTypesenseConnectionDetails(properties); + } + + private static class PropertiesTypesenseConnectionDetails implements TypesenseConnectionDetails { + + private final TypesenseVectorStoreProperties properties; + + PropertiesTypesenseConnectionDetails(TypesenseVectorStoreProperties properties) { + this.properties = properties; + } + + @Override + public String getProtocol() { + return this.properties.getProtocol(); + } + + @Override + public String getHost() { + return this.properties.getHost(); + } + + @Override + public String getPort() { + return this.properties.getPort(); + } + + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreProperties.java new file mode 100644 index 0000000000..b97a7ba34a --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreProperties.java @@ -0,0 +1,57 @@ +package org.springframework.ai.autoconfigure.vectorstore.typesense; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Pablo Sanchidrian Herrera + */ +@ConfigurationProperties(TypesenseVectorStoreProperties.CONFIG_PREFIX) +public class TypesenseVectorStoreProperties { + + public static final String CONFIG_PREFIX = "spring.ai.vectorstore.typesense"; + + private String protocol = "http"; + + private String host = "localhost"; + + private String port = "8108"; + + /** + * Typesense API key. This is the default api key when the user follows the Typesense + * quick start guide. + */ + private String apiKey = "xyz"; + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public String getPort() { + return port; + } + + public void setPort(String port) { + this.port = port; + } + + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + +} diff --git a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java index 1e0b2f5668..ae99a292af 100644 --- a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java +++ b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java @@ -6,18 +6,20 @@ import java.util.Optional; public class TypesenseVectorStore implements VectorStore { - @Override - public void add(List documents) { - } + @Override + public void add(List documents) { - @Override - public Optional delete(List idList) { - return Optional.empty(); - } + } + + @Override + public Optional delete(List idList) { + return Optional.empty(); + } + + @Override + public List similaritySearch(SearchRequest request) { + return null; + } - @Override - public List similaritySearch(SearchRequest request) { - return null; - } } From bc79d689cacad884fdde34a32e6944a804ceaa96 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sat, 6 Apr 2024 22:15:36 +0200 Subject: [PATCH 03/35] feat: add post bean initialization and create method --- .../ai/vectorstore/TypesenseVectorStore.java | 118 +++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java index ae99a292af..aeed586fbd 100644 --- a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java +++ b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java @@ -1,15 +1,86 @@ package org.springframework.ai.vectorstore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.ai.document.Document; +import org.springframework.ai.embedding.EmbeddingClient; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; +import org.typesense.api.Client; +import org.typesense.api.FieldTypes; +import org.typesense.model.CollectionSchema; +import org.typesense.model.Field; +import org.typesense.model.ImportDocumentsParameters; +import java.util.HashMap; import java.util.List; import java.util.Optional; -public class TypesenseVectorStore implements VectorStore { +/** + * @author Pablo Sanchidrian Herrera + */ +public class TypesenseVectorStore implements VectorStore, InitializingBean { + + private static final Logger logger = LoggerFactory.getLogger(TypesenseVectorStore.class); + + public static final String DOC_ID_FIELD_NAME = "doc_id"; + + public static final String CONTENT_FIELD_NAME = "content"; + + public static final String METADATA_FIELD_NAME = "metadata"; + + public static final String EMBEDDING_FIELD_NAME = "embedding"; + + private final Client client; + + private final EmbeddingClient embeddingClient; + + private final TypesenseConfig config; + + public static class TypesenseConfig { + + private String collectionName; + + } + + public TypesenseVectorStore(Client client, EmbeddingClient embeddingClient) { + this(client, embeddingClient, new TypesenseConfig()); + } + + public TypesenseVectorStore(Client client, EmbeddingClient embeddingClient, TypesenseConfig config) { + Assert.notNull(client, "MilvusServiceClient must not be null"); + Assert.notNull(embeddingClient, "EmbeddingClient must not be null"); + + this.client = client; + this.embeddingClient = embeddingClient; + this.config = config; + } @Override public void add(List documents) { + Assert.notNull(documents, "Documents must not be null"); + + List> documentList = documents.stream().map(document -> { + HashMap typesenseDoc = new HashMap<>(); + typesenseDoc.put(DOC_ID_FIELD_NAME, document.getId()); + typesenseDoc.put(CONTENT_FIELD_NAME, document.getContent()); + typesenseDoc.put(METADATA_FIELD_NAME, document.getMetadata()); + List embedding = this.embeddingClient.embed(document.getContent()); + + return typesenseDoc; + }).toList(); + ImportDocumentsParameters importDocumentsParameters = new ImportDocumentsParameters(); + importDocumentsParameters.action("upsert"); + + try { + this.client.collections(this.config.collectionName) + .documents() + .import_(documentList, importDocumentsParameters); + } + catch (Exception e) { + logger.error("Failed to add documents", e); + } } @Override @@ -19,7 +90,52 @@ public Optional delete(List idList) { @Override public List similaritySearch(SearchRequest request) { + Assert.notNull(request.getQuery(), "Query string must not be null"); return null; } + // --------------------------------------------------------------------------------- + // Initialization + // --------------------------------------------------------------------------------- + @Override + public void afterPropertiesSet() throws Exception { + this.createCollection(); + } + + private boolean hasCollection() { + try { + this.client.collections(this.config.collectionName).retrieve(); + return true; + } + catch (Exception e) { + return false; + } + } + + void createCollection() { + if (this.hasCollection()) { + logger.info("Collection {} already exists", this.config.collectionName); + return; + } + + CollectionSchema collectionSchema = new CollectionSchema(); + + collectionSchema.name(this.config.collectionName) + .addFieldsItem(new Field().name(DOC_ID_FIELD_NAME).type(FieldTypes.STRING).optional(false)) + .addFieldsItem(new Field().name(CONTENT_FIELD_NAME).type(FieldTypes.STRING).optional(false)) + .addFieldsItem(new Field().name(METADATA_FIELD_NAME).type(FieldTypes.OBJECT).optional(true)) + .addFieldsItem(new Field().name(EMBEDDING_FIELD_NAME) + .type(FieldTypes.FLOAT_ARRAY) + .numDim(this.embeddingClient.dimensions()) + .optional(false)); + + try { + this.client.collections().create(collectionSchema); + logger.info("Collection {} created", this.config.collectionName); + } + catch (Exception e) { + logger.error("Failed to create collection {}", this.config.collectionName, e); + } + } + } From 35612875ccacbf5940027607e081f5148c82877c Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sun, 7 Apr 2024 00:09:20 +0200 Subject: [PATCH 04/35] feat: add embedding field --- .../org/springframework/ai/vectorstore/TypesenseVectorStore.java | 1 + 1 file changed, 1 insertion(+) diff --git a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java index aeed586fbd..853e87b66a 100644 --- a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java +++ b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java @@ -66,6 +66,7 @@ public void add(List documents) { typesenseDoc.put(CONTENT_FIELD_NAME, document.getContent()); typesenseDoc.put(METADATA_FIELD_NAME, document.getMetadata()); List embedding = this.embeddingClient.embed(document.getContent()); + typesenseDoc.put(EMBEDDING_FIELD_NAME, embedding); return typesenseDoc; }).toList(); From 47bea8cc40cf1c9de786b09920db2abf281c06eb Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sun, 7 Apr 2024 00:13:15 +0200 Subject: [PATCH 05/35] fix: change datastore name, typo in code reuse --- .../springframework/ai/vectorstore/TypesenseVectorStore.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java index 853e87b66a..1a3fb3ab4a 100644 --- a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java +++ b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java @@ -48,7 +48,7 @@ public TypesenseVectorStore(Client client, EmbeddingClient embeddingClient) { } public TypesenseVectorStore(Client client, EmbeddingClient embeddingClient, TypesenseConfig config) { - Assert.notNull(client, "MilvusServiceClient must not be null"); + Assert.notNull(client, "Typesense must not be null"); Assert.notNull(embeddingClient, "EmbeddingClient must not be null"); this.client = client; From f32be953bdddb7bca135701fe0c20b7012fb1544 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sun, 7 Apr 2024 20:02:51 +0200 Subject: [PATCH 06/35] WIP: implement search --- .../ai/vectorstore/TypesenseVectorStore.java | 109 ++++++++++++++++-- 1 file changed, 101 insertions(+), 8 deletions(-) diff --git a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java index 1a3fb3ab4a..487eefb56e 100644 --- a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java +++ b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java @@ -8,12 +8,11 @@ import org.springframework.util.Assert; import org.typesense.api.Client; import org.typesense.api.FieldTypes; -import org.typesense.model.CollectionSchema; -import org.typesense.model.Field; -import org.typesense.model.ImportDocumentsParameters; +import org.typesense.model.*; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; /** @@ -39,12 +38,76 @@ public class TypesenseVectorStore implements VectorStore, InitializingBean { public static class TypesenseConfig { - private String collectionName; + private final String collectionName; + + private final int embeddingDimension; + + public TypesenseConfig(String collectionName, int embeddingDimension) { + this.collectionName = collectionName; + this.embeddingDimension = embeddingDimension; + } + + /** + * {@return the default config} + */ + public static TypesenseConfig defaultConfig() { + return builder().build(); + } + + private TypesenseConfig(Builder builder) { + this.collectionName = builder.collectionName; + this.embeddingDimension = builder.embeddingDimension; + } + + /** + * Start building a new configuration. + * @return The entry point for creating a new configuration. + */ + public static Builder builder() { + + return new Builder(); + } + + public static class Builder { + + private String collectionName; + + private int embeddingDimension; + + /** + * Set the collection name. + * @param collectionName The collection name. + * @return The builder. + */ + public Builder withCollectionName(String collectionName) { + this.collectionName = collectionName; + return this; + } + + /** + * Set the embedding dimension. + * @param embeddingDimension The embedding dimension. + * @return The builder. + */ + public Builder withEmbeddingDimension(int embeddingDimension) { + this.embeddingDimension = embeddingDimension; + return this; + } + + /** + * Build the configuration. + * @return The configuration. + */ + public TypesenseConfig build() { + return new TypesenseConfig(this); + } + + } } public TypesenseVectorStore(Client client, EmbeddingClient embeddingClient) { - this(client, embeddingClient, new TypesenseConfig()); + this(client, embeddingClient, TypesenseConfig.defaultConfig()); } public TypesenseVectorStore(Client client, EmbeddingClient embeddingClient, TypesenseConfig config) { @@ -86,15 +149,45 @@ public void add(List documents) { @Override public Optional delete(List idList) { - return Optional.empty(); + DeleteDocumentsParameters deleteDocumentsParameters = new DeleteDocumentsParameters(); + deleteDocumentsParameters.filterBy(DOC_ID_FIELD_NAME + ":[" + String.join(",", idList) + "]"); + + try { + int deletedDocst = (Integer) this.client.collections(this.config.collectionName) + .documents() + .delete(deleteDocumentsParameters) + .getOrDefault("num_deleted", 0); + return Optional.of(deletedDocst > 0); + } + catch (Exception e) { + logger.error("Failed to delete documents", e); + return Optional.of(Boolean.FALSE); + } } @Override public List similaritySearch(SearchRequest request) { Assert.notNull(request.getQuery(), "Query string must not be null"); - return null; - } + List embedding = this.embeddingClient.embed(request.getQuery()); + + HashMap search = new HashMap<>(); + search.put("collection", this.config.collectionName); + search.put("q", "*"); + search.put("vector_query", String.join(",", embedding.stream().map(String::valueOf).toList())); + + SearchParameters searchParameters = new SearchParameters() + .q("*") + .vectorQuery("vec:([" + String.join(",", embedding.stream().map(String::valueOf).toList()) + "])"); + + try { + SearchResult searchResult = client.collections(this.config.collectionName).documents().search(searchParameters); + return List.of(); + } catch (Exception e) { + logger.error("Failed to search documents", e); + return List.of(); + } + } // --------------------------------------------------------------------------------- // Initialization // --------------------------------------------------------------------------------- From eed6bfe2609a43f82c9af9d00dc36f653e8b1b31 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Tue, 9 Apr 2024 17:22:47 +0200 Subject: [PATCH 07/35] feat: add transformers embedding dependency --- vector-stores/spring-ai-typesense/pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/vector-stores/spring-ai-typesense/pom.xml b/vector-stores/spring-ai-typesense/pom.xml index c2c8fdbd0d..ef31939216 100644 --- a/vector-stores/spring-ai-typesense/pom.xml +++ b/vector-stores/spring-ai-typesense/pom.xml @@ -43,6 +43,13 @@ test + + org.springframework.ai + spring-ai-transformers + ${parent.version} + test + + org.springframework.boot spring-boot-starter-test From 2604ff370437aab369b562e1e695087fd3649b5e Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Tue, 9 Apr 2024 17:23:15 +0200 Subject: [PATCH 08/35] fix: create collection add nested field options --- .../ai/vectorstore/TypesenseVectorStore.java | 67 +++++++++++++------ 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java index 487eefb56e..5e2e36704b 100644 --- a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java +++ b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java @@ -12,7 +12,6 @@ import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Optional; /** @@ -22,7 +21,11 @@ public class TypesenseVectorStore implements VectorStore, InitializingBean { private static final Logger logger = LoggerFactory.getLogger(TypesenseVectorStore.class); - public static final String DOC_ID_FIELD_NAME = "doc_id"; + /** + * The name of the field that contains the document ID. It is mandatory to "id" as the + * field name because that is the name that typesense is going to look for. + */ + public static final String DOC_ID_FIELD_NAME = "id"; public static final String CONTENT_FIELD_NAME = "content"; @@ -34,15 +37,15 @@ public class TypesenseVectorStore implements VectorStore, InitializingBean { private final EmbeddingClient embeddingClient; - private final TypesenseConfig config; + private final TypesenseVectorStoreConfig config; - public static class TypesenseConfig { + public static class TypesenseVectorStoreConfig { private final String collectionName; private final int embeddingDimension; - public TypesenseConfig(String collectionName, int embeddingDimension) { + public TypesenseVectorStoreConfig(String collectionName, int embeddingDimension) { this.collectionName = collectionName; this.embeddingDimension = embeddingDimension; } @@ -50,11 +53,11 @@ public TypesenseConfig(String collectionName, int embeddingDimension) { /** * {@return the default config} */ - public static TypesenseConfig defaultConfig() { + public static TypesenseVectorStoreConfig defaultConfig() { return builder().build(); } - private TypesenseConfig(Builder builder) { + private TypesenseVectorStoreConfig(Builder builder) { this.collectionName = builder.collectionName; this.embeddingDimension = builder.embeddingDimension; } @@ -98,8 +101,8 @@ public Builder withEmbeddingDimension(int embeddingDimension) { * Build the configuration. * @return The configuration. */ - public TypesenseConfig build() { - return new TypesenseConfig(this); + public TypesenseVectorStoreConfig build() { + return new TypesenseVectorStoreConfig(this); } } @@ -107,10 +110,10 @@ public TypesenseConfig build() { } public TypesenseVectorStore(Client client, EmbeddingClient embeddingClient) { - this(client, embeddingClient, TypesenseConfig.defaultConfig()); + this(client, embeddingClient, TypesenseVectorStoreConfig.defaultConfig()); } - public TypesenseVectorStore(Client client, EmbeddingClient embeddingClient, TypesenseConfig config) { + public TypesenseVectorStore(Client client, EmbeddingClient embeddingClient, TypesenseVectorStoreConfig config) { Assert.notNull(client, "Typesense must not be null"); Assert.notNull(embeddingClient, "EmbeddingClient must not be null"); @@ -141,6 +144,8 @@ public void add(List documents) { this.client.collections(this.config.collectionName) .documents() .import_(documentList, importDocumentsParameters); + + logger.info("Added {} documents", documentList.size()); } catch (Exception e) { logger.error("Failed to add documents", e); @@ -153,11 +158,16 @@ public Optional delete(List idList) { deleteDocumentsParameters.filterBy(DOC_ID_FIELD_NAME + ":[" + String.join(",", idList) + "]"); try { - int deletedDocst = (Integer) this.client.collections(this.config.collectionName) + int deletedDocs = (Integer) this.client.collections(this.config.collectionName) .documents() .delete(deleteDocumentsParameters) .getOrDefault("num_deleted", 0); - return Optional.of(deletedDocst > 0); + + if (deletedDocs < idList.size()) { + logger.warn("Failed to delete all documents"); + } + + return Optional.of(deletedDocs > 0); } catch (Exception e) { logger.error("Failed to delete documents", e); @@ -176,18 +186,21 @@ public List similaritySearch(SearchRequest request) { search.put("q", "*"); search.put("vector_query", String.join(",", embedding.stream().map(String::valueOf).toList())); - SearchParameters searchParameters = new SearchParameters() - .q("*") - .vectorQuery("vec:([" + String.join(",", embedding.stream().map(String::valueOf).toList()) + "])"); + SearchParameters searchParameters = new SearchParameters().q("*") + .vectorQuery("vec:([" + String.join(",", embedding.stream().map(String::valueOf).toList()) + "])"); try { - SearchResult searchResult = client.collections(this.config.collectionName).documents().search(searchParameters); + SearchResult searchResult = client.collections(this.config.collectionName) + .documents() + .search(searchParameters); return List.of(); - } catch (Exception e) { + } + catch (Exception e) { logger.error("Failed to search documents", e); return List.of(); } } + // --------------------------------------------------------------------------------- // Initialization // --------------------------------------------------------------------------------- @@ -221,7 +234,8 @@ void createCollection() { .addFieldsItem(new Field().name(EMBEDDING_FIELD_NAME) .type(FieldTypes.FLOAT_ARRAY) .numDim(this.embeddingClient.dimensions()) - .optional(false)); + .optional(false)) + .enableNestedFields(true); try { this.client.collections().create(collectionSchema); @@ -232,4 +246,19 @@ void createCollection() { } } + void dropCollection() { + if (!this.hasCollection()) { + logger.info("Collection {} does not exist", this.config.collectionName); + return; + } + + try { + this.client.collections(this.config.collectionName).delete(); + logger.info("Collection {} dropped", this.config.collectionName); + } + catch (Exception e) { + logger.error("Failed to drop collection {}", this.config.collectionName, e); + } + } + } From d0396129d4f1e10785ffae918b5f1b1bc6342674 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Tue, 9 Apr 2024 17:23:52 +0200 Subject: [PATCH 09/35] feat: add typesense tests | WIP --- .../vectorstore/TypesenseVectorStoreIT.java | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java diff --git a/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java b/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java new file mode 100644 index 0000000000..cb48976c64 --- /dev/null +++ b/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java @@ -0,0 +1,112 @@ +package org.springframework.ai.vectorstore; + +import org.junit.jupiter.api.Test; +import org.springframework.ai.document.Document; +import org.springframework.ai.embedding.EmbeddingClient; +import org.springframework.ai.transformers.TransformersEmbeddingClient; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.core.io.DefaultResourceLoader; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.typesense.api.Client; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.springframework.ai.vectorstore.TypesenseVectorStore.TypesenseVectorStoreConfig; +import org.typesense.api.Configuration; +import org.typesense.resources.Node; + +import static org.assertj.core.api.Assertions.assertThat; + +@Testcontainers +public class TypesenseVectorStoreIT { + + @Container + private static final GenericContainer typesenseContainer = new GenericContainer<>("typesense/typesense:26.0") + .withExposedPorts(8108) + .withCommand("--data-dir", "/data", "--api-key=xyz", "--enable-cors") + .withFileSystemBind("typesenseDataVolume", "/data", BindMode.READ_WRITE); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(TestApplication.class); + + List documents = List.of( + new Document(getText("classpath:/test/data/spring.ai.txt"), Map.of("meta1", "meta1")), + new Document(getText("classpath:/test/data/time.shelter.txt")), + new Document(getText("classpath:/test/data/great.depression.txt"), Map.of("meta2", "meta2"))); + + 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); + } + } + + private void resetCollection(VectorStore vectorStore) { + ((TypesenseVectorStore) vectorStore).dropCollection(); + ((TypesenseVectorStore) vectorStore).createCollection(); + } + + @Test + void addAndSearch() { + + contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + + resetCollection(vectorStore); + + vectorStore.add(documents); + + List results = vectorStore.similaritySearch(SearchRequest.query("Spring")); + + assertThat(results).hasSize(1); + }); + } + + @SpringBootConfiguration + @EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class }) + public static class TestApplication { + + @Bean + public VectorStore vectorStore(Client client, EmbeddingClient embeddingClient) { + + TypesenseVectorStoreConfig config = TypesenseVectorStoreConfig.builder() + .withCollectionName("test_vector_store") + .withEmbeddingDimension(embeddingClient.dimensions()) + .build(); + + return new TypesenseVectorStore(client, embeddingClient, config); + } + + @Bean + public Client typesenseClient() { + List nodes = new ArrayList<>(); + nodes + .add(new Node("http", typesenseContainer.getHost(), typesenseContainer.getMappedPort(8108).toString())); + + Configuration configuration = new Configuration(nodes, Duration.ofSeconds(5), "xyz"); + return new Client(configuration); + } + + @Bean + public EmbeddingClient embeddingClient() { + return new TransformersEmbeddingClient(); + } + + } + +} From 180a2d9a7057308f42f4025d397fb5c4d28e60c4 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Tue, 9 Apr 2024 17:46:41 +0200 Subject: [PATCH 10/35] feat: add temporary directory as typesense need it --- .../vectorstore/TypesenseVectorStoreIT.java | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java b/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java index cb48976c64..76d33847fd 100644 --- a/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java +++ b/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java @@ -1,5 +1,7 @@ package org.springframework.ai.vectorstore; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingClient; @@ -18,6 +20,8 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -32,11 +36,21 @@ @Testcontainers public class TypesenseVectorStoreIT { - @Container - private static final GenericContainer typesenseContainer = new GenericContainer<>("typesense/typesense:26.0") - .withExposedPorts(8108) - .withCommand("--data-dir", "/data", "--api-key=xyz", "--enable-cors") - .withFileSystemBind("typesenseDataVolume", "/data", BindMode.READ_WRITE); + private static Path tempDirectory; + + static { + try { + tempDirectory = Files.createTempDirectory("typesense-test"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Container + private static GenericContainer typesenseContainer = new GenericContainer<>("typesense/typesense:26.0") + .withExposedPorts(8108) + .withCommand("--data-dir", "/data", "--api-key=xyz", "--enable-cors") + .withFileSystemBind(tempDirectory.toString(), "/data", BindMode.READ_WRITE); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withUserConfiguration(TestApplication.class); @@ -109,4 +123,15 @@ public EmbeddingClient embeddingClient() { } + @AfterAll + static void deleteContainer() { + if(typesenseContainer != null) { + typesenseContainer.stop(); + } + + if(tempDirectory != null) { + tempDirectory.toFile().delete(); + } + } + } From 50cad2a715135a4d605a61b7bc5481bcfe7bd749 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sun, 5 May 2024 19:16:27 +0200 Subject: [PATCH 11/35] fix: use embedding variable instead of word vec --- .../ai/vectorstore/TypesenseVectorStore.java | 56 ++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java index 5e2e36704b..6b8bb6a6c7 100644 --- a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java +++ b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java @@ -1,5 +1,6 @@ package org.springframework.ai.vectorstore; +import com.fasterxml.jackson.databind.util.JSONPObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.document.Document; @@ -10,9 +11,8 @@ import org.typesense.api.FieldTypes; import org.typesense.model.*; -import java.util.HashMap; -import java.util.List; -import java.util.Optional; +import java.util.*; +import java.util.stream.Collectors; /** * @author Pablo Sanchidrian Herrera @@ -155,7 +155,7 @@ public void add(List documents) { @Override public Optional delete(List idList) { DeleteDocumentsParameters deleteDocumentsParameters = new DeleteDocumentsParameters(); - deleteDocumentsParameters.filterBy(DOC_ID_FIELD_NAME + ":[" + String.join(",", idList) + "]"); + deleteDocumentsParameters.filterBy(DOC_ID_FIELD_NAME + ":=[" + String.join(",", idList) + "]"); try { int deletedDocs = (Integer) this.client.collections(this.config.collectionName) @@ -181,19 +181,34 @@ public List similaritySearch(SearchRequest request) { List embedding = this.embeddingClient.embed(request.getQuery()); - HashMap search = new HashMap<>(); - search.put("collection", this.config.collectionName); - search.put("q", "*"); - search.put("vector_query", String.join(",", embedding.stream().map(String::valueOf).toList())); + MultiSearchCollectionParameters multiSearchCollectionParameters = new MultiSearchCollectionParameters(); + multiSearchCollectionParameters.collection(this.config.collectionName); + multiSearchCollectionParameters.q("*"); + multiSearchCollectionParameters.vectorQuery(EMBEDDING_FIELD_NAME + ":([" + String.join(",", embedding.stream().map(String::valueOf).toList()) + "], k: 100)"); - SearchParameters searchParameters = new SearchParameters().q("*") - .vectorQuery("vec:([" + String.join(",", embedding.stream().map(String::valueOf).toList()) + "])"); + MultiSearchSearchesParameter multiSearchesParameter = new MultiSearchSearchesParameter().addSearchesItem(multiSearchCollectionParameters); try { - SearchResult searchResult = client.collections(this.config.collectionName) - .documents() - .search(searchParameters); - return List.of(); + MultiSearchResult result = this.client.multiSearch.perform(multiSearchesParameter, Map.of("query_by", EMBEDDING_FIELD_NAME)); + + List documents = result.getResults() + .stream() + .flatMap(searchResult -> + searchResult.getHits() + .stream() + .filter(hit -> hit.getVectorDistance() >= request.getSimilarityThreshold()) + .map(hit -> { + Map rawDocument = hit.getDocument(); + String docId = rawDocument.get(DOC_ID_FIELD_NAME).toString(); + String content = rawDocument.get(CONTENT_FIELD_NAME).toString(); + Map metadata = rawDocument.get(METADATA_FIELD_NAME) instanceof Map ? (Map) rawDocument.get(METADATA_FIELD_NAME) : Map.of(); + return new Document(docId, content, metadata); + }) + ) + .toList(); + + logger.info("Found {} documents", documents.size()); + return documents; } catch (Exception e) { logger.error("Failed to search documents", e); @@ -204,6 +219,7 @@ public List similaritySearch(SearchRequest request) { // --------------------------------------------------------------------------------- // Initialization // --------------------------------------------------------------------------------- + @Override public void afterPropertiesSet() throws Exception { this.createCollection(); @@ -261,4 +277,16 @@ void dropCollection() { } } + Map getCollectionInfo() { + try { + CollectionResponse retrievedCollection = this.client.collections(this.config.collectionName).retrieve(); + return Map.of("name", retrievedCollection.getName(), "num_documents", retrievedCollection.getNumDocuments()); + } + catch (Exception e) { + logger.error("Failed to retrieve collection info", e); + return null; + } + + } + } From 9432dc5105045a4d4702bdd67dfcbab76775bba4 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sun, 5 May 2024 19:17:06 +0200 Subject: [PATCH 12/35] feat: check in runtime the number of documents in the collection --- .../vectorstore/TypesenseVectorStoreIT.java | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java b/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java index 76d33847fd..45a67c1b80 100644 --- a/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java +++ b/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java @@ -38,19 +38,20 @@ public class TypesenseVectorStoreIT { private static Path tempDirectory; - static { - try { - tempDirectory = Files.createTempDirectory("typesense-test"); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Container + static { + try { + tempDirectory = Files.createTempDirectory("typesense-test"); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Container private static GenericContainer typesenseContainer = new GenericContainer<>("typesense/typesense:26.0") - .withExposedPorts(8108) - .withCommand("--data-dir", "/data", "--api-key=xyz", "--enable-cors") - .withFileSystemBind(tempDirectory.toString(), "/data", BindMode.READ_WRITE); + .withExposedPorts(8108) + .withCommand("--data-dir", "/data", "--api-key=xyz", "--enable-cors") + .withFileSystemBind(tempDirectory.toString(), "/data", BindMode.READ_WRITE); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withUserConfiguration(TestApplication.class); @@ -85,9 +86,13 @@ void addAndSearch() { vectorStore.add(documents); + Map info = ((TypesenseVectorStore) vectorStore).getCollectionInfo(); + + assertThat(info.get("num_documents")).isEqualTo(3L); + List results = vectorStore.similaritySearch(SearchRequest.query("Spring")); - assertThat(results).hasSize(1); + assertThat(results).hasSize(3); }); } @@ -124,12 +129,12 @@ public EmbeddingClient embeddingClient() { } @AfterAll - static void deleteContainer() { - if(typesenseContainer != null) { + static void deleteContainer() { + if (typesenseContainer != null) { typesenseContainer.stop(); } - if(tempDirectory != null) { + if (tempDirectory != null) { tempDirectory.toFile().delete(); } } From d79b8939218442945b73face251905958e66d584 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sat, 11 May 2024 20:19:40 +0200 Subject: [PATCH 13/35] feat: add typesense expression converter --- .../TypesenseFilterExpressionConverter.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/TypesenseFilterExpressionConverter.java diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/TypesenseFilterExpressionConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/TypesenseFilterExpressionConverter.java new file mode 100644 index 0000000000..6733b5022d --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/TypesenseFilterExpressionConverter.java @@ -0,0 +1,57 @@ +package org.springframework.ai.vectorstore.filter.converter; + +import org.springframework.ai.vectorstore.filter.Filter; + +/** + * Converts {@link Filter.Expression} into Typesense metadata filter expression format. + * (https://typesense.org/docs/0.24.0/api/search.html#filter-parameters) + * + * @author Pablo Sanchidrian + */ +public class TypesenseFilterExpressionConverter extends AbstractFilterExpressionConverter { + + @Override + protected void doExpression(Filter.Expression exp, StringBuilder context) { + this.convertOperand(exp.left(), context); + context.append(getOperationSymbol(exp)); + this.convertOperand(exp.right(), context); + } + + private String getOperationSymbol(Filter.Expression exp) { + switch (exp.type()) { + case AND: + return " && "; + case OR: + return " || "; + case EQ: + return " "; // in typesense "EQ" operator looks like -> country:USA + case NE: + return " != "; + case LT: + return " < "; + case LTE: + return " <= "; + case GT: + return " > "; + case GTE: + return " >= "; + case IN: + return " "; // in typesense "IN" operator looks like -> country: [USA, UK] + case NIN: + return " != "; // in typesense "NIN" operator looks like -> country: !=[USA, UK] + default: + throw new RuntimeException("Not supported expression type:" + exp.type()); + } + } + + @Override + protected void doGroup(Filter.Group group, StringBuilder context) { + this.convertOperand(new Filter.Expression(Filter.ExpressionType.AND, group.content(), group.content()), context); // trick + } + + @Override + protected void doKey(Filter.Key key, StringBuilder context) { + context.append("metadata." + key.key() + ":"); + } + +} \ No newline at end of file From 7ae6982782fcf69d615c1a6f048bfd98c11fcf05 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sat, 11 May 2024 20:20:12 +0200 Subject: [PATCH 14/35] feat: add expression converter --- .../ai/vectorstore/TypesenseVectorStore.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java index 6b8bb6a6c7..2760fef70f 100644 --- a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java +++ b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java @@ -5,6 +5,8 @@ import org.slf4j.LoggerFactory; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingClient; +import org.springframework.ai.vectorstore.filter.FilterExpressionConverter; +import org.springframework.ai.vectorstore.filter.converter.TypesenseFilterExpressionConverter; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; import org.typesense.api.Client; @@ -38,6 +40,7 @@ public class TypesenseVectorStore implements VectorStore, InitializingBean { private final EmbeddingClient embeddingClient; private final TypesenseVectorStoreConfig config; + public final FilterExpressionConverter filterExpressionConverter = new TypesenseFilterExpressionConverter(); public static class TypesenseVectorStoreConfig { @@ -179,12 +182,25 @@ public Optional delete(List idList) { public List similaritySearch(SearchRequest request) { Assert.notNull(request.getQuery(), "Query string must not be null"); + String nativeFilterExpressions = (request.getFilterExpression() != null) + ? this.filterExpressionConverter.convertExpression(request.getFilterExpression()) : ""; + + logger.info("Filter expression: {}", nativeFilterExpressions); + List embedding = this.embeddingClient.embed(request.getQuery()); MultiSearchCollectionParameters multiSearchCollectionParameters = new MultiSearchCollectionParameters(); multiSearchCollectionParameters.collection(this.config.collectionName); multiSearchCollectionParameters.q("*"); - multiSearchCollectionParameters.vectorQuery(EMBEDDING_FIELD_NAME + ":([" + String.join(",", embedding.stream().map(String::valueOf).toList()) + "], k: 100)"); + + // typesnese usues only cosine similarity and shifted by 1 [-1, 1] -> [0, 2] + String vectorQuery = EMBEDDING_FIELD_NAME + ":(" + + "[" + String.join(",", embedding.stream().map(String::valueOf).toList()) + "], " + + "k: " + request.getTopK() + ", " + + "distance_threshold: " + (1 + request.getSimilarityThreshold()) + ")"; + + multiSearchCollectionParameters.vectorQuery(vectorQuery); + multiSearchCollectionParameters.filterBy(nativeFilterExpressions); MultiSearchSearchesParameter multiSearchesParameter = new MultiSearchSearchesParameter().addSearchesItem(multiSearchCollectionParameters); @@ -196,7 +212,6 @@ public List similaritySearch(SearchRequest request) { .flatMap(searchResult -> searchResult.getHits() .stream() - .filter(hit -> hit.getVectorDistance() >= request.getSimilarityThreshold()) .map(hit -> { Map rawDocument = hit.getDocument(); String docId = rawDocument.get(DOC_ID_FIELD_NAME).toString(); @@ -208,6 +223,7 @@ public List similaritySearch(SearchRequest request) { .toList(); logger.info("Found {} documents", documents.size()); + logger.info("Docs: \n {}", result); return documents; } catch (Exception e) { From 65d79c7dd07290cb1ab1e4a02ee638bcf83aeb85 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sat, 11 May 2024 20:20:25 +0200 Subject: [PATCH 15/35] feat: add filter tests --- .../vectorstore/TypesenseVectorStoreIT.java | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java b/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java index 45a67c1b80..93b83d79d2 100644 --- a/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java +++ b/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java @@ -1,7 +1,6 @@ package org.springframework.ai.vectorstore; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingClient; @@ -96,6 +95,69 @@ void addAndSearch() { }); } + @Test + void searchWithFilters() { + + contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + + resetCollection(vectorStore); + + var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "BG", "year", 2020)); + var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "NL")); + var bgDocument2 = new Document("The World is Big and Salvation Lurks Around the Corner", + Map.of("country", "BG", "year", 2023)); + + vectorStore.add(List.of(bgDocument, nlDocument, bgDocument2)); + + List results = vectorStore.similaritySearch(SearchRequest.query("The World").withTopK(5)); + assertThat(results).hasSize(3); + + results = vectorStore.similaritySearch(SearchRequest.query("The World") + .withTopK(5) + .withSimilarityThresholdAll() + .withFilterExpression("country == 'NL'")); + assertThat(results).hasSize(1); + assertThat(results.get(0).getId()).isEqualTo(nlDocument.getId()); + + results = vectorStore.similaritySearch(SearchRequest.query("The World") + .withTopK(5) + .withSimilarityThresholdAll() + .withFilterExpression("country == 'BG'")); + + assertThat(results).hasSize(2); + assertThat(results.get(0).getId()).isIn(bgDocument.getId(), bgDocument2.getId()); + assertThat(results.get(1).getId()).isIn(bgDocument.getId(), bgDocument2.getId()); + + results = vectorStore.similaritySearch(SearchRequest.query("The World") + .withTopK(5) + .withSimilarityThresholdAll() + .withFilterExpression("country == 'BG' && year == 2020")); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getId()).isEqualTo(bgDocument.getId()); + + results = vectorStore.similaritySearch(SearchRequest.query("The World") + .withTopK(5) + .withSimilarityThresholdAll() + .withFilterExpression("NOT(country == 'BG' && year == 2020)")); + + assertThat(results).hasSize(2); + assertThat(results.get(0).getId()).isIn(nlDocument.getId(), bgDocument2.getId()); + assertThat(results.get(1).getId()).isIn(nlDocument.getId(), bgDocument2.getId()); + + }); + } + +// @Test +// void addAndSearchWithThreshold() { +// contextRunner.run(context -> { +// +// }) +// } + @SpringBootConfiguration @EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class }) public static class TestApplication { From 0dd707419311c7e81714c6dc686f541fb997cfbe Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sat, 11 May 2024 20:34:45 +0200 Subject: [PATCH 16/35] feat: add update document test and search with threshold test --- .../vectorstore/TypesenseVectorStoreIT.java | 91 +++++++++++++++++-- 1 file changed, 82 insertions(+), 9 deletions(-) diff --git a/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java b/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java index 93b83d79d2..35fd087f0b 100644 --- a/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java +++ b/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java @@ -22,9 +22,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; import org.springframework.ai.vectorstore.TypesenseVectorStore.TypesenseVectorStoreConfig; import org.typesense.api.Configuration; @@ -75,6 +73,55 @@ private void resetCollection(VectorStore vectorStore) { ((TypesenseVectorStore) vectorStore).createCollection(); } + @Test + void documentUpdate() { + contextRunner.run(context -> { + + VectorStore vectorStore = context.getBean(VectorStore.class); + + resetCollection(vectorStore); + + Document document = new Document(UUID.randomUUID().toString(), "Spring AI rocks!!", + Collections.singletonMap("meta1", "meta1")); + + vectorStore.add(List.of(document)); + + Map info = ((TypesenseVectorStore) vectorStore).getCollectionInfo(); + assertThat(info.get("num_documents")).isEqualTo(1L); + + List results = vectorStore.similaritySearch(SearchRequest.query("Spring").withTopK(5)); + + assertThat(results).hasSize(1); + Document resultDoc = results.get(0); + assertThat(resultDoc.getId()).isEqualTo(document.getId()); + assertThat(resultDoc.getContent()).isEqualTo("Spring AI rocks!!"); + assertThat(resultDoc.getMetadata()).containsKey("meta1"); + + Document sameIdDocument = new Document(document.getId(), + "The World is Big and Salvation Lurks Around the Corner", + Collections.singletonMap("meta2", "meta2")); + + vectorStore.add(List.of(sameIdDocument)); + + info = ((TypesenseVectorStore) vectorStore).getCollectionInfo(); + assertThat(info.get("num_documents")).isEqualTo(1L); + + results = vectorStore.similaritySearch(SearchRequest.query("FooBar").withTopK(5)); + + assertThat(results).hasSize(1); + resultDoc = results.get(0); + assertThat(resultDoc.getId()).isEqualTo(document.getId()); + assertThat(resultDoc.getContent()).isEqualTo("The World is Big and Salvation Lurks Around the Corner"); + assertThat(resultDoc.getMetadata()).containsKey("meta2"); + + vectorStore.delete(List.of(document.getId())); + + info = ((TypesenseVectorStore) vectorStore).getCollectionInfo(); + assertThat(info.get("num_documents")).isEqualTo(0L); + + }); + } + @Test void addAndSearch() { @@ -151,12 +198,38 @@ void searchWithFilters() { }); } -// @Test -// void addAndSearchWithThreshold() { -// contextRunner.run(context -> { -// -// }) -// } + @Test + void searchWithThreshold() { + + contextRunner.run(context -> { + + VectorStore vectorStore = context.getBean(VectorStore.class); + + resetCollection(vectorStore); + + vectorStore.add(documents); + + List fullResult = vectorStore + .similaritySearch(SearchRequest.query("Spring").withTopK(5).withSimilarityThresholdAll()); + + List distances = fullResult.stream().map(doc -> (Float) doc.getMetadata().get("distance")).toList(); + + assertThat(distances).hasSize(3); + + float threshold = (distances.get(0) + distances.get(1)) / 2; + + List results = vectorStore + .similaritySearch(SearchRequest.query("Spring").withTopK(5).withSimilarityThreshold(1 - threshold)); + + 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()).containsKeys("meta1", "distance"); + + }); + } @SpringBootConfiguration @EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class }) From bbf3805ca3d7093a3e5d05319594a033bcffa6fc Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sun, 12 May 2024 21:36:10 +0200 Subject: [PATCH 17/35] fix: apply linter --- .../TypesenseFilterExpressionConverter.java | 82 ++++++++++--------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/TypesenseFilterExpressionConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/TypesenseFilterExpressionConverter.java index 6733b5022d..ae431327d7 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/TypesenseFilterExpressionConverter.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/TypesenseFilterExpressionConverter.java @@ -10,48 +10,50 @@ */ public class TypesenseFilterExpressionConverter extends AbstractFilterExpressionConverter { - @Override - protected void doExpression(Filter.Expression exp, StringBuilder context) { - this.convertOperand(exp.left(), context); - context.append(getOperationSymbol(exp)); - this.convertOperand(exp.right(), context); - } + @Override + protected void doExpression(Filter.Expression exp, StringBuilder context) { + this.convertOperand(exp.left(), context); + context.append(getOperationSymbol(exp)); + this.convertOperand(exp.right(), context); + } - private String getOperationSymbol(Filter.Expression exp) { - switch (exp.type()) { - case AND: - return " && "; - case OR: - return " || "; - case EQ: - return " "; // in typesense "EQ" operator looks like -> country:USA - case NE: - return " != "; - case LT: - return " < "; - case LTE: - return " <= "; - case GT: - return " > "; - case GTE: - return " >= "; - case IN: - return " "; // in typesense "IN" operator looks like -> country: [USA, UK] - case NIN: - return " != "; // in typesense "NIN" operator looks like -> country: !=[USA, UK] - default: - throw new RuntimeException("Not supported expression type:" + exp.type()); - } - } + private String getOperationSymbol(Filter.Expression exp) { + switch (exp.type()) { + case AND: + return " && "; + case OR: + return " || "; + case EQ: + return " "; // in typesense "EQ" operator looks like -> country:USA + case NE: + return " != "; + case LT: + return " < "; + case LTE: + return " <= "; + case GT: + return " > "; + case GTE: + return " >= "; + case IN: + return " "; // in typesense "IN" operator looks like -> country: [USA, UK] + case NIN: + return " != "; // in typesense "NIN" operator looks like -> country: + // !=[USA, UK] + default: + throw new RuntimeException("Not supported expression type:" + exp.type()); + } + } - @Override - protected void doGroup(Filter.Group group, StringBuilder context) { - this.convertOperand(new Filter.Expression(Filter.ExpressionType.AND, group.content(), group.content()), context); // trick - } + @Override + protected void doGroup(Filter.Group group, StringBuilder context) { + this.convertOperand(new Filter.Expression(Filter.ExpressionType.AND, group.content(), group.content()), + context); // trick + } - @Override - protected void doKey(Filter.Key key, StringBuilder context) { - context.append("metadata." + key.key() + ":"); - } + @Override + protected void doKey(Filter.Key key, StringBuilder context) { + context.append("metadata." + key.key() + ":"); + } } \ No newline at end of file From f6a6733b85e8d1a62d21b59cf42b3b19d75da623 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sun, 12 May 2024 21:37:54 +0200 Subject: [PATCH 18/35] fix: add distance assert --- .../vectorstore/TypesenseVectorStoreIT.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java b/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java index 35fd087f0b..57d2d6309c 100644 --- a/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java +++ b/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java @@ -96,6 +96,7 @@ void documentUpdate() { assertThat(resultDoc.getId()).isEqualTo(document.getId()); assertThat(resultDoc.getContent()).isEqualTo("Spring AI rocks!!"); assertThat(resultDoc.getMetadata()).containsKey("meta1"); + assertThat(resultDoc.getMetadata()).containsKey("distance"); Document sameIdDocument = new Document(document.getId(), "The World is Big and Salvation Lurks Around the Corner", @@ -113,6 +114,7 @@ void documentUpdate() { assertThat(resultDoc.getId()).isEqualTo(document.getId()); assertThat(resultDoc.getContent()).isEqualTo("The World is Big and Salvation Lurks Around the Corner"); assertThat(resultDoc.getMetadata()).containsKey("meta2"); + assertThat(resultDoc.getMetadata()).containsKey("distance"); vectorStore.delete(List.of(document.getId())); @@ -163,33 +165,33 @@ void searchWithFilters() { assertThat(results).hasSize(3); results = vectorStore.similaritySearch(SearchRequest.query("The World") - .withTopK(5) - .withSimilarityThresholdAll() - .withFilterExpression("country == 'NL'")); + .withTopK(5) + .withSimilarityThresholdAll() + .withFilterExpression("country == 'NL'")); assertThat(results).hasSize(1); assertThat(results.get(0).getId()).isEqualTo(nlDocument.getId()); results = vectorStore.similaritySearch(SearchRequest.query("The World") - .withTopK(5) - .withSimilarityThresholdAll() - .withFilterExpression("country == 'BG'")); + .withTopK(5) + .withSimilarityThresholdAll() + .withFilterExpression("country == 'BG'")); assertThat(results).hasSize(2); assertThat(results.get(0).getId()).isIn(bgDocument.getId(), bgDocument2.getId()); assertThat(results.get(1).getId()).isIn(bgDocument.getId(), bgDocument2.getId()); results = vectorStore.similaritySearch(SearchRequest.query("The World") - .withTopK(5) - .withSimilarityThresholdAll() - .withFilterExpression("country == 'BG' && year == 2020")); + .withTopK(5) + .withSimilarityThresholdAll() + .withFilterExpression("country == 'BG' && year == 2020")); assertThat(results).hasSize(1); assertThat(results.get(0).getId()).isEqualTo(bgDocument.getId()); results = vectorStore.similaritySearch(SearchRequest.query("The World") - .withTopK(5) - .withSimilarityThresholdAll() - .withFilterExpression("NOT(country == 'BG' && year == 2020)")); + .withTopK(5) + .withSimilarityThresholdAll() + .withFilterExpression("NOT(country == 'BG' && year == 2020)")); assertThat(results).hasSize(2); assertThat(results.get(0).getId()).isIn(nlDocument.getId(), bgDocument2.getId()); @@ -210,7 +212,7 @@ void searchWithThreshold() { vectorStore.add(documents); List fullResult = vectorStore - .similaritySearch(SearchRequest.query("Spring").withTopK(5).withSimilarityThresholdAll()); + .similaritySearch(SearchRequest.query("Spring").withTopK(5).withSimilarityThresholdAll()); List distances = fullResult.stream().map(doc -> (Float) doc.getMetadata().get("distance")).toList(); @@ -219,7 +221,7 @@ void searchWithThreshold() { float threshold = (distances.get(0) + distances.get(1)) / 2; List results = vectorStore - .similaritySearch(SearchRequest.query("Spring").withTopK(5).withSimilarityThreshold(1 - threshold)); + .similaritySearch(SearchRequest.query("Spring").withTopK(5).withSimilarityThreshold(1 - threshold)); assertThat(results).hasSize(1); Document resultDoc = results.get(0); From a240c6efe6e7bcb18eb1fc7defcaee423106fe15 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sun, 12 May 2024 21:39:29 +0200 Subject: [PATCH 19/35] fix: distance threshold and add distance key into metadata --- .../ai/vectorstore/TypesenseVectorStore.java | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java index 2760fef70f..cd01ae2b7f 100644 --- a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java +++ b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java @@ -40,6 +40,7 @@ public class TypesenseVectorStore implements VectorStore, InitializingBean { private final EmbeddingClient embeddingClient; private final TypesenseVectorStoreConfig config; + public final FilterExpressionConverter filterExpressionConverter = new TypesenseFilterExpressionConverter(); public static class TypesenseVectorStoreConfig { @@ -193,37 +194,35 @@ public List similaritySearch(SearchRequest request) { multiSearchCollectionParameters.collection(this.config.collectionName); multiSearchCollectionParameters.q("*"); - // typesnese usues only cosine similarity and shifted by 1 [-1, 1] -> [0, 2] - String vectorQuery = EMBEDDING_FIELD_NAME + ":(" + - "[" + String.join(",", embedding.stream().map(String::valueOf).toList()) + "], " + - "k: " + request.getTopK() + ", " + - "distance_threshold: " + (1 + request.getSimilarityThreshold()) + ")"; + // typesnese uses only cosine similarity + String vectorQuery = EMBEDDING_FIELD_NAME + ":(" + "[" + + String.join(",", embedding.stream().map(String::valueOf).toList()) + "], " + "k: " + request.getTopK() + + ", " + "distance_threshold: " + (1 - request.getSimilarityThreshold()) + ")"; - multiSearchCollectionParameters.vectorQuery(vectorQuery); + multiSearchCollectionParameters.vectorQuery(vectorQuery); multiSearchCollectionParameters.filterBy(nativeFilterExpressions); - MultiSearchSearchesParameter multiSearchesParameter = new MultiSearchSearchesParameter().addSearchesItem(multiSearchCollectionParameters); + MultiSearchSearchesParameter multiSearchesParameter = new MultiSearchSearchesParameter() + .addSearchesItem(multiSearchCollectionParameters); try { - MultiSearchResult result = this.client.multiSearch.perform(multiSearchesParameter, Map.of("query_by", EMBEDDING_FIELD_NAME)); + MultiSearchResult result = this.client.multiSearch.perform(multiSearchesParameter, + Map.of("query_by", EMBEDDING_FIELD_NAME)); List documents = result.getResults() - .stream() - .flatMap(searchResult -> - searchResult.getHits() - .stream() - .map(hit -> { - Map rawDocument = hit.getDocument(); - String docId = rawDocument.get(DOC_ID_FIELD_NAME).toString(); - String content = rawDocument.get(CONTENT_FIELD_NAME).toString(); - Map metadata = rawDocument.get(METADATA_FIELD_NAME) instanceof Map ? (Map) rawDocument.get(METADATA_FIELD_NAME) : Map.of(); - return new Document(docId, content, metadata); - }) - ) - .toList(); + .stream() + .flatMap(searchResult -> searchResult.getHits().stream().map(hit -> { + Map rawDocument = hit.getDocument(); + String docId = rawDocument.get(DOC_ID_FIELD_NAME).toString(); + String content = rawDocument.get(CONTENT_FIELD_NAME).toString(); + Map metadata = rawDocument.get(METADATA_FIELD_NAME) instanceof Map + ? (Map) rawDocument.get(METADATA_FIELD_NAME) : Map.of(); + metadata.put("distance", hit.getVectorDistance()); + return new Document(docId, content, metadata); + })) + .toList(); logger.info("Found {} documents", documents.size()); - logger.info("Docs: \n {}", result); return documents; } catch (Exception e) { @@ -296,7 +295,8 @@ void dropCollection() { Map getCollectionInfo() { try { CollectionResponse retrievedCollection = this.client.collections(this.config.collectionName).retrieve(); - return Map.of("name", retrievedCollection.getName(), "num_documents", retrievedCollection.getNumDocuments()); + return Map.of("name", retrievedCollection.getName(), "num_documents", + retrievedCollection.getNumDocuments()); } catch (Exception e) { logger.error("Failed to retrieve collection info", e); From 3e1b1bb1d9aa8b9589e4f803d615e49553448bef Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Mon, 13 May 2024 09:44:32 +0200 Subject: [PATCH 20/35] feat: add typesesne starter --- .../spring-ai-starter-typesense/pom.xml | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 spring-ai-spring-boot-starters/spring-ai-starter-typesense/pom.xml diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-typesense/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-typesense/pom.xml new file mode 100644 index 0000000000..190ad181fc --- /dev/null +++ b/spring-ai-spring-boot-starters/spring-ai-starter-typesense/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + org.springframework.ai + spring-ai + 1.0.0-SNAPSHOT + ../../pom.xml + + + spring-ai-starter-typesense + + + 21 + 21 + UTF-8 + + + \ No newline at end of file From 4d25b684e6d63de583a9ba4380ded0d1c6591c4e Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Mon, 13 May 2024 09:45:20 +0200 Subject: [PATCH 21/35] feat: add starter and autoconfigure imports --- pom.xml | 3 +- spring-ai-bom/pom.xml | 13 +++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../spring-ai-starter-typesense/pom.xml | 35 +++++++++++++++---- 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index b0865be28f..fecfb8f13d 100644 --- a/pom.xml +++ b/pom.xml @@ -75,7 +75,8 @@ spring-ai-spring-boot-starters/spring-ai-starter-watsonx-ai spring-ai-spring-boot-starters/spring-ai-starter-elasticsearch-store vector-stores/spring-ai-typesense - + spring-ai-spring-boot-starters/spring-ai-starter-typesense + VMware Inc. diff --git a/spring-ai-bom/pom.xml b/spring-ai-bom/pom.xml index d1c857f9da..367c5c8eae 100644 --- a/spring-ai-bom/pom.xml +++ b/spring-ai-bom/pom.xml @@ -302,6 +302,12 @@ ${project.version} + + org.springframework.ai + spring-ai-typesense + ${project.version} + + org.springframework.ai spring-ai-pinecone-store-spring-boot-starter @@ -367,12 +373,19 @@ spring-ai-mongodb-atlas-store-spring-boot-starter ${project.version} + org.springframework.ai spring-ai-anthropic-spring-boot-starter ${project.version} + + org.springframework.ai + spring-ai-typesense-spring-boot-starter + ${project.version} + + org.springframework.ai spring-ai-spring-boot-testcontainers diff --git a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index c2816002bc..e8b16446da 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -32,3 +32,4 @@ org.springframework.ai.autoconfigure.anthropic.AnthropicAutoConfiguration org.springframework.ai.autoconfigure.watsonxai.WatsonxAiAutoConfiguration org.springframework.ai.autoconfigure.vectorstore.elasticsearch.ElasticsearchVectorStoreAutoConfiguration org.springframework.ai.autoconfigure.vectorstore.cassandra.CassandraVectorStoreAutoConfiguration +org.springframework.ai.autoconfigure.vectorstore.typesense.TypesenseVectorStoreAutoConfiguration diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-typesense/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-typesense/pom.xml index 190ad181fc..53ad081d01 100644 --- a/spring-ai-spring-boot-starters/spring-ai-starter-typesense/pom.xml +++ b/spring-ai-spring-boot-starters/spring-ai-starter-typesense/pom.xml @@ -9,13 +9,36 @@ 1.0.0-SNAPSHOT ../../pom.xml + spring-ai-typesense-spring-boot-starter + jar + Spring AI Starter - Typesense + Spring AI Typesense Auto Configuration + https://github.com/spring-projects/spring-ai - spring-ai-starter-typesense + + https://github.com/spring-projects/spring-ai + git://github.com/spring-projects/spring-ai.git + git@github.com:spring-projects/spring-ai.git + - - 21 - 21 - UTF-8 - + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.ai + spring-ai-spring-boot-autoconfigure + ${project.parent.version} + + + + org.springframework.ai + spring-ai-typesense + ${project.parent.version} + + \ No newline at end of file From 8a32dd0ab89c5aa8ec04e84035156216fdb5f757 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Mon, 13 May 2024 10:31:22 +0200 Subject: [PATCH 22/35] fix: add signature and fix typos --- .../vectorstore/typesense/TypesenseConnectionDetails.java | 3 +++ .../typesense/TypesenseVectorStoreAutoConfiguration.java | 3 +++ .../springframework/ai/vectorstore/TypesenseVectorStore.java | 4 ++-- .../ai/vectorstore/TypesenseVectorStoreIT.java | 3 +++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseConnectionDetails.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseConnectionDetails.java index bdf03a2afe..7c342996ae 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseConnectionDetails.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseConnectionDetails.java @@ -2,6 +2,9 @@ import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +/** + * @author Pablo Sanchidrian Herrera + */ public interface TypesenseConnectionDetails extends ConnectionDetails { String getHost(); diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java index 50e423db2b..fb3eb606e7 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java @@ -7,6 +7,9 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +/** + * @author Pablo Sanchidrian Herrera + */ @AutoConfiguration @ConditionalOnClass({ EmbeddingClient.class }) @EnableConfigurationProperties({ TypesenseVectorStoreProperties.class }) diff --git a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java index cd01ae2b7f..9c07af7424 100644 --- a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java +++ b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java @@ -24,8 +24,8 @@ public class TypesenseVectorStore implements VectorStore, InitializingBean { private static final Logger logger = LoggerFactory.getLogger(TypesenseVectorStore.class); /** - * The name of the field that contains the document ID. It is mandatory to "id" as the - * field name because that is the name that typesense is going to look for. + * The name of the field that contains the document ID. It is mandatory to set "id" as + * the field name because that is the name that typesense is going to look for. */ public static final String DOC_ID_FIELD_NAME = "id"; diff --git a/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java b/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java index 57d2d6309c..b9ac96ebc4 100644 --- a/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java +++ b/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java @@ -30,6 +30,9 @@ import static org.assertj.core.api.Assertions.assertThat; +/** + * @author Pablo Sanchidrian Herrera + */ @Testcontainers public class TypesenseVectorStoreIT { From 43aed3206a0936ea75f5ed79512aa08e393f8426 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Mon, 13 May 2024 17:34:50 +0200 Subject: [PATCH 23/35] fix: use 'in' operator in tests --- .../springframework/ai/vectorstore/TypesenseVectorStoreIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java b/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java index b9ac96ebc4..29f894ab60 100644 --- a/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java +++ b/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java @@ -177,7 +177,7 @@ void searchWithFilters() { results = vectorStore.similaritySearch(SearchRequest.query("The World") .withTopK(5) .withSimilarityThresholdAll() - .withFilterExpression("country == 'BG'")); + .withFilterExpression("country in ['BG']")); assertThat(results).hasSize(2); assertThat(results.get(0).getId()).isIn(bgDocument.getId(), bgDocument2.getId()); From 322900b23f1d001eb1b61f89fa636b87df928fc5 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Mon, 13 May 2024 17:59:16 +0200 Subject: [PATCH 24/35] feat: add typesense docs --- .../ROOT/pages/api/vectordbs/typesense.adoc | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/typesense.adoc diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/typesense.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/typesense.adoc new file mode 100644 index 0000000000..e22f06b96c --- /dev/null +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/typesense.adoc @@ -0,0 +1,238 @@ += Typesense + +This section walks you through setting up `TypesenseVectorStore` to store document embeddings and perform similarity searches. + +link:https://typesense.org[Typesense] Typesense is an open source, typo tolerant search engine that is optimized for instant sub-50ms searches, while providing an intuitive developer experience. + +== Prerequisites + +1. A Typesense instance +- link:https://typesense.org/docs/guide/install-typesense.html[Typesense Cloud] (recommended) +- link:https://hub.docker.com/r/typesense/typesense/[Docker] image _typesense/typesense:latest_ + +2. `EmbeddingClient` instance to compute the document embeddings. Several options are available: +- If required, an API key for the xref:api/embeddings.adoc#available-implementations[EmbeddingClient] to generate the embeddings stored by the `TypesenseVectorStore`. + +== Auto-configuration + +Spring AI provides Spring Boot auto-configuration for the Typesense Vector Sore. +To enable it, add the following dependency to your project's Maven `pom.xml` file: + +[source, xml] +---- + + org.springframework.ai + spring-ai-typesense-spring-boot-starter + +---- + +or to your Gradle `build.gradle` build file. + +[source,groovy] +---- +dependencies { + implementation 'org.springframework.ai:spring-ai-typesense-spring-boot-starter' +} +---- + +TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file. + +TIP: Refer to the xref:getting-started.adoc#repositories[Repositories] section to add Milestone and/or Snapshot Repositories to your build file. + +Additionally, you will need a configured `EmbeddingClient` bean. Refer to the xref:api/embeddings.adoc#available-implementations[EmbeddingClient] section for more information. + +Here is an example of the needed bean: + +[source,java] +---- +@Bean +public EmbeddingClient embeddingClient() { + // Can be any other EmbeddingClient implementation. + return new OpenAiEmbeddingClient(new OpenAiApi(System.getenv("SPRING_AI_OPENAI_API_KEY"))); +} +---- + +To connect to Typesense you need to provide access details for your instance. +A simple configuration can either be provided via Spring Boot's _application.yml_, + +[source,yaml] +---- +spring: + ai: + vectorstore: + typesense: + protocl: http + host: localhost + port: 8108 + apiKey: xyz + +---- + +Please have a look at the list of xref:#_configuration_properties[configuration parameters] for the vector store to learn about the default values and configuration options. + +Now you can Auto-wire the Typesense Vector Store in your application and use it + +[source,java] +---- +@Autowired VectorStore vectorStore; + +// ... + +List documents = List.of( + new Document("Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!!", Map.of("meta1", "meta1")), + new Document("The World is Big and Salvation Lurks Around the Corner"), + new Document("You walk forward facing the past and you turn back toward the future.", Map.of("meta2", "meta2"))); + +// Add the documents to Typesense +vectorStore.add(documents); + +// Retrieve documents similar to a query +List results = vectorStore.similaritySearch(SearchRequest.query("Spring").withTopK(5)); +---- + +=== Configuration properties + +You can use the following properties in your Spring Boot configuration to customize the Typesense vector store. + +|=== +|Property| Description | Default value + +|`spring.ai.vectorstore.typesense.protocol`| HTTP Protocol | `http` +|`spring.ai.vectorstore.typesense.host`| Hostname | `localhost` +|`spring.ai.vectorstore.typesense.port`| Port | `8108` +|`spring.ai.vectorstore.typesense.apiKey`| ApiKey | `xyz` + +|=== + +== Metadata filtering + +You can leverage the generic, portable link:https://docs.spring.io/spring-ai/reference/api/vectordbs.html#_metadata_filters[metadata filters] with `TypesenseVectorStore` as well. + +For example, you can use either the text expression language: + +[source,java] +---- +vectorStore.similaritySearch( + SearchRequest + .query("The World") + .withTopK(TOP_K) + .withSimilarityThreshold(SIMILARITY_THRESHOLD) + .withFilterExpression("country in ['UK', 'NL'] && year >= 2020")); +---- + +or programmatically using the expression DSL: + +[source,java] +---- +FilterExpressionBuilder b = new FilterExpressionBuilder(); + +vectorStore.similaritySearch( + SearchRequest + .query("The World") + .withTopK(TOP_K) + .withSimilarityThreshold(SIMILARITY_THRESHOLD) + .withFilterExpression(b.and( + b.in("country", "UK", "NL"), + b.gte("year", 2020)).build())); +---- + +The portable filter expressions get automatically converted into link:https://typesense.org/docs/0.24.0/api/search.html#filter-parameters[Typesense Search Filters]. +For example, the following portable filter expression: + +[source,sql] +---- +country in ['UK', 'NL'] && year >= 2020 +---- + +is converted into Typesense filter: + +[source] +---- +country: ['UK', 'NL'] && year: >=2020 +---- + +== Manual configuration + +If you prefer not to use the auto-configuration, you can manually configure the Typesense Vector Store. +Add the Typesense Vector Store and Jedis dependencies + +[source,xml] +---- + + org.springframework.ai + spring-ai-typesense + +---- + +TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file. + +Then, create a `TypesenseVectorStore` bean in your Spring configuration: + +[source,java] +---- +@Bean +public VectorStore vectorStore(Client client, EmbeddingClient embeddingClient) { + + TypesenseVectorStoreConfig config = TypesenseVectorStoreConfig.builder() + .withCollectionName("test_vector_store") + .withEmbeddingDimension(embeddingClient.dimensions()) + .build(); + + return new TypesenseVectorStore(client, embeddingClient, config); +} + +@Bean +public Client typesenseClient() { + List nodes = new ArrayList<>(); + nodes + .add(new Node("http", typesenseContainer.getHost(), typesenseContainer.getMappedPort(8108).toString())); + + Configuration configuration = new Configuration(nodes, Duration.ofSeconds(5), "xyz"); + return new Client(configuration); +} +---- + +[NOTE] +==== +It is more convenient and preferred to create the `TypesenseVectorStore` as a Bean. +But if you decide to create it manually, then you must call the `TypesenseVectorStore#afterPropertiesSet()` after setting the properties and before using the client. +==== + + +Then in your main code, create some documents: + +[source,java] +---- +List documents = List.of( + new Document("Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!!", Map.of("country", "UK", "year", 2020)), + new Document("The World is Big and Salvation Lurks Around the Corner", Map.of()), + new Document("You walk forward facing the past and you turn back toward the future.", Map.of("country", "NL", "year", 2023))); +---- + +Now add the documents to your vector store: + + +[source,java] +---- +vectorStore.add(documents); +---- + +And finally, retrieve documents similar to a query: + +[source,java] +---- +List results = vectorStore.similaritySearch( + SearchRequest + .query("Spring") + .withTopK(5)); +---- + +If all goes well, you should retrieve the document containing the text "Spring AI rocks!!". + +[NOTE] +==== +If you are not retrieveing the documents in the expected order or the search results are not as expected, check the embedding model you are using. + +Embedding models can have a significant impact on the search results (i.e. make sure if your data is in Spanish to use a Spanish or multilingual embedding model). +==== + From 5e893eb635724328b3c32c1dad75e6b54f2f6ec8 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Thu, 16 May 2024 17:17:01 +0200 Subject: [PATCH 25/35] feat: add client properties in autoconfigure --- .../TypesenseServiceClientProperties.java | 51 +++++++++++++++++++ ...TypesenseVectorStoreAutoConfiguration.java | 42 +++++++++++++-- .../TypesenseVectorStoreProperties.java | 47 ++++++----------- 3 files changed, 104 insertions(+), 36 deletions(-) create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseServiceClientProperties.java diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseServiceClientProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseServiceClientProperties.java new file mode 100644 index 0000000000..54e40dcaac --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseServiceClientProperties.java @@ -0,0 +1,51 @@ +package org.springframework.ai.autoconfigure.vectorstore.typesense; + +public class TypesenseServiceClientProperties { + + public static final String CONFIG_PREFIX = "spring.ai.vectorstore.typesense.client"; + + private String protocol = "http"; + + private String host = "localhost"; + + private String port = "8108"; + + /** + * Typesense API key. This is the default api key when the user follows the Typesense + * quick start guide. + */ + private String apiKey = "xyz"; + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public String getPort() { + return port; + } + + public void setPort(String port) { + this.port = port; + } + + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java index fb3eb606e7..f7d828701d 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java @@ -1,11 +1,21 @@ package org.springframework.ai.autoconfigure.vectorstore.typesense; import org.springframework.ai.embedding.EmbeddingClient; +import org.springframework.ai.vectorstore.TypesenseVectorStore; +import org.springframework.ai.vectorstore.TypesenseVectorStore.TypesenseVectorStoreConfig; +import org.springframework.ai.vectorstore.VectorStore; 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; +import org.typesense.api.Client; +import org.typesense.api.Configuration; +import org.typesense.resources.Node; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; /** * @author Pablo Sanchidrian Herrera @@ -17,15 +27,39 @@ public class TypesenseVectorStoreAutoConfiguration { @Bean @ConditionalOnMissingBean(TypesenseConnectionDetails.class) - public PropertiesTypesenseConnectionDetails typesenseConnectionDetails(TypesenseVectorStoreProperties properties) { - return new PropertiesTypesenseConnectionDetails(properties); + TypesenseVectorStoreAutoConfiguration.PropertiesTypesenseConnectionDetails typesenseServiceClientConnectionDetails( + TypesenseServiceClientProperties properties) { + return new TypesenseVectorStoreAutoConfiguration.PropertiesTypesenseConnectionDetails(properties); + } + + @Bean + public VectorStore vectorStore(Client typesenseClient, EmbeddingClient embeddingClient, + TypesenseVectorStoreProperties properties) { + + TypesenseVectorStoreConfig config = TypesenseVectorStoreConfig.builder() + .withCollectionName(properties.getCollectionName()) + .withEmbeddingDimension(properties.getEmbeddingDimension()) + .build(); + + return new TypesenseVectorStore(typesenseClient, embeddingClient, config); + } + + @Bean + @ConditionalOnMissingBean + public Client typesenseClient(TypesenseServiceClientProperties clientProperties, + TypesenseConnectionDetails connectionDetails) { + List nodes = new ArrayList<>(); + nodes.add(new Node(clientProperties.getProtocol(), clientProperties.getHost(), clientProperties.getPort())); + + Configuration configuration = new Configuration(nodes, Duration.ofSeconds(5), clientProperties.getApiKey()); + return new Client(configuration); } private static class PropertiesTypesenseConnectionDetails implements TypesenseConnectionDetails { - private final TypesenseVectorStoreProperties properties; + private final TypesenseServiceClientProperties properties; - PropertiesTypesenseConnectionDetails(TypesenseVectorStoreProperties properties) { + PropertiesTypesenseConnectionDetails(TypesenseServiceClientProperties properties) { this.properties = properties; } diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreProperties.java index b97a7ba34a..006c071c43 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreProperties.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreProperties.java @@ -1,5 +1,6 @@ package org.springframework.ai.autoconfigure.vectorstore.typesense; +import org.springframework.ai.vectorstore.TypesenseVectorStore; import org.springframework.boot.context.properties.ConfigurationProperties; /** @@ -10,48 +11,30 @@ public class TypesenseVectorStoreProperties { public static final String CONFIG_PREFIX = "spring.ai.vectorstore.typesense"; - private String protocol = "http"; - - private String host = "localhost"; - - private String port = "8108"; - /** - * Typesense API key. This is the default api key when the user follows the Typesense - * quick start guide. + * Typesense collection name to store the vectors. */ - private String apiKey = "xyz"; - - public String getProtocol() { - return protocol; - } - - public void setProtocol(String protocol) { - this.protocol = protocol; - } - - public String getHost() { - return host; - } + private String collectionName = TypesenseVectorStore.DEFAULT_COLLECTION_NAME; - public void setHost(String host) { - this.host = host; - } + /** + * The dimension of the vectors to be stored in the Typesense collection. + */ + private int embeddingDimension = TypesenseVectorStore.OPENAI_EMBEDDING_DIMENSION_SIZE; - public String getPort() { - return port; + public String getCollectionName() { + return collectionName; } - public void setPort(String port) { - this.port = port; + public void setCollectionName(String collectionName) { + this.collectionName = collectionName; } - public String getApiKey() { - return apiKey; + public int getEmbeddingDimension() { + return embeddingDimension; } - public void setApiKey(String apiKey) { - this.apiKey = apiKey; + public void setEmbeddingDimension(int embeddingDimension) { + this.embeddingDimension = embeddingDimension; } } From 60a5d40c4aaf8f60fb671390413c128b10e5c4a4 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Thu, 16 May 2024 17:20:52 +0200 Subject: [PATCH 26/35] feat:add typesense dependency --- spring-ai-spring-boot-autoconfigure/pom.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spring-ai-spring-boot-autoconfigure/pom.xml b/spring-ai-spring-boot-autoconfigure/pom.xml index ee5a26bc45..ad6b3f97d7 100644 --- a/spring-ai-spring-boot-autoconfigure/pom.xml +++ b/spring-ai-spring-boot-autoconfigure/pom.xml @@ -267,6 +267,14 @@ true + + + org.springframework.ai + spring-ai-typesense + ${project.parent.version} + true + + From a523d83947ed01601c50ef88ea6e4245ed0274e9 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Thu, 16 May 2024 17:23:09 +0200 Subject: [PATCH 27/35] feat: add embedding dimension method --- .../ai/vectorstore/TypesenseVectorStore.java | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java index 9c07af7424..52dd7f1361 100644 --- a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java +++ b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java @@ -35,6 +35,12 @@ public class TypesenseVectorStore implements VectorStore, InitializingBean { public static final String EMBEDDING_FIELD_NAME = "embedding"; + public static final int OPENAI_EMBEDDING_DIMENSION_SIZE = 1536; + + public static final String DEFAULT_COLLECTION_NAME = "default_collection"; + + public static final int INVALID_EMBEDDING_DIMENSION = -1; + private final Client client; private final EmbeddingClient embeddingClient; @@ -231,10 +237,26 @@ public List similaritySearch(SearchRequest request) { } } + int embeddingDimensions() { + if (this.config.embeddingDimension != INVALID_EMBEDDING_DIMENSION) { + return this.config.embeddingDimension; + } + try { + int embeddingDimensions = this.embeddingClient.dimensions(); + if (embeddingDimensions > 0) { + return embeddingDimensions; + } + } + catch (Exception e) { + logger.warn("Failed to obtain the embedding dimensions from the embedding client and fall backs to default:" + + this.config.embeddingDimension, e); + } + return OPENAI_EMBEDDING_DIMENSION_SIZE; + } + // --------------------------------------------------------------------------------- // Initialization // --------------------------------------------------------------------------------- - @Override public void afterPropertiesSet() throws Exception { this.createCollection(); @@ -264,7 +286,7 @@ void createCollection() { .addFieldsItem(new Field().name(METADATA_FIELD_NAME).type(FieldTypes.OBJECT).optional(true)) .addFieldsItem(new Field().name(EMBEDDING_FIELD_NAME) .type(FieldTypes.FLOAT_ARRAY) - .numDim(this.embeddingClient.dimensions()) + .numDim(this.embeddingDimensions()) .optional(false)) .enableNestedFields(true); From 13348c97493fdcf9f3bb0f5959925aebdbf82c20 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Thu, 16 May 2024 17:26:54 +0200 Subject: [PATCH 28/35] fix: remove unused import --- .../org/springframework/ai/vectorstore/TypesenseVectorStore.java | 1 - 1 file changed, 1 deletion(-) diff --git a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java index 52dd7f1361..64f5de2343 100644 --- a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java +++ b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java @@ -1,6 +1,5 @@ package org.springframework.ai.vectorstore; -import com.fasterxml.jackson.databind.util.JSONPObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.document.Document; From 203949d6fae86430db0d4986787e1c795d81ee23 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Thu, 16 May 2024 17:32:53 +0200 Subject: [PATCH 29/35] fix: change default collection name --- .../springframework/ai/vectorstore/TypesenseVectorStore.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java index 64f5de2343..eb5c43ffc8 100644 --- a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java +++ b/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java @@ -13,7 +13,6 @@ import org.typesense.model.*; import java.util.*; -import java.util.stream.Collectors; /** * @author Pablo Sanchidrian Herrera @@ -36,7 +35,7 @@ public class TypesenseVectorStore implements VectorStore, InitializingBean { public static final int OPENAI_EMBEDDING_DIMENSION_SIZE = 1536; - public static final String DEFAULT_COLLECTION_NAME = "default_collection"; + public static final String DEFAULT_COLLECTION_NAME = "vector_store"; public static final int INVALID_EMBEDDING_DIMENSION = -1; From 0fc122bbe725a53ab36091019518c8d393ec51a0 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Thu, 16 May 2024 17:33:03 +0200 Subject: [PATCH 30/35] feat: update docs --- .../ROOT/pages/api/vectordbs/typesense.adoc | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/typesense.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/typesense.adoc index e22f06b96c..e48aaa975c 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/typesense.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/typesense.adoc @@ -61,11 +61,13 @@ spring: ai: vectorstore: typesense: - protocl: http - host: localhost - port: 8108 - apiKey: xyz - + collectionName: "vector_store" + embeddingDimension: 1536 + client: + protocl: http + host: localhost + port: 8108 + apiKey: xyz ---- Please have a look at the list of xref:#_configuration_properties[configuration parameters] for the vector store to learn about the default values and configuration options. @@ -97,10 +99,12 @@ You can use the following properties in your Spring Boot configuration to custom |=== |Property| Description | Default value -|`spring.ai.vectorstore.typesense.protocol`| HTTP Protocol | `http` -|`spring.ai.vectorstore.typesense.host`| Hostname | `localhost` -|`spring.ai.vectorstore.typesense.port`| Port | `8108` -|`spring.ai.vectorstore.typesense.apiKey`| ApiKey | `xyz` +|`spring.ai.vectorstore.typesense.client.protocol`| HTTP Protocol | `http` +|`spring.ai.vectorstore.typesense.client.host`| Hostname | `localhost` +|`spring.ai.vectorstore.typesense.client.port`| Port | `8108` +|`spring.ai.vectorstore.typesense.client.apiKey`| ApiKey | `xyz` +|`spring.ai.vectorstore.typesense.collectionName`| Collection Name | `vector_store` +|`spring.ai.vectorstore.typesense.embeddingDimension`| Embedding Dimension | `1536` |=== From ba41f405ac7df43d0d0b29381dd952e1c4708269 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Thu, 16 May 2024 18:22:50 +0200 Subject: [PATCH 31/35] fix: error with service client bean, missing configuration annotation --- .../typesense/TypesenseServiceClientProperties.java | 6 ++++++ .../typesense/TypesenseVectorStoreAutoConfiguration.java | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseServiceClientProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseServiceClientProperties.java index 54e40dcaac..2ede36c66a 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseServiceClientProperties.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseServiceClientProperties.java @@ -1,5 +1,11 @@ package org.springframework.ai.autoconfigure.vectorstore.typesense; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Pablo Sanchidrian Herrera + */ +@ConfigurationProperties(TypesenseServiceClientProperties.CONFIG_PREFIX) public class TypesenseServiceClientProperties { public static final String CONFIG_PREFIX = "spring.ai.vectorstore.typesense.client"; diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java index f7d828701d..75ccf682cc 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java @@ -21,8 +21,8 @@ * @author Pablo Sanchidrian Herrera */ @AutoConfiguration -@ConditionalOnClass({ EmbeddingClient.class }) -@EnableConfigurationProperties({ TypesenseVectorStoreProperties.class }) +@ConditionalOnClass({ TypesenseVectorStore.class, EmbeddingClient.class }) +@EnableConfigurationProperties({ TypesenseServiceClientProperties.class, TypesenseVectorStoreProperties.class }) public class TypesenseVectorStoreAutoConfiguration { @Bean @@ -33,6 +33,7 @@ TypesenseVectorStoreAutoConfiguration.PropertiesTypesenseConnectionDetails types } @Bean + @ConditionalOnMissingBean public VectorStore vectorStore(Client typesenseClient, EmbeddingClient embeddingClient, TypesenseVectorStoreProperties properties) { From 05200f8aab4ff16e33b16c039533c2b56205fb48 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Tue, 21 May 2024 15:41:39 +0200 Subject: [PATCH 32/35] feat: add typesense vector store autoconfiguration tests --- ...pesenseVectorStoreAutoConfigurationIT.java | 109 ++++++++++++++++++ .../pom.xml | 2 +- .../ai/vectorstore/TypesenseVectorStore.java | 0 .../vectorstore/TypesenseVectorStoreIT.java | 0 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfigurationIT.java rename vector-stores/{spring-ai-typesense => spring-ai-typesense-store}/pom.xml (97%) rename vector-stores/{spring-ai-typesense => spring-ai-typesense-store}/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java (100%) rename vector-stores/{spring-ai-typesense => spring-ai-typesense-store}/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java (100%) diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfigurationIT.java new file mode 100644 index 0000000000..0a558d31f4 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfigurationIT.java @@ -0,0 +1,109 @@ +package org.springframework.ai.autoconfigure.vectorstore.typesense; + +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.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.TypesenseVectorStore; +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.springframework.util.FileSystemUtils; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.io.File; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Pablo Sanchidrian Herrera + */ +@Testcontainers +public class TypesenseVectorStoreAutoConfigurationIT { + + private static GenericContainer typesenseContainer; + + private static final File TEMP_FOLDER = new File("target/test-" + UUID.randomUUID().toString()); + + 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"))); + + @BeforeAll + public static void beforeAll() { + FileSystemUtils.deleteRecursively(TEMP_FOLDER); + TEMP_FOLDER.mkdirs(); + + typesenseContainer = new GenericContainer<>("typesense/typesense:26.0").withExposedPorts(8108) + .withCommand("--data-dir", "/data", "--api-key=xyz", "--enable-cors") + .withFileSystemBind(TEMP_FOLDER.getAbsolutePath(), "/data", BindMode.READ_WRITE) + .withStartupTimeout(Duration.ofSeconds(100)); + + typesenseContainer.start(); + } + + @AfterAll + public static void afterAll() { + typesenseContainer.stop(); + FileSystemUtils.deleteRecursively(TEMP_FOLDER); + } + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(TypesenseVectorStoreAutoConfiguration.class)) + .withUserConfiguration(Config.class); + + @Test + public void addAndSearch() { + contextRunner + .withPropertyValues("spring.ai.vectorstore.typesense.embeddingDimension=384", + "spring.ai.vectorstore.typesense.collectionName=myTestCollection", + "spring.ai.vectorstore.typesense.client.apiKey=xyz", + "spring.ai.vectorstore.typesense.client.protocol=http", + "spring.ai.vectorstore.typesense.client.host=" + typesenseContainer.getHost(), + "spring.ai.vectorstore.typesense.client.port=" + typesenseContainer.getMappedPort(8108).toString()) + .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."); + assertThat(resultDoc.getMetadata()).hasSize(2); + assertThat(resultDoc.getMetadata()).containsKeys("spring", "distance"); + + vectorStore.delete(documents.stream().map(doc -> doc.getId()).toList()); + + results = vectorStore.similaritySearch(SearchRequest.query("Spring").withTopK(1)); + assertThat(results).hasSize(0); + }); + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + public EmbeddingClient embeddingClient() { + return new TransformersEmbeddingClient(); + } + + } + +} diff --git a/vector-stores/spring-ai-typesense/pom.xml b/vector-stores/spring-ai-typesense-store/pom.xml similarity index 97% rename from vector-stores/spring-ai-typesense/pom.xml rename to vector-stores/spring-ai-typesense-store/pom.xml index ef31939216..df2484c19f 100644 --- a/vector-stores/spring-ai-typesense/pom.xml +++ b/vector-stores/spring-ai-typesense-store/pom.xml @@ -10,7 +10,7 @@ ../../pom.xml - spring-ai-typesense + spring-ai-typesense-store jar Spring AI Typesense Vector Store Spring AI Typesense Vector Store diff --git a/vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java b/vector-stores/spring-ai-typesense-store/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java similarity index 100% rename from vector-stores/spring-ai-typesense/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java rename to vector-stores/spring-ai-typesense-store/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java diff --git a/vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java b/vector-stores/spring-ai-typesense-store/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java similarity index 100% rename from vector-stores/spring-ai-typesense/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java rename to vector-stores/spring-ai-typesense-store/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java From d1052473bb26c4a47ab7ada233eeeec3e89a3c71 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Tue, 21 May 2024 15:42:08 +0200 Subject: [PATCH 33/35] fix: remove unused imports --- .../typesense/TypesenseVectorStoreAutoConfigurationIT.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfigurationIT.java index 0a558d31f4..fb799be1a4 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfigurationIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfigurationIT.java @@ -4,12 +4,10 @@ import org.junit.jupiter.api.BeforeAll; 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.TypesenseVectorStore; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; From 7181fa7f31b8a2b370b85044f4a942a37d4f2492 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Tue, 21 May 2024 15:48:46 +0200 Subject: [PATCH 34/35] feat: renaming typesense to typesense-store --- pom.xml | 2 +- spring-ai-bom/pom.xml | 4 ++-- spring-ai-spring-boot-autoconfigure/pom.xml | 2 +- .../spring-ai-starter-typesense/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 64ebb77c9a..af19651408 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ vector-stores/spring-ai-elasticsearch-store spring-ai-spring-boot-starters/spring-ai-starter-watsonx-ai spring-ai-spring-boot-starters/spring-ai-starter-elasticsearch-store - vector-stores/spring-ai-typesense + vector-stores/spring-ai-typesense-store spring-ai-spring-boot-starters/spring-ai-starter-typesense diff --git a/spring-ai-bom/pom.xml b/spring-ai-bom/pom.xml index 367c5c8eae..b388a42bf3 100644 --- a/spring-ai-bom/pom.xml +++ b/spring-ai-bom/pom.xml @@ -304,7 +304,7 @@ org.springframework.ai - spring-ai-typesense + spring-ai-typesense-store ${project.version} @@ -382,7 +382,7 @@ org.springframework.ai - spring-ai-typesense-spring-boot-starter + spring-ai-typesense-store-spring-boot-starter ${project.version} diff --git a/spring-ai-spring-boot-autoconfigure/pom.xml b/spring-ai-spring-boot-autoconfigure/pom.xml index ad6b3f97d7..10453186ce 100644 --- a/spring-ai-spring-boot-autoconfigure/pom.xml +++ b/spring-ai-spring-boot-autoconfigure/pom.xml @@ -270,7 +270,7 @@ org.springframework.ai - spring-ai-typesense + spring-ai-typesense-store ${project.parent.version} true diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-typesense/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-typesense/pom.xml index 53ad081d01..de5170aa8c 100644 --- a/spring-ai-spring-boot-starters/spring-ai-starter-typesense/pom.xml +++ b/spring-ai-spring-boot-starters/spring-ai-starter-typesense/pom.xml @@ -9,7 +9,7 @@ 1.0.0-SNAPSHOT ../../pom.xml - spring-ai-typesense-spring-boot-starter + spring-ai-typesense-store-spring-boot-starter jar Spring AI Starter - Typesense Spring AI Typesense Auto Configuration @@ -36,7 +36,7 @@ org.springframework.ai - spring-ai-typesense + spring-ai-typesense-store ${project.parent.version} From c146ee46b117a19f37a63a0a310ae3289537debf Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Wed, 22 May 2024 22:59:27 +0200 Subject: [PATCH 35/35] fix: refactor client to model --- pom.xml | 9 ++------- .../TypesenseVectorStoreAutoConfiguration.java | 6 +++--- .../TypesenseVectorStoreAutoConfigurationIT.java | 8 ++++---- .../ai/vectorstore/TypesenseVectorStore.java | 8 ++++---- .../ai/vectorstore/TypesenseVectorStoreIT.java | 10 +++++----- 5 files changed, 18 insertions(+), 23 deletions(-) diff --git a/pom.xml b/pom.xml index b04ed166d8..3354379f88 100644 --- a/pom.xml +++ b/pom.xml @@ -38,15 +38,10 @@ vector-stores/spring-ai-qdrant-store vector-stores/spring-ai-redis-store vector-stores/spring-ai-weaviate-store - spring-ai-spring-boot-starters/spring-ai-starter-azure-store spring-ai-spring-boot-starters/spring-ai-starter-cassandra-store spring-ai-spring-boot-starters/spring-ai-starter-chroma-store spring-ai-spring-boot-starters/spring-ai-starter-elasticsearch-store - - vector-stores/spring-ai-typesense-store - spring-ai-spring-boot-starters/spring-ai-starter-typesense - spring-ai-spring-boot-starters/spring-ai-starter-hanadb-store spring-ai-spring-boot-starters/spring-ai-starter-milvus-store spring-ai-spring-boot-starters/spring-ai-starter-mongodb-atlas-store @@ -56,7 +51,6 @@ spring-ai-spring-boot-starters/spring-ai-starter-qdrant-store spring-ai-spring-boot-starters/spring-ai-starter-redis-store spring-ai-spring-boot-starters/spring-ai-starter-weaviate-store - models/spring-ai-anthropic models/spring-ai-azure-openai models/spring-ai-bedrock @@ -72,7 +66,7 @@ models/spring-ai-vertex-ai-palm2 models/spring-ai-watsonx-ai models/spring-ai-zhipuai - + vector-stores/spring-ai-typesense-store spring-ai-spring-boot-starters/spring-ai-starter-anthropic spring-ai-spring-boot-starters/spring-ai-starter-azure-openai spring-ai-spring-boot-starters/spring-ai-starter-bedrock-ai @@ -87,6 +81,7 @@ spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-palm2 spring-ai-spring-boot-starters/spring-ai-starter-watsonx-ai spring-ai-spring-boot-starters/spring-ai-starter-zhipuai + spring-ai-spring-boot-starters/spring-ai-starter-typesense diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java index 75ccf682cc..f16a04df8b 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfiguration.java @@ -1,6 +1,6 @@ package org.springframework.ai.autoconfigure.vectorstore.typesense; -import org.springframework.ai.embedding.EmbeddingClient; +import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.vectorstore.TypesenseVectorStore; import org.springframework.ai.vectorstore.TypesenseVectorStore.TypesenseVectorStoreConfig; import org.springframework.ai.vectorstore.VectorStore; @@ -21,7 +21,7 @@ * @author Pablo Sanchidrian Herrera */ @AutoConfiguration -@ConditionalOnClass({ TypesenseVectorStore.class, EmbeddingClient.class }) +@ConditionalOnClass({ TypesenseVectorStore.class, EmbeddingModel.class }) @EnableConfigurationProperties({ TypesenseServiceClientProperties.class, TypesenseVectorStoreProperties.class }) public class TypesenseVectorStoreAutoConfiguration { @@ -34,7 +34,7 @@ TypesenseVectorStoreAutoConfiguration.PropertiesTypesenseConnectionDetails types @Bean @ConditionalOnMissingBean - public VectorStore vectorStore(Client typesenseClient, EmbeddingClient embeddingClient, + public VectorStore vectorStore(Client typesenseClient, EmbeddingModel embeddingClient, TypesenseVectorStoreProperties properties) { TypesenseVectorStoreConfig config = TypesenseVectorStoreConfig.builder() diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfigurationIT.java index fb799be1a4..b6c937f963 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfigurationIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/typesense/TypesenseVectorStoreAutoConfigurationIT.java @@ -5,8 +5,8 @@ import org.junit.jupiter.api.Test; import org.springframework.ai.ResourceUtils; import org.springframework.ai.document.Document; -import org.springframework.ai.embedding.EmbeddingClient; -import org.springframework.ai.transformers.TransformersEmbeddingClient; +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; @@ -98,8 +98,8 @@ public void addAndSearch() { static class Config { @Bean - public EmbeddingClient embeddingClient() { - return new TransformersEmbeddingClient(); + public EmbeddingModel embeddingClient() { + return new TransformersEmbeddingModel(); } } diff --git a/vector-stores/spring-ai-typesense-store/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java b/vector-stores/spring-ai-typesense-store/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java index eb5c43ffc8..d5e47016e4 100644 --- a/vector-stores/spring-ai-typesense-store/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java +++ b/vector-stores/spring-ai-typesense-store/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java @@ -3,7 +3,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.document.Document; -import org.springframework.ai.embedding.EmbeddingClient; +import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.vectorstore.filter.FilterExpressionConverter; import org.springframework.ai.vectorstore.filter.converter.TypesenseFilterExpressionConverter; import org.springframework.beans.factory.InitializingBean; @@ -41,7 +41,7 @@ public class TypesenseVectorStore implements VectorStore, InitializingBean { private final Client client; - private final EmbeddingClient embeddingClient; + private final EmbeddingModel embeddingClient; private final TypesenseVectorStoreConfig config; @@ -117,11 +117,11 @@ public TypesenseVectorStoreConfig build() { } - public TypesenseVectorStore(Client client, EmbeddingClient embeddingClient) { + public TypesenseVectorStore(Client client, EmbeddingModel embeddingClient) { this(client, embeddingClient, TypesenseVectorStoreConfig.defaultConfig()); } - public TypesenseVectorStore(Client client, EmbeddingClient embeddingClient, TypesenseVectorStoreConfig config) { + public TypesenseVectorStore(Client client, EmbeddingModel embeddingClient, TypesenseVectorStoreConfig config) { Assert.notNull(client, "Typesense must not be null"); Assert.notNull(embeddingClient, "EmbeddingClient must not be null"); diff --git a/vector-stores/spring-ai-typesense-store/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java b/vector-stores/spring-ai-typesense-store/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java index 29f894ab60..952458ea68 100644 --- a/vector-stores/spring-ai-typesense-store/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java +++ b/vector-stores/spring-ai-typesense-store/src/test/java/org/springframework/ai/vectorstore/TypesenseVectorStoreIT.java @@ -3,8 +3,8 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import org.springframework.ai.document.Document; -import org.springframework.ai.embedding.EmbeddingClient; -import org.springframework.ai.transformers.TransformersEmbeddingClient; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.transformers.TransformersEmbeddingModel; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; @@ -241,7 +241,7 @@ void searchWithThreshold() { public static class TestApplication { @Bean - public VectorStore vectorStore(Client client, EmbeddingClient embeddingClient) { + public VectorStore vectorStore(Client client, EmbeddingModel embeddingClient) { TypesenseVectorStoreConfig config = TypesenseVectorStoreConfig.builder() .withCollectionName("test_vector_store") @@ -262,8 +262,8 @@ public Client typesenseClient() { } @Bean - public EmbeddingClient embeddingClient() { - return new TransformersEmbeddingClient(); + public EmbeddingModel embeddingClient() { + return new TransformersEmbeddingModel(); } }