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.
3
pom.xml
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/0.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/1.png
Normal file
|
After Width: | Height: | Size: 614 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/10.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/11.png
Normal file
|
After Width: | Height: | Size: 881 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/13.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/14.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/15.png
Normal file
|
After Width: | Height: | Size: 362 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/16.png
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/17.png
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/18.png
Normal file
|
After Width: | Height: | Size: 241 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/19.png
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/2.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 267 KiB |
|
After Width: | Height: | Size: 416 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/21.png
Normal file
|
After Width: | Height: | Size: 478 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/22.png
Normal file
|
After Width: | Height: | Size: 358 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/23.png
Normal file
|
After Width: | Height: | Size: 393 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/24.png
Normal file
|
After Width: | Height: | Size: 406 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/25.png
Normal file
|
After Width: | Height: | Size: 354 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/26.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/27.png
Normal file
|
After Width: | Height: | Size: 320 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/28.png
Normal file
|
After Width: | Height: | Size: 382 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/29.png
Normal file
|
After Width: | Height: | Size: 368 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/3.png
Normal file
|
After Width: | Height: | Size: 230 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/30.png
Normal file
|
After Width: | Height: | Size: 300 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/31.png
Normal file
|
After Width: | Height: | Size: 357 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/32.png
Normal file
|
After Width: | Height: | Size: 296 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/33.png
Normal file
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 385 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/35.png
Normal file
|
After Width: | Height: | Size: 263 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/36.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/37.png
Normal file
|
After Width: | Height: | Size: 276 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/38.png
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/39.png
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/4.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/40.png
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/5.png
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/6.png
Normal file
|
After Width: | Height: | Size: 338 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/7.png
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/8.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
spring-ai-docs/src/main/antora/modules/ROOT/images/hanadb/9.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 778 KiB |
@@ -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[]
|
||||
|
||||
@@ -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.
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
1
vector-stores/spring-ai-hanadb-store/README.md
Normal file
@@ -0,0 +1 @@
|
||||
[SAP Hana Cloud Vector Store Documentation](https://docs.spring.io/spring-ai/reference/api/vectordbs/hana.html)
|
||||
81
vector-stores/spring-ai-hanadb-store/pom.xml
Normal 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>
|
||||
@@ -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(", "))
|
||||
+ "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||