開發日誌 #2:向量化與檢索系統建置

完成智能分片後,下一步是將文本轉換為向量,並建立高效的檢索系統。這個階段的核心任務是:

  1. 選擇合適的 Embedding 模型
  2. 整合 ChromaDB 向量資料庫
  3. 實現高效的語義檢索

Embedding 模型選擇

需求分析

我們的需求很明確:

  • 中文優化:招標文件都是繁體中文,需要專為中文設計的模型
  • 模型輕量:希望能在一般電腦上跑,不需要高階 GPU
  • 檢索效果:準確率要高,能理解專業術語
  • 本地部署:資料安全,不能外洩

模型評估

經過調研,我選定了 BAAI/bge-small-zh-v1.5

模型向量維度模型大小適用場景
bge-small-zh-v1.5512~100MB⭐ 中小型應用,平衡性能與效率
bge-base-zh-v1.5768~400MB更高準確率,需要更多資源
bge-large-zh-v1.51024~1GB最高準確率,需要 GPU

選擇理由

  • 專為中文優化,支援繁體中文
  • 模型較小(約 100MB),載入快速
  • 向量維度適中(512),儲存空間友善
  • CPU 即可運行,無需 GPU

模型測試

先做個簡單的相似度測試:

from sentence_transformers import SentenceTransformer
 
model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
 
# 測試文本
text1 = "數位雙生智能火場鑑識輔助系統"
text2 = "智慧科技消防全民防救災整備研發計畫"
 
# 生成向量
emb1 = model.encode(text1)
emb2 = model.encode(text2)
 
# 計算相似度
from sklearn.metrics.pairwise import cosine_similarity
similarity = cosine_similarity([emb1], [emb2])[0][0]
 
print(f"相似度: {similarity:.4f}")

結果

相似度: 0.5867

不錯!雖然兩個文本字面上不同,但模型能理解它們都與消防系統相關。

ChromaDB 整合

為什麼選擇 ChromaDB?

市面上的向量資料庫很多(Pinecone, Weaviate, Qdrant, Milvus…),我選擇 ChromaDB 的原因:

簡單易用:API 設計直觀,幾行程式碼就能上手 ✅ 內建持久化:自動儲存到磁碟,重啟後無需重新載入 ✅ Python 原生:完美整合 Python 生態系 ✅ 適合中小型專案:對於我們的文檔量(數百到數千個分片)效能足夠

向量化流程設計

def embed_to_vectordb(chunks_folder: str):
    """將分片向量化並存入 ChromaDB"""
 
    # 1. 載入分片數據
    with open(f"{chunks_folder}/chunks.json", 'r', encoding='utf-8') as f:
        chunks = json.load(f)
 
    print(f"📦 載入 {len(chunks)} 個分片")
 
    # 2. 載入 Embedding 模型
    print("🔄 載入 Embedding 模型...")
    model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
 
    # 3. 批次生成向量
    print("🔢 生成向量...")
    texts = [chunk['text'] for chunk in chunks]
    embeddings = model.encode(texts, batch_size=32, show_progress_bar=True)
 
    # 4. 建立 ChromaDB 集合
    client = chromadb.PersistentClient(path=f"{chunks_folder}/vectordb")
    collection_name = f"collection_{uuid.uuid4().hex[:16]}"
    collection = client.create_collection(name=collection_name)
 
    # 5. 批次插入向量
    print("💾 存入向量資料庫...")
    collection.add(
        embeddings=embeddings.tolist(),
        documents=texts,
        metadatas=[chunk['metadata'] for chunk in chunks],
        ids=[chunk['chunk_id'] for chunk in chunks]
    )
 
    print("✅ 向量化完成!")

實際執行結果

$ python embed_to_vectordb.py "rag/01 華電聯網服務建議書_20251012_124248"
 
📦 載入 360 個分片
🔄 載入 Embedding 模型...
   模型載入完成 (BAAI/bge-small-zh-v1.5)
 
🔢 生成向量...
Batches: 100%|████████████| 12/12 [00:11<00:00,  1.05batch/s]
 
