EN

3.6 將處理後的資料儲存到向量資料庫

把每一塊的向量、原文和 metadata 一起存進去,以及資料更新時怎麼處理。

RAG下一篇 →

資料切好、metadata 也備好了,最後一步是把它們存進向量資料庫,讓檢索器可以搜尋。

要存哪些東西

每一塊要存四樣東西:

  1. id:這一塊的唯一識別碼。
  2. 向量(embedding):用嵌入模型產生的。
  3. 原文(document):要交給 LLM 的文字。
  4. metadata:3.5 準備的描述資訊。

流程

  1. 把所有塊的文字送進嵌入模型,取得向量(通常一次送很多筆,比一筆一筆快)。
  2. 把 id、向量、原文、metadata 一起寫入資料庫。
  3. 建立索引(大多數資料庫會自動處理)。

實作:用 Chroma

這裡用 Chroma 示範,它可以直接在自己的電腦上用,不需要另外架設。接續上一節的 heading_chunks(依標題切分):

import chromadb
from sentence_transformers import SentenceTransformer

from chunk_demo import DOC, heading_chunks

# 1. 切塊(依標題),每一塊帶著它的標題路徑當 metadata
chunks = heading_chunks(DOC)
texts = [c["text"] for c in chunks]
metadatas = [{"source": "員工手冊.md", "headings": c["headings"]} for c in chunks]
ids = [f"handbook-{i}" for i in range(len(chunks))]

# 2. 轉成向量(和 2.7 同一個嵌入模型)
model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
embeddings = model.encode(texts).tolist()

# 3. 存進 Chroma(這裡用記憶體版本,程式結束就消失)
client = chromadb.Client()
collection = client.create_collection(name="handbook", metadata={"hnsw:space": "cosine"})
collection.add(ids=ids, embeddings=embeddings, documents=texts, metadatas=metadatas)
print("已存入", collection.count(), "塊")

# 4. 查詢:問題一樣要用同一個模型轉成向量
question = "沒休完的特休假會怎麼處理?"
result = collection.query(query_embeddings=model.encode([question]).tolist(), n_results=2)

for doc, meta, dist in zip(result["documents"][0], result["metadatas"][0], result["distances"][0]):
    print(f"距離 {dist:.3f}|{meta['headings']}")
    print(doc[:40], "...")

# 5. 加上 metadata 過濾:只在「加班規定」那一節找
filtered = collection.query(
    query_embeddings=model.encode([question]).tolist(),
    n_results=1,
    where={"headings": "員工手冊 > 加班規定"},
)
print("限定加班規定時找到:", filtered["metadatas"][0][0]["headings"])

我實際跑過,輸出是:

已存入 2 塊
距離 0.483|員工手冊 > 特別休假
員工在同一公司服務滿六個月未滿一年者,每年有三天特別休假。滿一年未滿兩年者,有七 ...
距離 0.659|員工手冊 > 加班規定
加班必須事先經主管同意。平日加班前兩小時,以平日每小時工資的 1.34 倍計算。 ...
限定加班規定時找到: 員工手冊 > 加班規定

幾個重點:

  • 問題「沒休完的特休假會怎麼處理?」沒有任何一個字跟標題「特別休假」完全一樣,但還是找到了特別休假那一塊,距離最小(0.483)。
  • hnsw:space: cosine 指定用餘弦距離。這裡的數字是「距離」,越小越相似,跟上一章的「相似度」相反。
  • 第 5 步用 where 過濾 metadata,只在指定的那一節找,雖然那一節的內容其實不相關,但資料庫照樣回傳。可見過濾是「縮小範圍」,不是「判斷相不相關」。

注意 Chroma 的 chromadb.Client() 預設只存在記憶體,程式結束資料就沒了。要保存的話,改用 chromadb.PersistentClient(path="./db")。

資料會變,要怎麼更新

知識庫不是存了就結束,文件會新增、修改、刪除。幾個要處理的情況:

用穩定的 id

id 最好能由內容決定,例如「來源檔名 + 第幾塊」,或是內容的雜湊值。這樣:

  • 重複載入同一份文件時,不會重複存兩份。
  • 要更新、刪除某一塊時,找得到它。

用 upsert

大多數向量資料庫提供 upsert(存在就更新、不存在就新增),比單純新增更適合重複執行的資料準備流程。

文件修改時

最簡單的做法:刪掉這份文件的所有舊塊,再重新處理、存入新塊。因為文件一改,切出來的塊可能整個都不一樣,個別更新很容易漏掉。這也是為什麼 metadata 要記 source,才能找到「這份文件的所有塊」。

文件刪除時

要把對應的所有塊也刪掉,否則檢索還是會找到已經不存在的內容。

換嵌入模型時

所有向量都要重新產生,因為不同模型的向量不能混用(2.2 講過)。

檢查存進去的東西

存完之後,建議做兩個簡單的檢查:

  1. 數量對不對:存進去的塊數,是不是和預期的一樣。
  2. 抽樣看內容:隨機取幾塊,看看文字有沒有亂碼、有沒有被切得奇怪、metadata 有沒有都填。

很多檢索問題,其實是資料準備階段就出的問題。

← 全部文章