Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<module>spring-ai-spring-boot-starters/spring-ai-starter-chroma-store</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-milvus-store</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-pgvector-store</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-hanadb-store</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-pinecone-store</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-azure-store</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-weaviate-store</module>
Expand All @@ -47,6 +48,7 @@
<module>spring-ai-spring-boot-starters/spring-ai-starter-postgresml-embedding</module>
<module>spring-ai-docs</module>
<module>vector-stores/spring-ai-pgvector-store</module>
<module>vector-stores/spring-ai-hanadb-store</module>
<module>vector-stores/spring-ai-milvus-store</module>
<module>vector-stores/spring-ai-neo4j-store</module>
<module>document-readers/pdf-reader</module>
Expand Down Expand Up @@ -135,6 +137,7 @@
<!-- readers/writer/stores dependencies-->
<pdfbox.version>3.0.1</pdfbox.version>
<pgvector.version>0.1.4</pgvector.version>
<sap.hanadb.version>2.20.11</sap.hanadb.version>
<postgresql.version>42.7.2</postgresql.version>
<milvus.version>2.3.4</milvus.version>
<pinecone.version>0.8.0</pinecone.version>
Expand Down
12 changes: 12 additions & 0 deletions spring-ai-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-hanadb-store</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pinecone</artifactId>
Expand Down Expand Up @@ -272,6 +278,12 @@
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-hanadb-store-spring-boot-starter</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-watsonx-ai</artifactId>
Expand Down
1 change: 1 addition & 0 deletions spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
= 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

==== 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 `<spring-ai-version>1.0.0-SNAPSHOT</spring-ai-version>`:
[source,xml]
----
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
<version>${spring-ai-version}</version>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>${spring-ai-version}</version>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-hanadb-store-spring-boot-starter</artifactId>
<version>${spring-ai-version}</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
----

==== Add the following properties in `application.properties` file:
[yml]
----
spring.ai.openai.api-key=<YOUR_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=<YOUR_HANA_CLOUD_DB_URL>
spring.datasource.username=<HANA_DB_USERNAME>
spring.datasource.password=<HANA_DB_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<CricketWorldCup> {
@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<String> 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<CricketWorldCup> 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<String> 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<String> handleFileUpload(@RequestParam("pdf") MultipartFile file) throws IOException {
Resource pdf = file.getResource();
Supplier<List<Document>> reader = new PagePdfDocumentReader(pdf);
Function<List<Document>, List<Document>> splitter = new TokenTextSplitter();
List<Document> 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<String, String> 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. Upload this PDF file using the file-upload REST endpoint that we created in the previous step.
8 changes: 8 additions & 0 deletions spring-ai-spring-boot-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@
<optional>true</optional>
</dependency>

<!-- SAP Hana Cloud Vector Store-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-hanadb-store</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>

<dependency>
<groupId>com.pgvector</groupId>
<artifactId>pgvector</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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<? extends HanaVectorEntity> repository,
EmbeddingClient embeddingClient, HanaCloudVectorStoreProperties properties) {

return new HanaCloudVectorStore(repository, embeddingClient,
HanaCloudVectorStoreConfig.builder()
.tableName(properties.getTableName())
.topK(properties.getTopK())
.build());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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;
}

}
Loading