iT邦幫忙

2025 iThome 鐵人賽

DAY 28
0
AI & Data

讓電腦聽懂人話:30 天 NLP 入門系列 第 28

Day 28|RAG Step 2:Retrieval 向量檢索

  • 分享至 

  • xImage
  •  

引言

在上一篇的內容中,我們把所有文章切成小段落(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


Vector Search

這個部分會有兩個步驟:

  • 把使用者問題轉成向量
  • 在 Qdrant 找到 top_k 個最相似的 chunk
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 做生成~


上一篇
Day 27|RAG Step 1:Chunking、向量資料庫
系列文
讓電腦聽懂人話:30 天 NLP 入門28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言