iT邦幫忙

2025 iThome 鐵人賽

DAY 26
0
生成式 AI

nutc_imac_Agent拼裝車系列 第 26

Day26|RAG 實戰篇 (三):使用 Ollama 生成 Embedding 並存入 Qdrant

  • 分享至 

  • xImage
  •  

在 Day25,我們成功啟動了 Qdrant,並能建立與刪除 Collection。
今天,我們要讓這個資料庫「學會記住文字」。

我們將整合 Ollama + Spring AI + Qdrant (gRPC)
實作一個能將使用者輸入轉成向量,並存入資料庫的流程。


一、整體架構回顧

Ollama Embedding
       ↓
Qdrant 向量資料庫
       ↓
語意搜尋(相似度比對)

今天的重點是 第一段:Embedding → 存進 Qdrant
下一篇(Day27)我們會處理「檢索與回答」。


二、啟動環境

請確認兩個服務都在運行:

# 啟動 Qdrant
docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant

# 啟動 Ollama
ollama serve

✅ 安裝嵌入模型

ollama pull "{詞遷入模型}"

這個模型會負責將文字轉換成語意向量。


三、Spring 設定

1️⃣ 加入依賴(pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring AI - Ollama 支援 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-ollama</artifactId>
    </dependency>

    <!-- Spring AI - Qdrant 支援 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-vector-store-qdrant</artifactId>
    </dependency>
</dependencies>

這樣 Spring AI 就能自動幫我們建立好 Ollama 與 Qdrant 的連線。


四、建立 Embedding API

我們設計一個簡單的 REST API,
讓前端可以送文字給後端進行 Embedding + 儲存。

Controller

@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/rag/embedding")
@Slf4j
public class EmbeddingController {

    private final EmbeddingService embeddingService;
    /**
     * 文字 embedding
     * */
    @PostMapping("/userInput")
    public ResponseEntity<BaseResponse> userInputEmbedding(@RequestBody UserInputEmbeddingRequest request) {
        return ResponseEntity.ok(embeddingService.userInputEmbedding(request));
    }


    /**
     * 相似搜尋
     * */
    @PostMapping("/similarSearch")
    public ResponseEntity<BaseResponse> similarSearch(@RequestBody SimilarSearchRequest request) {
        return ResponseEntity.ok(embeddingService.similarSearch(request));
    }
}

五、Service 實作

@Slf4j
@Service
@RequiredArgsConstructor
public class EmbeddingImpl implements EmbeddingService {

    private final QdrantClient qdrantClient;
    private final OllamaEmbeddingModel ollamaEmbeddingModel;
    private final TokenTextSplitter tokenTextSplitter;


    /**
     * UserInputEmbedding 方法
     */
    @Override
    public BaseResponse userInputEmbedding(UserInputEmbeddingRequest request) {

        // 做 Embedding
        List<float[]> embeddingData = embeddingMethod(request.getContent());

        // 將 embedding 組裝成 PointStruct
        List<PointStruct> points = new ArrayList<>();

        for (float[] embedding: embeddingData) {
            PointStruct point = PointStruct.newBuilder()
                    .setVectors(vectors(embedding))
                    .putPayload("document", value(request.getContent()))
                    .setId(Points.PointId.newBuilder().setUuid(UUID.randomUUID().toString()).build())
                    .build();

            points.add(point);
        }

        // 將 embedding 文本存入 Qdrant
        try {
            Future<UpdateResult> future = qdrantClient.upsertAsync(request.getCollectionName(), points);
            return BaseResponse.builder().code("0000").msg("Success").data(future).build();
        } catch (Exception e) {
            return BaseResponse.builder().code("1111").msg("Fail").data(e.getMessage()).build();
        }    }

    /**
     * SimilarSearch 方法
     */
    @Override
    public BaseResponse similarSearch(SimilarSearchRequest request) {
        // 做 Embedding
        List<float[]> embeddingData = embeddingMethod(request.getUserPrompt());

        List<Float> embeddingFloatList = new ArrayList<>();

        // 遍歷 embeddingData
        for (float[] vectorArray : embeddingData) {
            for (float value : vectorArray) {
                embeddingFloatList.add(value);
            }
        }

        try {
            ListenableFuture<List<Points.ScoredPoint>> future = qdrantClient.searchAsync( Points.SearchPoints.newBuilder()
                    .setCollectionName(request.getCollectionName())
                    .addAllVector(embeddingFloatList)
                    .setWithPayload(Points.WithPayloadSelector.newBuilder().setEnable(true).build())
                    .setLimit(3)
                    .build());

            List<Points.ScoredPoint> results = future.get();

            // 處理搜尋結果
            List<String> decodedResults = new ArrayList<>();
            for (Points.ScoredPoint point : results) {
                // 從 payload 中獲取 "document" 欄位的值
                JsonWithInt.Value value = point.getPayloadMap().get("document");
                if (value != null && value.hasStringValue()) {
                    // 直接獲取字串值
                    String decodedString = value.getStringValue();
                    decodedResults.add(decodedString);
                }
            }

            return BaseResponse.builder().code("0000").msg("Success").data(decodedResults.toString()).build();
        } catch (Exception e) {
            return BaseResponse.builder().code("9999").msg("Failed").data(e.getMessage()).build();
        }
    }


    /**
     * Embedding 方法
     */
    private List<float[]> embeddingMethod(String content) {

        // 轉成 Document
        Document document = Document
                .builder()
                .withContent(content)
                .build();

        // 切分文本
        List<Document> chunks = tokenTextSplitter.split(document);

        BatchingStrategy batchingStrategy = new BatchingStrategy() {
            @Override
            public List<List<Document>> batch(List<Document> documents) {
                // 將所有文檔作為單一批次返回
                return List.of(documents);
            }
        };

        // Embedding
        return ollamaEmbeddingModel.embed(
                chunks,
                null,
                batchingStrategy
        );
    }
}

Request DTO

@Data
public class SimilarSearchRequest {
    @JsonProperty("User_prompt")
    @NotNull
    private String userPrompt;

    @JsonProperty("Collection_Name")
    @NotNull
    private String collectionName;

}

@Data
public class UserInputEmbeddingRequest {

    @JsonProperty("Content")
    @NotNull
    private String content;

    @JsonProperty("Collection_Name")
    @NotNull
    private String collectionName;

}


六、測試流程

Step 1:建立 Collection


Step 2:新增文字並產生 Embedding

這裡我加入一段 RAG 的流程介紹

https://ithelp.ithome.com.tw/upload/images/20251007/20150369OeNkU9hrJ8.png

https://ithelp.ithome.com.tw/upload/images/20251007/201503691t4Na8Saah.png


Step 3:相似搜尋驗證

https://ithelp.ithome.com.tw/upload/images/20251007/201503697T5I46rMvp.png


七、工作流程回顧

步驟 處理項目 使用元件
1️⃣ 轉成文字文件 Document Spring AI Core
2️⃣ 切分長文本 TokenTextSplitter Spring AI Text 工具
3️⃣ 生成向量 OllamaEmbeddingModel 本地嵌入模型
4️⃣ 寫入資料庫 QdrantClient.upsertAsync() gRPC Qdrant 連線

八、結果與驗證

我們已成功完成:

  • 使用 Ollama 產生向量
  • 儲存至 Qdrant Collection
  • 系統可作為「語意資料庫」基礎架構

接下來,我們要讓這些向量「被搜尋、被理解」。



上一篇
Day25|RAG 實戰篇 (二):啟動 Qdrant 並建立 Collection
下一篇
Day27|RAG 實戰篇 (四):文件上傳與文字擷取(OCR + PDF Parsing)
系列文
nutc_imac_Agent拼裝車28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言