在上一篇的內容中,我們把所有文章切成小段落(chunk),再用 BGE-M3 轉成向量,存進 Qdrant,完成了我們的 知識向量資料庫。前一篇傳送門🚪 今天的重點就是要讓 AI 知道要怎麼去找答案~
假如你有一整座圖書館的資源,但不知道哪本書有你想要的答案,那就算有再多書也沒用對吧><
Retrieval(檢索) 就是要讓 AI 先在圖書館裡找出跟你想知道的主題比較有相關的內容,好方便我們之後再從中細讀找答案。
所以這部分會做的就是:把使用者的問題(Query)向量化,然後拿去向量資料庫中搜尋比對,找到 語意接近 aka. 有相關 的 chunk。
今天就要來一起逛圖書館囉~~
本系列實作的架構圖:
ithomeNLP_RAG/
├── requirements.txt
├── data/
├── db/ # qdrant vector db
├── indexer.py # store vector
├── retriever.py # vector search
├── reranker.txt # rerank
└── frontend.py # generation + UI
我們今天會做的是 retriever.py
這個部分會有兩個步驟:
from typing import List
from qdrant_client import QdrantClient
from langchain_community.embeddings import HuggingFaceEmbeddings
DB_DIR = "./db"
EMBEDDING_MODEL = "BAAI/bge-m3"
COLLECTION_NAME = "ithome_nlp"
class VectorStore:
def __init__(self):
# Qdrant 本地 DB
self.client = QdrantClient(path=DB_DIR)
# HuggingFace Embedding
self.embedding = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL)
# Collection
self.collection_name = COLLECTION_NAME
def embed_query(self, text: str) -> List[float]:
"""將 query 轉為向量"""
try:
return self.embedding.embed_query(text)
except Exception as e:
print(f"❌ Dense向量化查詢時發生錯誤: {str(e)}")
return []
def search(self, query: str, top_k: int):
"""在 collection 中搜尋相關內容"""
try:
print(f"🔍 執行向量搜尋,top_k={top_k}")
# 查詢向量化
query_vector = self.embed_query(query)
# 向量庫搜尋
search_result = self.client.query_points(
collection_name=self.collection_name,
query=query_vector,
with_payload=True,
limit=top_k
).points
# 搜尋結果
results=[]
for point in search_result:
result = {
'similarity_score': point.score,
'source': point.payload.get('source', ''),
'chunk_index': point.payload.get('chunk_index', ''),
'content': point.payload.get('content', '')
}
results.append(result)
print(f"✅ 搜尋完成,返回 {len(results)} 個結果")
if results:
print(f"📊 相似度分數範圍: {min(r['similarity_score'] for r in results):.4f} - {max(r['similarity_score'] for r in results):.4f}")
return {
"results": results,
"total_count": len(results)
}
except Exception as e:
print(f"❌ 搜尋時發生錯誤: {str(e)}")
return {"results": [], "total_count": 0}
這個功能是封裝成一個 VectorStore 的模組,在其他程式腳本都可以直接呼叫這個它。
那我們先來單獨測試一下功能~
def main(query, top_k):
vector_store = VectorStore()
print("=== 測試開始 ===")
# 測試查詢向量化
print("\n🧪 測試查詢向量化...")
query_vector = vector_store.embed_query(query)
if query_vector:
print(f"✅ 查詢向量化成功,query: {query},向量維度: {len(query_vector)}")
else:
print("❌ 查詢向量化失敗")
# 測試相似度搜尋
print("\n🔍 測試相似度搜尋...")
search_results = vector_store.search(query=query, top_k=top_k)
if search_results['total_count'] > 0:
print(f"✅ 搜尋成功,共找到 {search_results['total_count']} 個相關結果")
print(search_results['results'])
else:
print("❌ 沒有找到相關結果")
print("=== 測試完成 ===")
if __name__ == "__main__":
query = "tfidf算法是什麼?"
top_k = 5
main(query, top_k)
=== 測試開始 ===
🧪 測試查詢向量化...
✅ 查詢向量化成功,query: tfidf算法是什麼?,向量維度: 1024
🔍 測試相似度搜尋...
🔍 執行向量搜尋,top_k=5
✅ 搜尋完成,返回 5 個結果
📊 相似度分數範圍: 0.4771 - 0.5558
✅ 搜尋成功,共找到 5 個相關結果
[{'similarity_score': 0.5557503649626927, 'source': 'day11.txt', 'chunk_index': 5, 'content': '\n\n\n## 結語\n\nTF-IDF 可以看成是 BoW 的進階版本。它同樣會統計詞頻,但還會進一步為每個詞加上「重要性」的權重。所以真正重要的詞會被賦予較高的分數,而過於常見、資訊量低的詞則會被壓低權重。這樣一來,向量就更能反映出文本的核心內容!\n\n不過大家也可以想像一下,如果文本長度一拉長,BoW 和 TF-IDF 這類基於詞袋、統計詞頻的方式,**向量維度就會變得非常龐大**。因為整個詞彙表可能會到上萬個詞,而一篇文章可能只用到其中幾百個,於是大部分維度其實都是 $0$。\n這樣的向量我們會稱為 **稀疏向量(sparse vector)**。雖然這樣的表示法在數學上可行,但在語意表達上卻顯得有點笨拙。\n\n接下來,我們會進入到 **「語意向量」(word embeddings)**。它會將文字壓縮成 **低維且稠密(dense)** 的向量,讓每個維度都真正承載語意上的資訊。換句話說,我們就不再只是計算詞頻,而是要探索詞語之間的語意關係~\n\n如果你想知道「語意」能怎麼被數學捕捉的話,那就一定要看下去啦!\n\n## References\n- [【資料分析概念大全|認識文本分析】給我一段話,'}, ...]
=== 測試完成 ===
今天我們完成了 RAG 流程的第二步:Retrieval 向量檢索 🔍
因為有了這一步,AI 就不需要自己編故事,而是能找到有根據的資料。但是通常這一階段只是先大範圍、粗略的抓回有相關的片段。
明天我們會介紹 Reranking 重排序。把今天找出來的候選 chunk 再更精準地排序一下,確保最相關的資訊可以優先送給 LLM 做生成~