SAP HANA Cloud

先決條件

自動配置

Spring AI 為 SAP Hana 向量儲存庫提供 Spring Boot 自動配置。若要啟用它,請將以下依賴項新增至您的專案 Maven pom.xml 檔案

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

或您的 Gradle build.gradle 建置檔案。

dependencies {
    implementation 'org.springframework.ai:spring-ai-hanadb-store-spring-boot-starter'
}
請參閱 依賴項管理 章節,將 Spring AI BOM 新增至您的建置檔案。

請查看 向量儲存庫的配置參數 列表,以了解預設值和配置選項。

請參閱 儲存庫 章節,將 Milestone 和/或 Snapshot 儲存庫新增至您的建置檔案。

此外,您還需要一個已配置的 EmbeddingModel bean。請參閱 EmbeddingModel 章節以取得更多資訊。

HanaCloudVectorStore 屬性

您可以在 Spring Boot 配置中使用以下屬性來自訂 SAP Hana 向量儲存庫。它使用 spring.datasource. 屬性來配置 Hana 資料來源,並使用 spring.ai.vectorstore.hanadb. 屬性來配置 Hana 向量儲存庫。

屬性 描述 預設值

spring.datasource.driver-class-name

驅動程式類別名稱

com.sap.db.jdbc.Driver

spring.datasource.url

Hana 資料來源 URL

-

spring.datasource.username

Hana 資料來源使用者名稱

-

spring.datasource.password

Hana 資料來源密碼

-

spring.ai.vectorstore.hanadb.top-k

待辦事項

-

spring.ai.vectorstore.hanadb.table-name

待辦事項

-

spring.ai.vectorstore.hanadb.initialize-schema

是否初始化必要的結構描述

false

建立範例 RAG 應用程式

展示如何設定一個專案,使用 SAP Hana Cloud 作為向量資料庫,並利用 OpenAI 實作 RAG 模式

  • 在 SAP Hana DB 中建立名為 CRICKET_WORLD_CUP 的資料表

CREATE TABLE CRICKET_WORLD_CUP (
    _ID VARCHAR2(255) PRIMARY KEY,
    CONTENT CLOB,
    EMBEDDING REAL_VECTOR(1536)
)
  • 在您的 pom.xml 中新增以下依賴項

您可以將屬性 spring-ai-version 設定為 <spring-ai-version>1.0.0-SNAPSHOT</spring-ai-version>

<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>
  • application.properties 檔案中新增以下屬性

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

建立名為 CricketWorldCupEntity 類別,該類別繼承自 HanaVectorEntity

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;
}
  • 建立名為 CricketWorldCupRepositoryRepository,該 Repository 實作 HanaVectorRepository 介面

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);

		this.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 this.entityManager.createNativeQuery(sql)
                .setParameter("ids", idList)
                .executeUpdate();
    }

    @Override
    @Transactional
    public int deleteAllEmbeddings(String tableName) {
        String sql = String.format("""
                DELETE FROM %s
                """, tableName);

        return this.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 this.entityManager.createNativeQuery(sql, CricketWorldCup.class)
                .setParameter("topK", topK)
                .setParameter("queryEmbedding", queryEmbedding)
                .getResultList();
    }
}
  • 現在,建立一個 REST Controller 類別 CricketWorldCupHanaController,並自動注入 ChatModelVectorStore 作為依賴項。在這個 controller 類別中,建立以下 REST 端點

    • /ai/hana-vector-store/cricket-world-cup/purge-embeddings - 從向量儲存庫清除所有嵌入

    • /ai/hana-vector-store/cricket-world-cup/upload - 上傳 Cricket_World_Cup.pdf,使其資料作為嵌入儲存在 SAP Hana Cloud 向量資料庫中

    • /ai/hana-vector-store/cricket-world-cup - 使用 SAP Hana DB 中的 Cosine_Similarity 實作 RAG

package com.interviewpedia.spring.ai.hana;

import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
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 ChatModel chatModel;

    @Autowired
    public CricketWorldCupHanaController(ChatModel chatModel, VectorStore hanaCloudVectorStore) {
        this.chatModel = chatModel;
        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());
		this.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 = this.chatModel.call(prompt).getResult().getOutput().getContent();
        log.info("Generation: {}", generation);
        return Map.of("generation", generation);
    }
}
  • 使用來自維基百科的上下文 PDF 檔案

前往 維基百科下載 Cricket World Cup 頁面為 PDF 檔案。

wikipedia

使用我們在上一步建立的檔案上傳 REST 端點上傳此 PDF 檔案。