💾 存入向量資料庫...
   ├─ 集合名稱: collection_a7f3e9c2d8b14f56
   ├─ 向量數量: 360
   ├─ 向量維度: 512
   └─ 儲存路徑: rag/.../vectordb/
 
 向量化完成!
⏱️  總耗時: 12.3

技術難題與解決

難題 3:ChromaDB 集合名稱限制

問題描述

一開始我想用原始檔名作為集合名稱:

collection_name = "rag_01 華電聯網服務建議書_20251012_124248"
collection = client.create_collection(name=collection_name)

結果報錯:

ValueError: Expected collection name that matches pattern:
[a-zA-Z0-9._-]{3,63}

Got: rag_01 華電聯網服務建議書_20251012_124248

ChromaDB 不支援中文和空格!

解決方案

使用 UUID 作為集合名稱,原始名稱存在 metadata:

import uuid
 
# 生成 UUID 作為集合名稱
collection_name = f"collection_{uuid.uuid4().hex[:16]}"
 
# 原始名稱存在 metadata
collection = client.create_collection(
    name=collection_name,
    metadata={
        "original_name": "rag_01 華電聯網服務建議書_20251012_124248",
        "source_file": "doc/01 華電聯網服務建議書.pdf",
        "created_at": datetime.now().isoformat()
    }
)
 
# 記錄 UUID 供後續查詢使用
with open(f"{chunks_folder}/vectordb_info.txt", 'w', encoding='utf-8') as f:
    f.write(f"Collection UUID: {collection_name}\n")
    f.write(f"Original Name: rag_01 華電聯網服務建議書_20251012_124248\n")

難題 4:Metadata 不能有 None 值

問題描述

插入向量時出現錯誤:

TypeError: Failed to extract enum MetadataValue
'NoneType' object cannot be converted to 'PyString'

原因是某些分片的 headingpage_number 可能是 None,而 ChromaDB 不接受 None 值。

解決方案

在插入前過濾掉 None 值:

def clean_metadata(metadata: dict) -> dict:
    """清理 metadata,移除 None 值"""
    return {k: v for k, v in metadata.items() if v is not None}
 
# 使用時
metadatas = [clean_metadata(chunk['metadata']) for chunk in chunks]
 
collection.add(
    embeddings=embeddings.tolist(),
    documents=texts,
    metadatas=metadatas,
    ids=[chunk['chunk_id'] for chunk in chunks]
)

或者更優雅的方式,在建立 metadata 時就只添加非 None 的欄位:

metadata = {}
if chunk_metadata.get('section'):
    metadata['section'] = chunk_metadata['section']
if chunk_metadata.get('heading'):
    metadata['heading'] = chunk_metadata['heading']
if chunk_metadata.get('page_number'):
    metadata['page_number'] = chunk_metadata['page_number']

檢索系統實現

查詢接口設計

def query_vectordb(chunks_folder: str, query: str, top_k: int = 5):
    """查詢向量資料庫"""
 
    # 1. 載入資料庫
    client = chromadb.PersistentClient(path=f"{chunks_folder}/vectordb")
 
    # 2. 讀取 collection UUID
    with open(f"{chunks_folder}/vectordb_info.txt", 'r', encoding='utf-8') as f:
        collection_name = f.readline().split(': ')[1].strip()
 
    collection = client.get_collection(name=collection_name)
 
    # 3. 載入 Embedding 模型
    model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
 
    # 4. 生成查詢向量
    query_embedding = model.encode(query)
 
    # 5. 執行檢索
    results = collection.query(
        query_embeddings=[query_embedding.tolist()],
        n_results=top_k
    )
 
    # 6. 格式化輸出
    return format_results(results)

檢索效果測試

查詢問題:「數位雙生智能火場鑑識輔助系統的架構是什麼」

檢索結果

🔍 查詢: 數位雙生智能火場鑑識輔助系統的架構是什麼
📊 找到 5 個相關結果

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【結果 1】相似度: 0.6748
章節: 二. 數位雙生智能火場鑑識輔助系統
標題: 系統架構說明
頁碼: 45

