Add SAP HanaDB vector store integration

- Implement a HanaCloudVectorStore and tests
 - Implement Autoconfiguraiton + properties
 - Add boot starter
 - Update BOM with vector store and boot dependencies.
 - Add antora docuementation
 - added junit for HanaCloudVectorStoreProperties.java and documentation
   to create a BTP trial account and provision an instance for SAP Hana Cloud db
 - updated license, formatting and javadoc
 - IT for HanaCloudVectorStoreAutoConfiguration
 - IT for HanaCloudVectorStoreAutoConfiguration

Additional
 - add @AutoConfiguration(after = { JpaRepositoriesAutoConfiguration.class })
 - update the handa docs structure.
This commit is contained in:
Rahul
2024-04-02 18:00:17 +05:30
committed by Christian Tzolov
parent 8e0fbe49ee
commit 466b824840
67 changed files with 1520 additions and 0 deletions

View File

@@ -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>
@@ -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>
@@ -138,6 +140,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>

View File

@@ -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>
@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 KiB

View File

@@ -55,6 +55,7 @@
*** xref:api/vectordbs/pinecone.adoc[]
*** xref:api/vectordbs/qdrant.adoc[]
*** xref:api/vectordbs/gemfire.adoc[GemFire]
*** xref:api/vectordbs/hana.adoc[SAP Hana]
** xref:api/functions.adoc[Function Calling]
** xref:api/prompt.adoc[]
** xref:api/output-parser.adoc[]

View File

