3.6 將處理後的資料儲存到向量資料庫
把每一塊的向量、原文和 metadata 一起存進去,以及資料更新時怎麼處理。
資料切好、metadata 也備好了,最後一步是把它們存進向量資料庫,讓檢索器可以搜尋。
要存哪些東西
每一塊要存四樣東西:
- id:這一塊的唯一識別碼。
- 向量(embedding):用嵌入模型產生的。
- 原文(document):要交給 LLM 的文字。
- metadata:3.5 準備的描述資訊。
流程
- 把所有塊的文字送進嵌入模型,取得向量(通常一次送很多筆,比一筆一筆快)。
- 把 id、向量、原文、metadata 一起寫入資料庫。
- 建立索引(大多數資料庫會自動處理)。
實作:用 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 講過)。
檢查存進去的東西
存完之後,建議做兩個簡單的檢查:
- 數量對不對:存進去的塊數,是不是和預期的一樣。
- 抽樣看內容:隨機取幾塊,看看文字有沒有亂碼、有沒有被切得奇怪、metadata 有沒有都填。
很多檢索問題,其實是資料準備階段就出的問題。