內容:
本系統採用多層架構設計,包含:
1. 使用者介面層 - 提供 Web-based 操作介面
2. 應用服務層 - 使用 ASP.NET Core 建置 RESTful API
3. 資料存取層 - 整合多種資料庫系統
4. 基礎設施層 - 包含雲端服務與資料治理機制

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【結果 2】相似度: 0.6733
章節: 壹、專案說明 > 一. 本專案規劃重點摘要
標題: (一) 系統建置概述

內容:
運用數位雙生(Digital Twin)技術建置火場智能鑑識輔助系統,
整合 3D 建模、IoT 感測、AI 分析等技術...

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【結果 3】相似度: 0.6255
章節: 參、專案需求規劃 > 一. 整體系統架構

內容:
本系統以「數位雙生智能火場鑑識輔助系統」平台為基礎,
採用微服務架構,各模組間透過 API Gateway 進行通訊...

效果評估

準確率高:前三個結果都直接命中「系統架構」相關內容 ✅ 語義理解:即使問題用「架構是什麼」,也能找到「系統架構說明」 ✅ 章節定位:完整的章節路徑讓用戶能快速找到原文位置 ✅ 相似度合理:0.67 的相似度顯示高度相關

互動式查詢模式

為了方便測試,我還加入了互動式查詢:

def interactive_query(chunks_folder: str):
    """互動式查詢模式"""
 
    print("🤖 進入互動式查詢模式(輸入 'exit' 離開)\n")
 
    while True:
        query = input("🔍 查詢 > ").strip()
 
        if query.lower() == 'exit':
            print("👋 再見!")
            break
 
        if not query:
            continue
 
        results = query_vectordb(chunks_folder, query, top_k=5)
        display_results(results)
        print()

使用示範

$ python query_vectordb.py "rag/01 華電聯網服務建議書_20251012_124248"
 
🤖 進入互動式查詢模式(輸入 'exit' 離開)
 
🔍 查詢 > 系統架構
📊 找到 5 個相關結果
[顯示結果...]
 
🔍 查詢 > 專案團隊成員
📊 找到 5 個相關結果
[顯示結果...]
 
🔍 查詢 > exit
👋 再見!

性能分析

向量化性能

指標數據
分片數量360 個
Batch Size32
總批次數12 批次
單批次耗時~1 秒
總耗時~12 秒
平均速度~30 chunks/秒

檢索性能

指標數據
向量維度512
資料庫大小~5 MB
查詢耗時~1 秒
Top-K5
相似度計算Cosine Similarity

資源使用

資源使用量
記憶體~1.5 GB (含模型)
磁碟空間~100 MB (模型) + ~5 MB (資料庫)
CPUIntel i5 即可
GPU非必需

數據持久化

向量化完成後的檔案結構:

rag/01 華電聯網服務建議書_20251012_124248/
├── chunks.json              # 原始分片數據
├── chunks/                  # 分片文字檔
├── vectordb/                # ⭐ ChromaDB 資料庫
│   ├── chroma.sqlite3       # SQLite 資料庫
│   └── [其他 ChromaDB 檔案]
└── vectordb_info.txt        # ⭐ 資料庫資訊

vectordb_info.txt 內容

Collection UUID: collection_a7f3e9c2d8b14f56
Original Name: rag_01 華電聯網服務建議書_20251012_124248
Source File: doc/01 華電聯網服務建議書.pdf
Created At: 2025-10-12T14:32:15
Vector Count: 360
Vector Dimension: 512
Model: BAAI/bge-small-zh-v1.5

這樣即使重新啟動,也能快速載入資料庫進行查詢,無需重新向量化。

階段總結

第二階段完成了向量化與檢索系統:

完成項目

  • Embedding 模型選擇與測試
  • ChromaDB 整合與持久化
  • 高效的語義檢索系統
  • 互動式查詢介面

技術突破

  • 解決 ChromaDB 中文命名限制
  • 處理 metadata None 值問題
  • 實現高效批次向量化
  • 完整的持久化方案

📊 性能表現

  • 360 個分片向量化:~12 秒
  • 查詢回應時間:~1 秒
  • 檢索準確率:優秀

🎯 下一步: 整合本地 LLM (Ollama),實現完整的 RAG 問答系統。


← 上一篇:專案初始化與智能分片引擎 | 返回專案主頁 | 下一篇:LLM 整合與完整 RAG 系統 →