@@ -0,0 +1,320 @@
= SAP HANA Cloud
== Prerequisites
* You need a SAP HANA Cloud vector engine account - 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.
* If required, an API key for the xref:api/embeddings.adoc#available-implementations[EmbeddingClient] to generate the embeddings stored by the vector store.
== Auto-configuration
Spring AI provides Spring Boot auto-configuration for the SAP Hana Vector Sore.
To enable it, add the following dependency to your project's Maven `pom.xml` file:
[source, xml]
----
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-hanadb-store-spring-boot-starter</artifactId>
</dependency>
----
or to your Gradle `build.gradle` build file.
[source,groovy]
----
dependencies {
implementation 'org.springframework.ai:spring-ai-hanadb-store-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.
Please have a look at the list of xref:#_hanacloudvectorstore_properties[configuration parameters] for the vector store to learn about the default values and configuration options.
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.
== HanaCloudVectorStore properties
You can use the following properties in your Spring Boot configuration to customize the SAP Hana vector store.
It uses `spring.datasource.*` properties to configure the Hana datasource and the `spring.ai.vectorstore.hanadb.*` properties to configure the Hana vector store.
|===
|Property| Description | Default value
|`spring.datasource.driver-class-name` | 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.top-k`| TODO | -
|`spring.ai.vectorstore.hanadb.table-name`| TODO | -
|===
== Build a Sample RAG application
Shows 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]
----
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<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>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-hanadb-store-spring-boot-starter</artifactId>
</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=${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<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.
image::hanadb/wikipedia.png[width=800]
Upload this PDF file using the file-upload REST endpoint that we created in the previous step.

View File

@@ -0,0 +1,186 @@
== Provision SAP HANA Cloud 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::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::hanadb/1.png[width=800]
Click `Advanced Trials`
image::hanadb/2.png[width=800]
Click `SAP BTP Trial`
image::hanadb/3.png[width=800]
Click `Start your free 90-day trial`
image::hanadb/4.png[width=800]
Paste the `temporary email id` that we created in the first step, and click `Next`
image::hanadb/5.png[width=800]
We fill in our details and click `Submit`
image::hanadb/6.png[width=800]
It's time to check the inbox of our temporary email account
image::hanadb/7.png[width=800]
Notice that there is an email received in our temporary email account
image::hanadb/8.png[width=800]
Open the email and `click to activate` the trial account
image::hanadb/9.png[width=800]
It will prompt to create a `password`. Provide a password and click `Submit`
image::hanadb/10.png[width=800]
The trial account is now created. Click to `start the trial`
image::hanadb/11.png[width=800]
Provide your phone number and click `Continue`
image::hanadb/13.png[width=800]
We receive an OTP on the phone number. Provide the `code` and click `continue`
image::hanadb/14.png[width=800]
Select the `region` as `US East (VA) - AWS`
image::hanadb/15.png[width=800]
Click `Continue`
image::hanadb/16.png[width=800]
The `SAP BTP trial` account is ready. Click `Go to your Trial account`
image::hanadb/17.png[width=800]
Click the `Trial` sub-account
image::hanadb/18.png[width=800]
Open `Instances and Subscriptions`
image::hanadb/19.png[width=800]
It's time to create a subscription. Click the `Create` button
image::hanadb/20.1.png[width=800]
While creating a subscription, Select `service` as `SAP Hana Cloud` and `Plan` as `tools` and click `Create`
image::hanadb/20.2.png[width=800]
Notice that `SAP Hana Cloud` subscription is now created. Click `Users` on the left panel
image::hanadb/21.png[width=800]
Select the username (temporary email that we supplied earlier) and click `Assign Role Collection`
image::hanadb/22.png[width=800]
Search `hana` and select all the 3 role collections that gets displayed. Click `Assign Role Collection`
image::hanadb/23.png[width=800]
Our `user` now has all the 3 role collections. Click `Instances and Subscriptions`
image::hanadb/24.png[width=800]
Now, click `SAP Hana Cloud` application under subscriptions
image::hanadb/25.png[width=800]
There are no instances yet. Let's click `Create Instance`
image::hanadb/26.png[width=800]
Select Type as `SAP HANA Cloud, SAP HANA Database`. Click `Next Step`
image::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::hanadb/28.png[width=800]
Keep everything as default. Click `Next Step`
image::hanadb/29.png[width=800]
Click `Next Step`
image::hanadb/30.png[width=800]
Select `Allow all IP addresses` and click `Next Step`
image::hanadb/31.png[width=800]
Click `Review and Create`
image::hanadb/32.png[width=800]
Click `Create Instance`
image::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::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::hanadb/34.2.png[width=800]
We navigate to `SAP Hana Database Explorer` by click the `...`
image::hanadb/35.png[width=800]
Provide the administrator credentials and click `OK`
image::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::hanadb/37.png[width=800]
Navigate to `hana_dev_db -> Catalog -> Tables` to find our table `CRICKET_WORLD_CUP`
image::hanadb/38.png[width=800]
Right-click on the table and click `Open Data`
image::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::hanadb/40.png[width=800]
Next steps: xref:api/vectordbs/hana.adoc[SAP Hana Vector Engine]

View File

@@ -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>

View File

@@ -0,0 +1,53 @@
/*
* 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 javax.sql.DataSource;
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.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* @author Rahul Mittal
* @since 1.0.0
*/
@AutoConfiguration(after = { JpaRepositoriesAutoConfiguration.class })
@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());
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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<Document> 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<Document> 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<Document> 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"));
}

View File

@@ -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());
}
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-hanadb-store-spring-boot-starter</artifactId>
<packaging>jar</packaging>
<name>Spring AI Starter - SAP Hana Cloud Vector Store</name>
<description>Spring AI SAP Hana Cloud Vector Store Auto Configuration</description>
<url>https://github.com/spring-projects/spring-ai</url>
<scm>
<url>https://github.com/spring-projects/spring-ai</url>
<connection>git://github.com/spring-projects/spring-ai.git</connection>
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-spring-boot-autoconfigure</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-hanadb-store</artifactId>
<version>${project.parent.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1 @@
[SAP Hana Cloud Vector Store Documentation](https://docs.spring.io/spring-ai/reference/api/vectordbs/hana.html)

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-hanadb-store</artifactId>
<packaging>jar</packaging>
<name>Spring AI Vector Store - HanaDB</name>
<description>Spring AI HanaDB Vector Store</description>
<url>https://github.com/spring-projects/spring-ai</url>
<scm>
<url>https://github.com/spring-projects/spring-ai</url>
<connection>git://github.com/spring-projects/spring-ai.git</connection>
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
<version>${parent.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
</dependency>
<!-- HanaDB -->
<dependency>
<groupId>com.sap.cloud.db.jdbc</groupId>
<artifactId>ngdbc</artifactId>
<version>${sap.hanadb.version}</version>
</dependency>
<!-- TESTING -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
<version>${parent.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-test</artifactId>
<version>${parent.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
<version>${parent.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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 <b>SAP HANA Cloud vector engine</b> 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 <b>Retrieval Augmented Generation</b> (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.
*
* <code>HanaCloudVectorStore</code> is an implementation of
* <code>org.springframework.ai.vectorstore.VectorStore</code> interface that provides
* implementation of <code>COSINE_SIMILARITY</code> function introduced in HanaDB in Mar,
* 2024
*
* Hana DB introduced a new datatype <code>REAL_VECTOR</code> that can store embeddings
* generated by <code>org.springframework.ai.embedding.EmbeddingClient</code>
*
* @author Rahul Mittal
* @see <a href=
* "https://help.sap.com/docs/hana-cloud-database/sap-hana-cloud-sap-hana-database-vector-engine-guide/introduction">SAP
* HANA Database Vector Engine Guide</a>
* @since 1.0.0
*/
public class HanaCloudVectorStore implements VectorStore {
private static final Logger logger = LoggerFactory.getLogger(HanaCloudVectorStore.class);
private final HanaVectorRepository<? extends HanaVectorEntity> repository;
private final EmbeddingClient embeddingClient;
private final HanaCloudVectorStoreConfig config;
public HanaCloudVectorStore(HanaVectorRepository<? extends HanaVectorEntity> repository,
EmbeddingClient embeddingClient, HanaCloudVectorStoreConfig config) {
this.repository = repository;
this.embeddingClient = embeddingClient;
this.config = config;
}
@Override
public void add(List<Document> 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<Boolean> delete(List<String> 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<Document> similaritySearch(String query) {
return similaritySearch(SearchRequest.query(query).withTopK(config.getTopK()));
}
@Override
public List<Document> similaritySearch(SearchRequest request) {
if (request.hasFilterExpression()) {
throw new UnsupportedOperationException(
"SAPHanaVectorEngine does not support metadata filter expressions yet.");
}
String queryEmbedding = getEmbedding(request);
List<? extends HanaVectorEntity> 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(", "))
+ "]";
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<T extends HanaVectorEntity> {
void save(String tableName, String id, String embedding, String content);
int deleteEmbeddingsById(String tableName, List<String> idList);
int deleteAllEmbeddings(String tableName);
List<T> cosineSimilaritySearch(String tableName, int topK, String queryEmbedding);
}

View File

@@ -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;
}
}

View File

@@ -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<String> 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<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());
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<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();
logger.info("Generation: {}", generation);
return Map.of("generation", generation);
}
}

View File

@@ -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<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();
}
}

View File

@@ -0,0 +1,133 @@
/*
* 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;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.sql.DataSource;
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.testcontainers.junit.jupiter.Testcontainers;
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;
/**
* @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<List<Document>> reader = new PagePdfDocumentReader("classpath:Cricket_World_Cup.pdf");
Function<List<Document>, List<Document>> splitter = new TokenTextSplitter();
List<Document> documents = splitter.apply(reader.get());
vectorStore.accept(documents);
List<Document> 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<Document> 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")));
}
}
}

View File

@@ -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