在 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 "{詞遷入模型}"
這個模型會負責將文字轉換成語意向量。
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 的連線。
我們設計一個簡單的 REST API,
讓前端可以送文字給後端進行 Embedding + 儲存。
@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));
}
}
@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
);
}
}
@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;
}
這裡我加入一段 RAG 的流程介紹
步驟 | 處理項目 | 使用元件 |
---|---|---|
1️⃣ 轉成文字文件 | Document |
Spring AI Core |
2️⃣ 切分長文本 | TokenTextSplitter |
Spring AI Text 工具 |
3️⃣ 生成向量 | OllamaEmbeddingModel |
本地嵌入模型 |
4️⃣ 寫入資料庫 | QdrantClient.upsertAsync() |
gRPC Qdrant 連線 |
我們已成功完成:
接下來,我們要讓這些向量「被搜尋、被理解」。