diff --git a/pom.xml b/pom.xml index 56004fd6519..7c0d9c0dbf0 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,7 @@ spring-ai-spring-boot-starters/spring-ai-starter-chroma-store spring-ai-spring-boot-starters/spring-ai-starter-milvus-store spring-ai-spring-boot-starters/spring-ai-starter-pgvector-store + spring-ai-spring-boot-starters/spring-ai-starter-hanadb-store spring-ai-spring-boot-starters/spring-ai-starter-pinecone-store spring-ai-spring-boot-starters/spring-ai-starter-azure-store spring-ai-spring-boot-starters/spring-ai-starter-weaviate-store @@ -47,6 +48,7 @@ spring-ai-spring-boot-starters/spring-ai-starter-postgresml-embedding spring-ai-docs vector-stores/spring-ai-pgvector-store + vector-stores/spring-ai-hanadb-store vector-stores/spring-ai-milvus-store vector-stores/spring-ai-neo4j-store document-readers/pdf-reader @@ -138,6 +140,7 @@ 3.0.1 0.1.4 + 2.20.11 42.7.2 2.3.4 0.8.0 diff --git a/spring-ai-bom/pom.xml b/spring-ai-bom/pom.xml index bc1ebc3aa2b..1bc27de0c36 100644 --- a/spring-ai-bom/pom.xml +++ b/spring-ai-bom/pom.xml @@ -156,6 +156,12 @@ ${project.version} + + org.springframework.ai + spring-ai-hanadb-store + ${project.version} + + org.springframework.ai spring-ai-pinecone @@ -272,6 +278,12 @@ ${project.version} + + org.springframework.ai + spring-ai-hanadb-store-spring-boot-starter + ${project.version} + + org.springframework.ai spring-ai-watsonx-ai diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/0.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/0.png new file mode 100644 index 00000000000..d380f45a9dd Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/0.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/1.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/1.png new file mode 100644 index 00000000000..cb4ab3516e6 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/1.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/10.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/10.png new file mode 100644 index 00000000000..e2019651ef2 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/10.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/11.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/11.png new file mode 100644 index 00000000000..ecde0d100a2 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/11.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/13.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/13.png new file mode 100644 index 00000000000..1f24fe2f55c Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/13.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/14.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/14.png new file mode 100644 index 00000000000..86353f1ff26 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/14.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/15.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/15.png new file mode 100644 index 00000000000..32a747dd8fe Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/15.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/16.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/16.png new file mode 100644 index 00000000000..2365e229fc7 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/16.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/17.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/17.png new file mode 100644 index 00000000000..6ff6d0bf5af Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/17.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/18.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/18.png new file mode 100644 index 00000000000..af28b7f1508 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/18.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/19.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/19.png new file mode 100644 index 00000000000..4e23648aec1 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/19.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/2.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/2.png new file mode 100644 index 00000000000..3acb0e37127 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/2.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/20.1.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/20.1.png new file mode 100644 index 00000000000..dd1a9d723c1 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/20.1.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/20.2.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/20.2.png new file mode 100644 index 00000000000..8794363da14 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/20.2.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/21.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/21.png new file mode 100644 index 00000000000..97c059c4d62 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/21.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/22.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/22.png new file mode 100644 index 00000000000..2952a0d8718 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/22.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/23.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/23.png new file mode 100644 index 00000000000..acbba62978c Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/23.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/24.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/24.png new file mode 100644 index 00000000000..1967dfb7e93 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/24.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/25.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/25.png new file mode 100644 index 00000000000..cee49c78e39 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/25.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/26.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/26.png new file mode 100644 index 00000000000..a87889a2be9 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/26.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/27.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/27.png new file mode 100644 index 00000000000..e46b0ad2add Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/27.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/28.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/28.png new file mode 100644 index 00000000000..13c104c5e36 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/28.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/29.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/29.png new file mode 100644 index 00000000000..8a655e0c4ce Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/29.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/3.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/3.png new file mode 100644 index 00000000000..97e27530cb6 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/3.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/30.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/30.png new file mode 100644 index 00000000000..68f1420e718 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/30.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/31.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/31.png new file mode 100644 index 00000000000..d6193e2ce9d Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/31.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/32.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/32.png new file mode 100644 index 00000000000..b1d4eb04bac Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/32.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/33.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/33.png new file mode 100644 index 00000000000..b41caa70d44 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/33.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/34.1.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/34.1.png new file mode 100644 index 00000000000..fc23b9ace9a Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/34.1.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/34.2.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/34.2.png new file mode 100644 index 00000000000..b9f9d94ef0a Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/34.2.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/35.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/35.png new file mode 100644 index 00000000000..8fc160fbcc2 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/35.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/36.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/36.png new file mode 100644 index 00000000000..01bd42d52f8 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/36.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/37.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/37.png new file mode 100644 index 00000000000..cc7c77315be Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/37.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/38.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/38.png new file mode 100644 index 00000000000..d7921f5982c Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/38.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/39.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/39.png new file mode 100644 index 00000000000..087fc90bf6a Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/39.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/4.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/4.png new file mode 100644 index 00000000000..edb7d2daa53 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/4.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/40.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/40.png new file mode 100644 index 00000000000..34214aef9a7 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/40.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/5.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/5.png new file mode 100644 index 00000000000..3ce821b519a Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/5.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/6.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/6.png new file mode 100644 index 00000000000..29c75552450 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/6.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/7.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/7.png new file mode 100644 index 00000000000..779b8fd44d2 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/7.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/8.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/8.png new file mode 100644 index 00000000000..faa2e2cf104 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/8.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/9.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/9.png new file mode 100644 index 00000000000..65fe65baea8 Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/9.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/wikipedia.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/wikipedia.png new file mode 100644 index 00000000000..fcd4c76ca0e Binary files /dev/null and b/spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/wikipedia.png differ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc index b8d6c396f61..d1650d19dfb 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc @@ -54,6 +54,7 @@ *** xref:api/vectordbs/pinecone.adoc[] *** xref:api/vectordbs/qdrant.adoc[] *** xref:api/vectordbs/gemfire.adoc[GemFire] +*** xref:api/vectordbs/hana.adoc[SAP Hana Vector Engine] ** xref:api/functions.adoc[Function Calling] ** xref:api/prompt.adoc[] ** xref:api/output-parser.adoc[] diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/hana.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/hana.adoc new file mode 100644 index 00000000000..6a41378fb75 --- /dev/null +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/hana.adoc @@ -0,0 +1,253 @@ += SAP HANA Cloud vector engine + +== How to setup a project that uses SAP Hana Cloud as the vector DB and leverage OpenAI to implement RAG pattern + +Refer xref:api/vectordbs/hanadb-provision-a-trial-account.adoc[SAP HANA Cloud vector engine - provision a trial account] guide +to create a trial account + +==== Create a table `CRICKET_WORLD_CUP` in SAP Hana DB: +[sql] +---- +CREATE TABLE CRICKET_WORLD_CUP ( + _ID VARCHAR2(255) PRIMARY KEY, + CONTENT CLOB, + EMBEDDING REAL_VECTOR(1536) +) +---- + +==== Add the following dependencies in your `pom.xml`. +You may set the property `spring-ai-version` as `1.0.0-SNAPSHOT`: +[source,xml] +---- + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.ai + spring-ai-pdf-document-reader + ${spring-ai-version} + + + + org.springframework.ai + spring-ai-openai-spring-boot-starter + ${spring-ai-version} + + + + org.springframework.ai + spring-ai-hanadb-store-spring-boot-starter + ${spring-ai-version} + + + + org.projectlombok + lombok + 1.18.30 + provided + +---- + +==== Add the following properties in `application.properties` file: +[yml] +---- +spring.ai.openai.api-key=${OPENAI_API_KEY} +spring.ai.openai.embedding.options.model=text-embedding-ada-002 + +spring.datasource.driver-class-name=com.sap.db.jdbc.Driver +spring.datasource.url=${HANA_DATASOURCE_URL} +spring.datasource.username=${HANA_DATASOURCE_USERNAME} +spring.datasource.password=${HANA_DATASOURCE_PASSWORD} + +spring.ai.vectorstore.hanadb.tableName=CRICKET_WORLD_CUP +spring.ai.vectorstore.hanadb.topK=3 +---- + +==== Create an `Entity` class named `CricketWorldCup` that extends from `HanaVectorEntity`: +[source,java] +---- +package com.interviewpedia.spring.ai.hana; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.jackson.Jacksonized; +import org.springframework.ai.vectorstore.HanaVectorEntity; + +@Entity +@Table(name = "CRICKET_WORLD_CUP") +@Data +@Jacksonized +@NoArgsConstructor +public class CricketWorldCup extends HanaVectorEntity { + @Column(name = "content") + private String content; +} + +---- + +==== Create a `Repository` named `CricketWorldCupRepository` that implements `HanaVectorRepository` interface: +[source,java] +---- +package com.interviewpedia.spring.ai.hana; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.transaction.Transactional; +import org.springframework.ai.vectorstore.HanaVectorRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public class CricketWorldCupRepository implements HanaVectorRepository { + @PersistenceContext + private EntityManager entityManager; + + @Override + @Transactional + public void save(String tableName, String id, String embedding, String content) { + String sql = String.format(""" + INSERT INTO %s (_ID, EMBEDDING, CONTENT) + VALUES(:_id, TO_REAL_VECTOR(:embedding), :content) + """, tableName); + + entityManager.createNativeQuery(sql) + .setParameter("_id", id) + .setParameter("embedding", embedding) + .setParameter("content", content) + .executeUpdate(); + } + + @Override + @Transactional + public int deleteEmbeddingsById(String tableName, List idList) { + String sql = String.format(""" + DELETE FROM %s WHERE _ID IN (:ids) + """, tableName); + + return entityManager.createNativeQuery(sql) + .setParameter("ids", idList) + .executeUpdate(); + } + + @Override + @Transactional + public int deleteAllEmbeddings(String tableName) { + String sql = String.format(""" + DELETE FROM %s + """, tableName); + + return entityManager.createNativeQuery(sql).executeUpdate(); + } + + @Override + public List cosineSimilaritySearch(String tableName, int topK, String queryEmbedding) { + String sql = String.format(""" + SELECT TOP :topK * FROM %s + ORDER BY COSINE_SIMILARITY(EMBEDDING, TO_REAL_VECTOR(:queryEmbedding)) DESC + """, tableName); + + return entityManager.createNativeQuery(sql, CricketWorldCup.class) + .setParameter("topK", topK) + .setParameter("queryEmbedding", queryEmbedding) + .getResultList(); + } +} +---- + +==== Now, create a REST Controller class `CricketWorldCupHanaController`, and autowire `ChatClient` and `VectorStore` as dependencies: +In this controller class, create the following REST endpoints: + +- `/ai/hana-vector-store/cricket-world-cup/purge-embeddings` - to purge all the embeddings from the Vector Store +- `/ai/hana-vector-store/cricket-world-cup/upload` - to upload the Cricket_World_Cup.pdf so that its data gets stored in SAP Hana Cloud Vector DB as embeddings +- `/ai/hana-vector-store/cricket-world-cup` - to implement `RAG` using link:https://help.sap.com/docs/hana-cloud-database/sap-hana-cloud-sap-hana-database-vector-engine-guide/vectors-vector-embeddings-and-metrics[Cosine_Similarity in SAP Hana DB] + +[source,java] +---- +package com.interviewpedia.spring.ai.hana; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.ChatClient; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.chat.prompt.SystemPromptTemplate; +import org.springframework.ai.document.Document; +import org.springframework.ai.reader.pdf.PagePdfDocumentReader; +import org.springframework.ai.transformer.splitter.TokenTextSplitter; +import org.springframework.ai.vectorstore.HanaCloudVectorStore; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +@RestController +@Slf4j +public class CricketWorldCupHanaController { + private final VectorStore hanaCloudVectorStore; + private final ChatClient chatClient; + + @Autowired + public CricketWorldCupHanaController(ChatClient chatClient, VectorStore hanaCloudVectorStore) { + this.chatClient = chatClient; + this.hanaCloudVectorStore = hanaCloudVectorStore; + } + + @PostMapping("/ai/hana-vector-store/cricket-world-cup/purge-embeddings") + public ResponseEntity purgeEmbeddings() { + int deleteCount = ((HanaCloudVectorStore) this.hanaCloudVectorStore).purgeEmbeddings(); + log.info("{} embeddings purged from CRICKET_WORLD_CUP table in Hana DB", deleteCount); + return ResponseEntity.ok().body(String.format("%d embeddings purged from CRICKET_WORLD_CUP table in Hana DB", deleteCount)); + } + + @PostMapping("/ai/hana-vector-store/cricket-world-cup/upload") + public ResponseEntity handleFileUpload(@RequestParam("pdf") MultipartFile file) throws IOException { + Resource pdf = file.getResource(); + Supplier> reader = new PagePdfDocumentReader(pdf); + Function, List> splitter = new TokenTextSplitter(); + List documents = splitter.apply(reader.get()); + log.info("{} documents created from pdf file: {}", documents.size(), pdf.getFilename()); + hanaCloudVectorStore.accept(documents); + return ResponseEntity.ok().body(String.format("%d documents created from pdf file: %s", + documents.size(), pdf.getFilename())); + } + + @GetMapping("/ai/hana-vector-store/cricket-world-cup") + public Map hanaVectorStoreSearch(@RequestParam(value = "message") String message) { + var documents = this.hanaCloudVectorStore.similaritySearch(message); + var inlined = documents.stream().map(Document::getContent).collect(Collectors.joining(System.lineSeparator())); + var similarDocsMessage = new SystemPromptTemplate("Based on the following: {documents}") + .createMessage(Map.of("documents", inlined)); + + var userMessage = new UserMessage(message); + Prompt prompt = new Prompt(List.of(similarDocsMessage, userMessage)); + String generation = chatClient.call(prompt).getResult().getOutput().getContent(); + log.info("Generation: {}", generation); + return Map.of("generation", generation); + } +} +---- + + +==== Use a `contextual` pdf file from wikipedia +Go to link:https://en.wikipedia.org/wiki/Cricket_World_Cup[wikipedia] and link:https://en.wikipedia.org/w/index.php?title=Special:DownloadAsPdf&page=Cricket_World_Cup&action=show-download-screen[download] `Cricket World Cup` page as a PDF file. + +image::../images/hanadb/wikipedia.png[width=800] + +Upload this PDF file using the file-upload REST endpoint that we created in the previous step. diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/hanadb-provision-a-trial-account.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/hanadb-provision-a-trial-account.adoc new file mode 100644 index 00000000000..c92a59b40a6 --- /dev/null +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/hanadb-provision-a-trial-account.adoc @@ -0,0 +1,186 @@ += SAP HANA Cloud vector engine - provision a trial account + +== Below are the steps to provision SAP Hana Database using a trial account + +Let's start with creating a link:https://temp-mail.org/en/[temporary email] for registration purposes + +image::../images/hanadb/0.png[width=800] + +TIP: Don't close the above window, otherwise a new email id would get generated. + +Go to link:https://sap.com/[sap.com] and navigate to `products` -> `Trials and Demos` + +image::../images/hanadb/1.png[width=800] + +Click `Advanced Trials` + +image::../images/hanadb/2.png[width=800] + +Click `SAP BTP Trial` + +image::../images/hanadb/3.png[width=800] + +Click `Start your free 90-day trial` + +image::../images/hanadb/4.png[width=800] + +Paste the `temporary email id` that we created in the first step, and click `Next` + +image::../images/hanadb/5.png[width=800] + +We fill in our details and click `Submit` + +image::../images/hanadb/6.png[width=800] + +It's time to check the inbox of our temporary email account + +image::../images/hanadb/7.png[width=800] + +Notice that there is an email received in our temporary email account + +image::../images/hanadb/8.png[width=800] + +Open the email and `click to activate` the trial account + +image::../images/hanadb/9.png[width=800] + +It will prompt to create a `password`. Provide a password and click `Submit` + +image::../images/hanadb/10.png[width=800] + +The trial account is now created. Click to `start the trial` + +image::../images/hanadb/11.png[width=800] + +Provide your phone number and click `Continue` + +image::../images/hanadb/13.png[width=800] + +We receive an OTP on the phone number. Provide the `code` and click `continue` + +image::../images/hanadb/14.png[width=800] + +Select the `region` as `US East (VA) - AWS` + +image::../images/hanadb/15.png[width=800] + +Click `Continue` + +image::../images/hanadb/16.png[width=800] + +The `SAP BTP trial` account is ready. Click `Go to your Trial account` + +image::../images/hanadb/17.png[width=800] + +Click the `Trial` sub-account + +image::../images/hanadb/18.png[width=800] + +Open `Instances and Subscriptions` + +image::../images/hanadb/19.png[width=800] + +It's time to create a subscription. Click the `Create` button + +image::../images/hanadb/20.1.png[width=800] + +While creating a subscription, Select `service` as `SAP Hana Cloud` and `Plan` as `tools` and click `Create` + +image::../images/hanadb/20.2.png[width=800] + +Notice that `SAP Hana Cloud` subscription is now created. Click `Users` on the left panel + +image::../images/hanadb/21.png[width=800] + +Select the username (temporary email that we supplied earlier) and click `Assign Role Collection` + +image::../images/hanadb/22.png[width=800] + +Search `hana` and select all the 3 role collections that gets displayed. Click `Assign Role Collection` + +image::../images/hanadb/23.png[width=800] + +Our `user` now has all the 3 role collections. Click `Instances and Subscriptions` + +image::../images/hanadb/24.png[width=800] + +Now, click `SAP Hana Cloud` application under subscriptions + +image::../images/hanadb/25.png[width=800] + +There are no instances yet. Let's click `Create Instance` + +image::../images/hanadb/26.png[width=800] + +Select Type as `SAP HANA Cloud, SAP HANA Database`. Click `Next Step` + +image::../images/hanadb/27.png[width=800] + +Provide `Instance Name`, `Description`, `password` for DBADMIN administrator. +Select the latest version `2024.2 (QRC 1/2024)`. Click `Next Step` + +image::../images/hanadb/28.png[width=800] + +Keep everything as default. Click `Next Step` + +image::../images/hanadb/29.png[width=800] + +Click `Next Step` + +image::../images/hanadb/30.png[width=800] + +Select `Allow all IP addresses` and click `Next Step` + +image::../images/hanadb/31.png[width=800] + +Click `Review and Create` + +image::../images/hanadb/32.png[width=800] + +Click `Create Instance` + +image::../images/hanadb/33.png[width=800] + +Notice that the provisioning of `SAP Hana Database` instance has started. It takes some time to provision - please be patient. + +image::../images/hanadb/34.1.png[width=800] + +Once the instance is provisioned (status is displayed as `Running`) we can get the datasource url (`SQL Endpoint`) by clicking the instance and selecting `Connections` + +image::../images/hanadb/34.2.png[width=800] + +We navigate to `SAP Hana Database Explorer` by click the `...` + +image::../images/hanadb/35.png[width=800] + +Provide the administrator credentials and click `OK` + +image::../images/hanadb/36.png[width=800] + +Open SQL console and create the table `CRICKET_WORLD_CUP` using the following DDL statement: +[sql] +---- +CREATE TABLE CRICKET_WORLD_CUP ( + _ID VARCHAR2(255) PRIMARY KEY, + CONTENT CLOB, + EMBEDDING REAL_VECTOR(1536) +) +---- + +image::../images/hanadb/37.png[width=800] + +Navigate to `hana_dev_db -> Catalog -> Tables` to find our table `CRICKET_WORLD_CUP` + +image::../images/hanadb/38.png[width=800] + +Right-click on the table and click `Open Data` + +image::../images/hanadb/39.png[width=800] + +Notice that the table data is now displayed. There are now rows as we didn't create any embeddings yet. + +image::../images/hanadb/40.png[width=800] + +Next steps: xref:api/vectordbs/hana.adoc[SAP Hana Vector Engine] + + diff --git a/spring-ai-spring-boot-autoconfigure/pom.xml b/spring-ai-spring-boot-autoconfigure/pom.xml index aceee800753..547d79b7956 100644 --- a/spring-ai-spring-boot-autoconfigure/pom.xml +++ b/spring-ai-spring-boot-autoconfigure/pom.xml @@ -109,6 +109,14 @@ true + + + org.springframework.ai + spring-ai-hanadb-store + ${project.parent.version} + true + + com.pgvector pgvector diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/hanadb/HanaCloudVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/hanadb/HanaCloudVectorStoreAutoConfiguration.java new file mode 100644 index 00000000000..7c56afb1d57 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/hanadb/HanaCloudVectorStoreAutoConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright 2023 - 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.autoconfigure.vectorstore.hanadb; + +import org.springframework.ai.embedding.EmbeddingClient; +import org.springframework.ai.vectorstore.HanaCloudVectorStore; +import org.springframework.ai.vectorstore.HanaCloudVectorStoreConfig; +import org.springframework.ai.vectorstore.HanaVectorEntity; +import org.springframework.ai.vectorstore.HanaVectorRepository; +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 javax.sql.DataSource; + +/** + * @author Rahul Mittal + * @since 1.0.0 + */ +@AutoConfiguration +@ConditionalOnClass({ HanaCloudVectorStore.class, DataSource.class, HanaVectorEntity.class }) +@EnableConfigurationProperties(HanaCloudVectorStoreProperties.class) +public class HanaCloudVectorStoreAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public HanaCloudVectorStore vectorStore(HanaVectorRepository repository, + EmbeddingClient embeddingClient, HanaCloudVectorStoreProperties properties) { + + return new HanaCloudVectorStore(repository, embeddingClient, + HanaCloudVectorStoreConfig.builder() + .tableName(properties.getTableName()) + .topK(properties.getTopK()) + .build()); + } + +} \ No newline at end of file diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/hanadb/HanaCloudVectorStoreProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/hanadb/HanaCloudVectorStoreProperties.java new file mode 100644 index 00000000000..79dfbfbc1af --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/hanadb/HanaCloudVectorStoreProperties.java @@ -0,0 +1,49 @@ +/* + * Copyright 2023 - 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.autoconfigure.vectorstore.hanadb; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Rahul Mittal + * @since 1.0.0 + */ +@ConfigurationProperties(HanaCloudVectorStoreProperties.CONFIG_PREFIX) +public class HanaCloudVectorStoreProperties { + + public static final String CONFIG_PREFIX = "spring.ai.vectorstore.hanadb"; + + private String tableName; + + private int topK; + + public String getTableName() { + return tableName; + } + + public void setTableName(String tableName) { + this.tableName = tableName; + } + + public int getTopK() { + return topK; + } + + public void setTopK(int topK) { + this.topK = topK; + } + +} 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 f614b7711ec..3e3fd3574fe 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 @@ -24,6 +24,7 @@ org.springframework.ai.autoconfigure.vectorstore.azure.AzureVectorStoreAutoConfi org.springframework.ai.autoconfigure.vectorstore.weaviate.WeaviateVectorStoreAutoConfiguration org.springframework.ai.autoconfigure.vectorstore.neo4j.Neo4jVectorStoreAutoConfiguration org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration +org.springframework.ai.autoconfigure.vectorstore.hanadb.HanaCloudVectorStoreAutoConfiguration org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration org.springframework.ai.autoconfigure.postgresml.PostgresMlAutoConfiguration org.springframework.ai.autoconfigure.vectorstore.mongo.MongoDBAtlasVectorStoreAutoConfiguration diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/hanadb/HanaCloudVectorStoreAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/hanadb/HanaCloudVectorStoreAutoConfigurationIT.java new file mode 100644 index 00000000000..36bc049bf37 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/hanadb/HanaCloudVectorStoreAutoConfigurationIT.java @@ -0,0 +1,75 @@ +/* + * Copyright 2023 - 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.autoconfigure.vectorstore.hanadb; + +import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration; +import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration; +import org.springframework.ai.document.Document; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration; +import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.List; + +@Testcontainers +@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") +@EnabledIfEnvironmentVariable(named = "HANA_DATASOURCE_URL", matches = ".+") +@EnabledIfEnvironmentVariable(named = "HANA_DATASOURCE_USERNAME", matches = ".+") +@EnabledIfEnvironmentVariable(named = "HANA_DATASOURCE_PASSWORD", matches = ".+") +@Disabled +public class HanaCloudVectorStoreAutoConfigurationIT { + + @Test + public void addAndSearch() { + contextRunner.run(context -> { + VectorStore vectorStore = context.getBean(VectorStore.class); + vectorStore.add(documents); + + List results = vectorStore.similaritySearch("What is Great Depression?"); + Assertions.assertEquals(1, results.size()); + + // Remove all documents from the store + vectorStore.delete(documents.stream().map(Document::getId).toList()); + List results2 = vectorStore.similaritySearch("Great Depression"); + Assertions.assertEquals(0, results2.size()); + }); + } + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(HanaCloudVectorStoreAutoConfiguration.class, + OpenAiAutoConfiguration.class, RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class, + JdbcRepositoriesAutoConfiguration.class)) + .withPropertyValues("spring.ai.openai.api-key=" + System.getenv("OPENAI_API_KEY"), + "spring.ai.openai.embedding.options.model=text-embedding-ada-002", + "spring.datasource.url=" + System.getenv("HANA_DATASOURCE_URL"), + "spring.datasource.username=" + System.getenv("HANA_DATASOURCE_USERNAME"), + "spring.datasource.password=" + System.getenv("HANA_DATASOURCE_PASSWORD"), + "spring.ai.vectorstore.hanadb.tableName=CRICKET_WORLD_CUP", "spring.ai.vectorstore.hanadb.topK=1"); + + List documents = List.of( + new Document("Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!!"), + new Document("Hello World Hello World Hello World Hello World Hello World Hello World Hello World"), + new Document( + "Great Depression Great Depression Great Depression Great Depression Great Depression Great Depression")); + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/hanadb/HanaCloudVectorStorePropertiesTest.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/hanadb/HanaCloudVectorStorePropertiesTest.java new file mode 100644 index 00000000000..1756e8cd7dc --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/hanadb/HanaCloudVectorStorePropertiesTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023 - 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.autoconfigure.vectorstore.hanadb; + +import org.junit.Test; +import org.junit.jupiter.api.Assertions; + +/** + * @author Rahul Mittal + */ +public class HanaCloudVectorStorePropertiesTest { + + @Test + public void testHanaCloudVectorStoreProperties() { + var props = new HanaCloudVectorStoreProperties(); + props.setTableName("CRICKET_WORLD_CUP"); + props.setTopK(5); + + Assertions.assertEquals("CRICKET_WORLD_CUP", props.getTableName()); + Assertions.assertEquals(5, props.getTopK()); + } + +} diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-hanadb-store/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-hanadb-store/pom.xml new file mode 100644 index 00000000000..bba6eddcef6 --- /dev/null +++ b/spring-ai-spring-boot-starters/spring-ai-starter-hanadb-store/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + org.springframework.ai + spring-ai + 1.0.0-SNAPSHOT + ../../pom.xml + + spring-ai-hanadb-store-spring-boot-starter + jar + Spring AI Starter - SAP Hana Cloud Vector Store + Spring AI SAP Hana Cloud Vector Store Auto Configuration + 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.boot + spring-boot-starter + + + + org.springframework.ai + spring-ai-spring-boot-autoconfigure + ${project.parent.version} + + + + org.springframework.ai + spring-ai-hanadb-store + ${project.parent.version} + + + + diff --git a/vector-stores/spring-ai-hanadb-store/README.md b/vector-stores/spring-ai-hanadb-store/README.md new file mode 100644 index 00000000000..a5f2443303a --- /dev/null +++ b/vector-stores/spring-ai-hanadb-store/README.md @@ -0,0 +1 @@ +[SAP Hana Cloud Vector Store Documentation](https://docs.spring.io/spring-ai/reference/api/vectordbs/hana.html) \ No newline at end of file diff --git a/vector-stores/spring-ai-hanadb-store/pom.xml b/vector-stores/spring-ai-hanadb-store/pom.xml new file mode 100644 index 00000000000..19b49e3a3c8 --- /dev/null +++ b/vector-stores/spring-ai-hanadb-store/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + org.springframework.ai + spring-ai + 1.0.0-SNAPSHOT + ../../pom.xml + + spring-ai-hanadb-store + jar + Spring AI Vector Store - HanaDB + Spring AI HanaDB 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.springframework.data + spring-data-jpa + + + + org.hibernate.orm + hibernate-core + + + + + com.sap.cloud.db.jdbc + ngdbc + ${sap.hanadb.version} + + + + + org.springframework.ai + spring-ai-openai + ${parent.version} + test + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.ai + spring-ai-test + ${parent.version} + test + + + + org.springframework.ai + spring-ai-pdf-document-reader + ${parent.version} + test + + + + org.testcontainers + junit-jupiter + test + + + diff --git a/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaCloudVectorStore.java b/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaCloudVectorStore.java new file mode 100644 index 00000000000..4f0415b7b74 --- /dev/null +++ b/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaCloudVectorStore.java @@ -0,0 +1,145 @@ +/* + * Copyright 2023 - 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.vectorstore; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.document.Document; +import org.springframework.ai.embedding.EmbeddingClient; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * The SAP HANA Cloud vector engine offers multiple use cases in AI scenarios. + * + * Recent advances in Generative AI (GenAI) and Large Language Models (LLM) have led to + * increased awareness of and popularity for vector databases. Similarity search, a key + * functionality of vector databases, complements traditional relational databases as well + * as full-text search systems. Using natural language text as an example, embedding + * functions map data to high dimensional vectors to preserve their semantic similarity. + * Developers can then use vector-based semantic search to find similarity between + * different passages of text. Because the data within an LLM is current only up to a + * specific point in time, vector databases can offer additional relevant text to make + * searches more accurate – known as Retrieval Augmented Generation (RAG). + * Therefore, the addition of RAG to an LLM using a vector database like SAP HANA Cloud + * provides an effective approach to increase the quality of responses from an LLM. + * + * The SAP HANA Cloud vector engine supports the create, read, update, and delete (CRUD) + * operations involving vectors using SQL. + * + * HanaCloudVectorStore is an implementation of + * org.springframework.ai.vectorstore.VectorStore interface that provides + * implementation of COSINE_SIMILARITY function introduced in HanaDB in Mar, + * 2024 + * + * Hana DB introduced a new datatype REAL_VECTOR that can store embeddings + * generated by org.springframework.ai.embedding.EmbeddingClient + * + * @author Rahul Mittal + * @see SAP + * HANA Database Vector Engine Guide + * @since 1.0.0 + */ +public class HanaCloudVectorStore implements VectorStore { + + private static final Logger logger = LoggerFactory.getLogger(HanaCloudVectorStore.class); + + private final HanaVectorRepository repository; + + private final EmbeddingClient embeddingClient; + + private final HanaCloudVectorStoreConfig config; + + public HanaCloudVectorStore(HanaVectorRepository repository, + EmbeddingClient embeddingClient, HanaCloudVectorStoreConfig config) { + this.repository = repository; + this.embeddingClient = embeddingClient; + this.config = config; + } + + @Override + public void add(List documents) { + int count = 1; + for (Document document : documents) { + logger.info("[{}/{}] Calling EmbeddingClient for document id = {}", count++, documents.size(), + document.getId()); + String content = document.getContent().replaceAll("\\s+", " "); + String embedding = getEmbedding(document); + repository.save(config.getTableName(), document.getId(), embedding, content); + } + logger.info("Embeddings saved in HanaCloudVectorStore for {} documents", count - 1); + } + + @Override + public Optional delete(List idList) { + int deleteCount = repository.deleteEmbeddingsById(config.getTableName(), idList); + logger.info("{} embeddings deleted", deleteCount); + return Optional.of(deleteCount == idList.size()); + } + + public int purgeEmbeddings() { + int deleteCount = repository.deleteAllEmbeddings(config.getTableName()); + logger.info("{} embeddings deleted", deleteCount); + return deleteCount; + } + + @Override + public List similaritySearch(String query) { + return similaritySearch(SearchRequest.query(query).withTopK(config.getTopK())); + } + + @Override + public List similaritySearch(SearchRequest request) { + if (request.hasFilterExpression()) { + throw new UnsupportedOperationException( + "SAPHanaVectorEngine does not support metadata filter expressions yet."); + } + + String queryEmbedding = getEmbedding(request); + List searchResult = repository.cosineSimilaritySearch(config.getTableName(), + request.getTopK(), queryEmbedding); + logger.info("Hana cosine-similarity for query={}, with topK={} returned {} results", request.getQuery(), + request.getTopK(), searchResult.size()); + + return searchResult.stream().map(c -> { + try { + return new Document(c.get_id(), c.toJson(), Collections.emptyMap()); + } + catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }).collect(Collectors.toList()); + } + + private String getEmbedding(SearchRequest searchRequest) { + return "[" + this.embeddingClient.embed(searchRequest.getQuery()) + .stream() + .map(String::valueOf) + .collect(Collectors.joining(", ")) + "]"; + } + + private String getEmbedding(Document document) { + return "[" + + this.embeddingClient.embed(document).stream().map(String::valueOf).collect(Collectors.joining(", ")) + + "]"; + } + +} diff --git a/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaCloudVectorStoreConfig.java b/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaCloudVectorStoreConfig.java new file mode 100644 index 00000000000..315bbdca955 --- /dev/null +++ b/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaCloudVectorStoreConfig.java @@ -0,0 +1,68 @@ +/* + * Copyright 2023 - 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.vectorstore; + +/** + * @author Rahul Mittal + * @since 1.0.0 + */ +public class HanaCloudVectorStoreConfig { + + private String tableName; + + private int topK; + + private HanaCloudVectorStoreConfig() { + } + + public static HanaCloudVectorStoreConfigBuilder builder() { + return new HanaCloudVectorStoreConfigBuilder(); + } + + public String getTableName() { + return tableName; + } + + public int getTopK() { + return topK; + } + + public static class HanaCloudVectorStoreConfigBuilder { + + private String tableName; + + private int topK; + + public HanaCloudVectorStoreConfigBuilder tableName(String tableName) { + this.tableName = tableName; + return this; + } + + public HanaCloudVectorStoreConfigBuilder topK(int topK) { + this.topK = topK; + return this; + } + + public HanaCloudVectorStoreConfig build() { + HanaCloudVectorStoreConfig config = new HanaCloudVectorStoreConfig(); + config.tableName = tableName; + config.topK = topK; + return config; + } + + } + +} diff --git a/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaVectorEntity.java b/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaVectorEntity.java new file mode 100644 index 00000000000..b5031b24892 --- /dev/null +++ b/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaVectorEntity.java @@ -0,0 +1,46 @@ +/* + * Copyright 2023 - 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.vectorstore; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; + +/** + * @author Rahul Mittal + * @since 1.0.0 + */ +@MappedSuperclass +public abstract class HanaVectorEntity { + + @Id + @Column(name = "_id") + protected String _id; + + public HanaVectorEntity() { + } + + public String toJson() throws JsonProcessingException { + return new ObjectMapper().writeValueAsString(this); + } + + public String get_id() { + return _id; + } + +} diff --git a/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaVectorRepository.java b/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaVectorRepository.java new file mode 100644 index 00000000000..8481f9300ec --- /dev/null +++ b/vector-stores/spring-ai-hanadb-store/src/main/java/org/springframework/ai/vectorstore/HanaVectorRepository.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 - 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.vectorstore; + +import java.util.List; + +/** + * @author Rahul Mittal + * @since 1.0.0 + */ +public interface HanaVectorRepository { + + void save(String tableName, String id, String embedding, String content); + + int deleteEmbeddingsById(String tableName, List idList); + + int deleteAllEmbeddings(String tableName); + + List cosineSimilaritySearch(String tableName, int topK, String queryEmbedding); + +} diff --git a/vector-stores/spring-ai-hanadb-store/src/test/java/org/springframework/ai/vectorstore/CricketWorldCup.java b/vector-stores/spring-ai-hanadb-store/src/test/java/org/springframework/ai/vectorstore/CricketWorldCup.java new file mode 100644 index 00000000000..ed1f1e393ed --- /dev/null +++ b/vector-stores/spring-ai-hanadb-store/src/test/java/org/springframework/ai/vectorstore/CricketWorldCup.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023 - 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.vectorstore; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +/** + * @author Rahul Mittal + * @since 1.0.0 + */ +@Entity +@Table(name = "CRICKET_WORLD_CUP") +public class CricketWorldCup extends HanaVectorEntity { + + @Column(name = "content") + private String content; + + public String getContent() { + return content; + } + +} diff --git a/vector-stores/spring-ai-hanadb-store/src/test/java/org/springframework/ai/vectorstore/CricketWorldCupHanaController.java b/vector-stores/spring-ai-hanadb-store/src/test/java/org/springframework/ai/vectorstore/CricketWorldCupHanaController.java new file mode 100644 index 00000000000..ad3f954a7a6 --- /dev/null +++ b/vector-stores/spring-ai-hanadb-store/src/test/java/org/springframework/ai/vectorstore/CricketWorldCupHanaController.java @@ -0,0 +1,96 @@ +/* + * Copyright 2023 - 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.vectorstore; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.chat.ChatClient; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.chat.prompt.SystemPromptTemplate; +import org.springframework.ai.document.Document; +import org.springframework.ai.reader.pdf.PagePdfDocumentReader; +import org.springframework.ai.transformer.splitter.TokenTextSplitter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * @author Rahul Mittal + * @since 1.0.0 + */ +@RestController +public class CricketWorldCupHanaController { + + private static final Logger logger = LoggerFactory.getLogger(CricketWorldCupHanaController.class); + + private final VectorStore hanaCloudVectorStore; + + private final ChatClient chatClient; + + @Autowired + public CricketWorldCupHanaController(ChatClient chatClient, VectorStore hanaCloudVectorStore) { + this.chatClient = chatClient; + this.hanaCloudVectorStore = hanaCloudVectorStore; + } + + @PostMapping("/ai/hana-vector-store/cricket-world-cup/purge-embeddings") + public ResponseEntity purgeEmbeddings() { + int deleteCount = ((HanaCloudVectorStore) this.hanaCloudVectorStore).purgeEmbeddings(); + logger.info("{} embeddings purged from CRICKET_WORLD_CUP table in Hana DB", deleteCount); + return ResponseEntity.ok() + .body(String.format("%d embeddings purged from CRICKET_WORLD_CUP table in Hana DB", deleteCount)); + } + + @PostMapping("/ai/hana-vector-store/cricket-world-cup/upload") + public ResponseEntity handleFileUpload(@RequestParam("pdf") MultipartFile file) throws IOException { + Resource pdf = file.getResource(); + Supplier> reader = new PagePdfDocumentReader(pdf); + Function, List> splitter = new TokenTextSplitter(); + List documents = splitter.apply(reader.get()); + logger.info("{} documents created from pdf file: {}", documents.size(), pdf.getFilename()); + hanaCloudVectorStore.accept(documents); + return ResponseEntity.ok() + .body(String.format("%d documents created from pdf file: %s", documents.size(), pdf.getFilename())); + } + + @GetMapping("/ai/hana-vector-store/cricket-world-cup") + public Map hanaVectorStoreSearch(@RequestParam(value = "message") String message) { + var documents = this.hanaCloudVectorStore.similaritySearch(message); + var inlined = documents.stream().map(Document::getContent).collect(Collectors.joining(System.lineSeparator())); + var similarDocsMessage = new SystemPromptTemplate("Based on the following: {documents}") + .createMessage(Map.of("documents", inlined)); + + var userMessage = new UserMessage(message); + Prompt prompt = new Prompt(List.of(similarDocsMessage, userMessage)); + String generation = chatClient.call(prompt).getResult().getOutput().getContent(); + logger.info("Generation: {}", generation); + return Map.of("generation", generation); + } + +} diff --git a/vector-stores/spring-ai-hanadb-store/src/test/java/org/springframework/ai/vectorstore/CricketWorldCupRepository.java b/vector-stores/spring-ai-hanadb-store/src/test/java/org/springframework/ai/vectorstore/CricketWorldCupRepository.java new file mode 100644 index 00000000000..397ea39be64 --- /dev/null +++ b/vector-stores/spring-ai-hanadb-store/src/test/java/org/springframework/ai/vectorstore/CricketWorldCupRepository.java @@ -0,0 +1,83 @@ +/* + * Copyright 2023 - 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.vectorstore; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.transaction.Transactional; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Rahul Mittal + * @since 1.0.0 + */ +@Repository +public class CricketWorldCupRepository implements HanaVectorRepository { + + @PersistenceContext + private EntityManager entityManager; + + @Override + @Transactional + public void save(String tableName, String id, String embedding, String content) { + String sql = String.format(""" + INSERT INTO %s (_ID, EMBEDDING, CONTENT) + VALUES(:_id, TO_REAL_VECTOR(:embedding), :content) + """, tableName); + + entityManager.createNativeQuery(sql) + .setParameter("_id", id) + .setParameter("embedding", embedding) + .setParameter("content", content) + .executeUpdate(); + } + + @Override + @Transactional + public int deleteEmbeddingsById(String tableName, List idList) { + String sql = String.format(""" + DELETE FROM %s WHERE _ID IN (:ids) + """, tableName); + + return entityManager.createNativeQuery(sql).setParameter("ids", idList).executeUpdate(); + } + + @Override + @Transactional + public int deleteAllEmbeddings(String tableName) { + String sql = String.format(""" + DELETE FROM %s + """, tableName); + + return entityManager.createNativeQuery(sql).executeUpdate(); + } + + @Override + public List cosineSimilaritySearch(String tableName, int topK, String queryEmbedding) { + String sql = String.format(""" + SELECT TOP :topK * FROM %s + ORDER BY COSINE_SIMILARITY(EMBEDDING, TO_REAL_VECTOR(:queryEmbedding)) DESC + """, tableName); + + return entityManager.createNativeQuery(sql, CricketWorldCup.class) + .setParameter("topK", topK) + .setParameter("queryEmbedding", queryEmbedding) + .getResultList(); + } + +} diff --git a/vector-stores/spring-ai-hanadb-store/src/test/java/org/springframework/ai/vectorstore/HanaCloudVectorStoreIT.java b/vector-stores/spring-ai-hanadb-store/src/test/java/org/springframework/ai/vectorstore/HanaCloudVectorStoreIT.java new file mode 100644 index 00000000000..463d2fe1086 --- /dev/null +++ b/vector-stores/spring-ai-hanadb-store/src/test/java/org/springframework/ai/vectorstore/HanaCloudVectorStoreIT.java @@ -0,0 +1,132 @@ +/* + * Copyright 2023 - 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.vectorstore; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.document.Document; +import org.springframework.ai.embedding.EmbeddingClient; +import org.springframework.ai.openai.OpenAiEmbeddingClient; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.reader.pdf.PagePdfDocumentReader; +import org.springframework.ai.transformer.splitter.TokenTextSplitter; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.testcontainers.junit.jupiter.Testcontainers; + +import javax.sql.DataSource; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * @author Rahul Mittal + * @since 1.0.0 + */ +@Testcontainers +@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") +@EnabledIfEnvironmentVariable(named = "HANA_DATASOURCE_URL", matches = ".+") +@EnabledIfEnvironmentVariable(named = "HANA_DATASOURCE_USERNAME", matches = ".+") +@EnabledIfEnvironmentVariable(named = "HANA_DATASOURCE_PASSWORD", matches = ".+") +public class HanaCloudVectorStoreIT { + + private static final Logger logger = LoggerFactory.getLogger(HanaCloudVectorStoreIT.class); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(HanaTestApplication.class); + + @Test + public void vectorStoreTest() { + contextRunner.run(context -> { + + VectorStore vectorStore = context.getBean(HanaCloudVectorStore.class); + int deleteCount = ((HanaCloudVectorStore) vectorStore).purgeEmbeddings(); + logger.info("Purged all embeddings: count={}", deleteCount); + + Supplier> reader = new PagePdfDocumentReader("classpath:Cricket_World_Cup.pdf"); + Function, List> splitter = new TokenTextSplitter(); + List documents = splitter.apply(reader.get()); + vectorStore.accept(documents); + + List results = vectorStore.similaritySearch("Who won the 2023 cricket world cup finals?"); + Assertions.assertEquals(1, results.size()); + Assertions.assertTrue(results.get(0).getContent().contains("Australia")); + + // Remove all documents from the store + vectorStore.delete(documents.stream().map(Document::getId).toList()); + List results2 = vectorStore.similaritySearch("Who won the 2023 cricket world cup finals?"); + Assertions.assertEquals(0, results2.size()); + }); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + public static class HanaTestApplication { + + @Bean + public VectorStore hanaCloudVectorStore(CricketWorldCupRepository cricketWorldCupRepository, + EmbeddingClient embeddingClient) { + return new HanaCloudVectorStore(cricketWorldCupRepository, embeddingClient, + HanaCloudVectorStoreConfig.builder().tableName("CRICKET_WORLD_CUP").topK(1).build()); + } + + @Bean + public CricketWorldCupRepository cricketWorldCupRepository() { + return new CricketWorldCupRepository(); + } + + @Bean + public DataSource dataSource() { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + + dataSource.setDriverClassName("com.sap.db.jdbc.Driver"); + dataSource.setUrl(System.getenv("HANA_DATASOURCE_URL")); + dataSource.setUsername(System.getenv("HANA_DATASOURCE_USERNAME")); + dataSource.setPassword(System.getenv("HANA_DATASOURCE_PASSWORD")); + + return dataSource; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory() { + LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); + em.setDataSource(dataSource()); + em.setPackagesToScan("org.springframework.ai.vectorstore"); + + JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + em.setJpaVendorAdapter(vendorAdapter); + + return em; + } + + @Bean + public EmbeddingClient embeddingClient() { + return new OpenAiEmbeddingClient(new OpenAiApi(System.getenv("OPENAI_API_KEY"))); + } + + } + +} \ No newline at end of file diff --git a/vector-stores/spring-ai-hanadb-store/src/test/resources/Cricket_World_Cup.pdf b/vector-stores/spring-ai-hanadb-store/src/test/resources/Cricket_World_Cup.pdf new file mode 100644 index 00000000000..1d8ffae5f77 Binary files /dev/null and b/vector-stores/spring-ai-hanadb-store/src/test/resources/Cricket_World_Cup.pdf differ diff --git a/vector-stores/spring-ai-hanadb-store/src/test/resources/application.properties b/vector-stores/spring-ai-hanadb-store/src/test/resources/application.properties new file mode 100644 index 00000000000..5f9e15ec539 --- /dev/null +++ b/vector-stores/spring-ai-hanadb-store/src/test/resources/application.properties @@ -0,0 +1,10 @@ +spring.ai.openai.api-key=${OPENAI_API_KEY} +spring.ai.openai.embedding.options.model=text-embedding-ada-002 + +spring.datasource.driver-class-name=com.sap.db.jdbc.Driver +spring.datasource.url=${HANA_DATASOURCE_URL} +spring.datasource.username=${HANA_DATASOURCE_USERNAME} +spring.datasource.password=${HANA_DATASOURCE_PASSWORD} + +spring.ai.vectorstore.hanadb.tableName=CRICKET_WORLD_CUP +spring.ai.vectorstore.hanadb.topK=3 \ No newline at end of file