昨天已經教學大家要怎麼做 Chunking 了,今天就是要教學如何把它放進一個可以用來查詢的資料庫,這邊我們也會試著提問,看產出。
雖然我昨天的教學只有教你怎麼切章的部分,但我實際上還有將章節、條款一筆筆的切細,當然你也可以試著把之前 chunk 的資料直接存進來練習這邊的操作也可以,只不過你可能要做一些刪減。
1. 建立一個 ChromaDB 資料庫
ChromaDB 是一個「向量資料庫」。我們先建立一個會存在地端的資料庫:
import chromadb
# 建一個存到 ./law_db 資料夾的 ChromaDB
client = chromadb.PersistentClient(path="./law_db")
coll = client.get_or_create_collection("laws")
「大家平常只要用 PersistentClient(path="...") 就好,因為它會真的存檔案起來,這樣也方便你去看裡面的內容;如果只是想快速試,Client() 就行;要連到 server 才用 HttpClient()。」
2. 準備 Embedding 模型
因為我們要做「語意檢索」,需要先把文字轉成向量。這邊用的是多語言的模型(可以自己換別的):
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
3. 幫每一個 chunk 產生唯一 ID
其實我覺得 Chroma 的資料庫就很像是一個圖書館,你要找到那本書肯定是需要靠索書號甚麼的來輔助你尋找,所以我們每一筆的資料都應該幫她貼上一個標籤,為了不讓系統幫你亂亂貼,我們就應該自己設計一個固定的標籤。
所以為了避免重複寫入,我們用「章名 + 條號 + 文字」去算一個 SHA1:
import hashlib
def make_id(c):
# 第 一 章 總則|第 3 條|政府應推動資通安全(範例)
base = c["chapter"] + "|" + c["section_id"] + "|" + " ".join(c["text"].split())
return hashlib.sha1(base.encode("utf-8")).hexdigest()
4. 把所有 chunks 存進去
為了避免他一次把一堆資料全部丟進去給電腦吃,電腦直接炸開,所們要批次丟資料,這邊先設定每次處理 3 筆,畢竟我們資料量不大,但你就視情況調整,如果資料量很多可以考慮設定 128、256,他的結果其實都會一樣,就是速度快慢而已,batch 設小會安全但較慢。
這裡我們會存三種東西:ID(避免重複)、文件內容(chunk 文字)、章/條等欄位(這些是 metadata,用來過濾或顯示)。
另外,在切 chunk 之前記得先做基本清理。法條通常夠乾淨,但還是建議做幾件小事:
batch = 3
buf_ids, buf_docs, buf_meta = [], [], []
把每一條 chunk 暫存起來。
for c in chunks:
buf_ids.append(make_id(c))
buf_docs.append(c["text"])
buf_meta.append({"chapter": c["chapter"], "section_id": c["section_id"]})
# 批次存入資料庫,存好三筆就清空放新的上去
if len(buf_ids) == batch:
emb = model.encode(buf_docs, convert_to_numpy=True, normalize_embeddings=True)
coll.upsert(ids=buf_ids, documents=buf_docs, metadatas=buf_meta, embeddings=emb)
buf_ids, buf_docs, buf_meta = [], [], []
如果還有不到三筆的資料,這邊還是會把他補上
if buf_ids:
emb = model.encode(buf_docs, convert_to_numpy=True, normalize_embeddings=True)
coll.upsert(ids=buf_ids, documents=buf_docs, metadatas=buf_meta, embeddings=emb)
5. 試著提問
我們的法規資料都切好跟存好了,接下來就是要做提問測試看看他的回答。
q = "什麼是關鍵基礎設施?"
q_emb = model.encode([q], normalize_embeddings=True) #先把問題轉成向量
# 用向量資料庫查詢
res = coll.query(
query_embeddings=q_emb,
n_results=5, # 看你要抓相關的幾筆出來
include=["documents", "metadatas", "distances"] # 除了內容我們還會抓的東西
)
下面這邊是我自己在看結果用的,你們也可以測試看看,順帶一提,距離越小代表越相近(越相關)
for i, (doc, meta, dist) in enumerate(zip(res["documents"][0], res["metadatas"][0], res["distances"][0]), 1):
print(f"[{i}] {meta['chapter']} | {meta['section_id']} | 距離={dist:.4f}")
print(doc[:160].replace("\n", " "), "\n---")
放上我這邊的結果照片:
大家跑出來的結果可能不完全一樣,因為 embedding 和檢索會有些差異。
明天我們會開始試試看怎麼跟 LLM 結合,希望今天的內容大家吸收的了,明天繼續努力!