iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
AI & Data

Notion遇上LLM:30天打造我的AI知識管理系統系列 第 27

【Day 27】向量資料庫 Metadata 問題診斷與修正:讓來源追蹤真正運作

  • 分享至 

  • xImage
  •  

Day 26,我們成功實作了對話記憶與來源追蹤功能,但在實測時發現了幾個系統的改進空間,因此今天會針對第一大類問題做修正與優化 -- 修正 Metadata 問題:

  1. 筆記的參考來源顯示異常:Streamlit UI 顯示的都是「⚠️ 未命名筆記 — 未分類」
  2. 缺少筆記原始連結:在 Streamlit UI 無法快速跳轉回 Notion 查看完整筆記
  3. Schema 設計不夠彈性:不同 Notion Database 的 properties 無法妥善儲存
  4. Pipeline 缺少增量更新機制:無法安全地重複執行同步

今天我們會深入分析這些問題的根本原因,並制定完整的修正計畫。

1. 問題診斷與分析

1.1 問題 1:筆記參考來源顯示異常

現象觀察

# Streamlit UI 顯示的結果
📚 參考來源
📄 ⚠️ 未命名筆記 — 未分類
📄 ⚠️ 未命名筆記 — 未分類
📄 ⚠️ 未命名筆記 — 未分類

所有來源都顯示相同的警告訊息,這不可能是巧合。問題一定出在資料流的某個環節。

追溯 Metadata 的生命週期

讓我們追蹤一筆筆記的 metadata 是如何在系統中流動的:

Notion API → SQLite → 文本切分 → Chroma DB → 查詢結果 → Streamlit UI

檢查寫入邏輯 src/embedding/embed_notion_chunks.py

回顧 【Day 16】從 Chunk 到向量:將 Notion 筆記寫入 Chroma DB 的程式碼,我們發現寫入 Chroma DB 時的 metadata 設定過於簡化:

# ChromaDB 架構:只有一個 collection
collection = chroma_client.get_or_create_collection("notion_notes")

chunks = fetch_and_chunk(limit=20)
texts = [c["text"] for c in chunks]
ids = [c["chunk_id"] for c in chunks]

# ❌ 問題:metadata 只有 block_id 和 page_id
metadatas = [
    {
        "block_id": c["block_id"], 
        "page_id": c["page_id"]
    } 
    for c in chunks
]

# 寫入 ChromaDB
collection.add(
    ids=ids,
    documents=texts,
    metadatas=metadatas,  # ← 缺少 Title 和 Category
    embeddings=embeddings
)

這就是問題所在!當 source_tracker.py 嘗試讀取時,因為我們只把 page_id 傳進去,卻沒有把 SQLite 裡的完整頁面資訊(page_namecategory 等)一起帶過去。導致 Chroma DB 的 metadata 不完整。
到了 Streamlit UI 顯示時:

page_name = metadata.get("page_name", "未命名筆記")  # ❌ 拿不到,回傳預設值
category = metadata.get("category", "未分類")        # ❌ 拿不到,回傳預設值

這就是為什麼所有來源都顯示「⚠️ 未命名筆記 — 未分類」的原因。

重構方向:從 SQLite 查詢完整資訊後再寫入 Chroma

# 建立完整的 metadata
metadata = {
    "block_id": chunk["block_id"],
    "page_id": chunk["page_id"],
    "page_name": page_info["page_name"],      # ✅ 加入標題
    "category": extract_category(page_info),   # ✅ 加入分類
    "page_url": page_info["page_url"],         # ✅ 加入連結
}

1.2 問題 2:為什麼沒有原始筆記連結?

現象說明

當我們想在 Streamlit UI 加入「開啟原始筆記」的按鈕時,發現根本拿不到 Notion 的頁面網址:

# Streamlit UI 的嘗試
page_url = metadata.get("page_url")
if page_url:
    st.markdown(f"[🔗 開啟 Notion 原始筆記]({page_url})")
else:
    st.caption("⚠️ 無法取得原始連結")  # ← 總是顯示這個

檢查 SQLite Schema

回到 【Day 9】設計 SQLite Schema:把 Notion JSON 轉成結構化資料 設計的資料表結構:
https://ithelp.ithome.com.tw/upload/images/20251011/20178104ZLD6jFoiht.png
問題很明顯:我們當初設計 Schema 時,根本沒有規劃在notion_pages 儲存 URL 的欄位。既然我們的Schema 沒有 URL,在 【Day 11】把 Notion JSON 寫入 SQLite:建立可查詢的筆記資料庫 的寫入函數自然也沒有處理 URL。

問題鏈分析**

每個環節都沒有設計處理 URL 的邏輯,若我們需要在 Streamlit UI 顯示 Notion 筆記URL,就必須依循資料流逐一加上。

Notion API ──✅ 有 URL──> [沒有擷取] 
                           ↓
SQLite ──────❌ 無欄位──> [沒有儲存]
                           ↓
Chroma DB ───❌ 無資料──> [無法傳遞]
                           ↓
Streamlit UI ❌ 無法顯示

重構方向:在 Schema 新增 page_url 欄位

-- 擴充後的 Schema(概念)
CREATE TABLE notion_pages (
    page_id TEXT PRIMARY KEY,
    page_name TEXT NOT NULL,
    page_url TEXT,              -- ✅ 新增:Notion 頁面連結
    database_id TEXT,
    created_time TEXT,
    last_edited_time TEXT,
    FOREIGN KEY (database_id) REFERENCES notion_databases(database_id)
);

同時需要修改:

  1. Notion API 擷取邏輯:從 response["url"] 取得 URL
  2. SQLite 寫入邏輯:將 URL 寫入資料庫
  3. Chroma 寫入邏輯:將 URL 加入 metadata
  4. Streamlit UI:顯示可點擊的連結

1.3 問題 3:不同 Notion Database 的 Properties 無處存放

現實世界的複雜性

當我們的 Notion 工作區逐漸成長,會建立各種不同用途的 Database,舉例來說:

範例 1 - 【Day 5】實作抓取 Notion Database:以釜山旅遊行程表為例

  • Property 有:TitleDay行程類別...等
    https://ithelp.ithome.com.tw/upload/images/20251011/20178104xrPvEROQzO.png

範例 2 - 【Day 6】實作抓取 Notion Page / Block:以 Python Basic 筆記為例

  • Property 有:TitleStatus
    https://ithelp.ithome.com.tw/upload/images/20251011/20178104LtBPRSuPAZ.png

範例 3 - 【Day 21】用 n8n 實作多來源電子報整合:Gmail × OpenAI × Notion:打造多線並行的 AI 知識中樞

  • Property 有:TitleStatusCategory...等
    https://ithelp.ithome.com.tw/upload/images/20251011/20178104FolGhnHYi7.png

當然不只這些範例,因此我們需要更具彈性的作法來收攏不同 Notion Database 的 Property。

固定欄位的困境

我們目前在 notion_pages 的 Schema 設計是固定欄位,如果要為不同的 Notion Database 都加上對應的 property column 會導致:

  1. Schema 爆炸:每新增一個 Database 就要修改資料表結構
  2. 資料稀疏:大部分欄位對大部分記錄都是 NULL
  3. 維護困難:需要頻繁執行 ALTER TABLE 來新增欄位
  4. 查詢混亂:不同類型的資料混在同一張表,查詢邏輯變複雜

試圖用多張表解決

有人可能會想:「那我為每個 Database 建一張表不就好了?」

-- 為每個 Database 建專屬的表
CREATE TABLE programming_notes (...);
CREATE TABLE project_management (...);
CREATE TABLE daily_logs (...);

但這會帶來新問題:

  1. 程式碼重複:每個表都要寫一套 CRUD 邏輯
  2. 擴充性差:使用者新增 Database 時,需要改程式碼建新表
  3. 統一查詢困難:要跨所有筆記搜尋時,需要 UNION 多張表

重構方向:使用 JSON 欄位儲存動態 Properties

-- 彈性的 Schema 設計(概念)
CREATE TABLE notion_pages (
    page_id TEXT PRIMARY KEY,
    page_name TEXT NOT NULL,
    page_url TEXT,
    page_properties TEXT,  -- ✅ JSON 格式儲存所有 properties
    database_id TEXT,
    created_time TEXT,
    last_edited_time TEXT
);

1.4 問題 4:Pipeline 加入增量更新機制

為什麼需要增量更新?

目前的 Pipeline 有個隱藏問題:如果重複執行,會如何處理已存在的資料?

# 現有的寫入邏輯(Day 11)
def insert_notion_page(conn, page_id, page_name, ...):
    cursor.execute("""
        INSERT INTO notion_pages (page_id, page_name, ...)
        VALUES (?, ?, ...)
    """, (page_id, page_name, ...))
    # ❌ 如果 page_id 已存在會報錯:UNIQUE constraint failed

這會導致:

  1. 無法重新同步已存在的筆記
  2. 筆記更新後無法反映到資料庫
  3. 每次都要先手動清空資料庫才能重新匯入

重構方向: Delete + Insert 策略

採用「先刪後插」的策略,確保資料的唯一性與最新性。

# ✅ 改進後的概念
def upsert_notion_page(conn, page_data):
    """
    使用 Delete + Insert 策略更新頁面
    確保每次都是最新、完整的資料
    """
    cursor = conn.cursor()
    
    # Step 1: 刪除舊資料(如果存在)
    cursor.execute(
        "DELETE FROM notion_pages WHERE page_id = ?", 
        (page_data["page_id"],)
    )
    
    # Step 2: 插入新資料
    cursor.execute("""
        INSERT INTO notion_pages 
        (page_id, page_name, page_url, page_properties, ...)
        VALUES (?, ?, ?, ?, ...)
    """, (...))
    
    conn.commit()

2. 修正計畫總覽

基於以上的問題分析,我們制定了完整的修正計畫:

2.1 SQLite Schema 擴充

  • 目標: 讓資料庫能儲存完整的頁面資訊
  • 需要的變更:
    -- 新增欄位
    ALTER TABLE notion_pages ADD COLUMN page_url TEXT;
    ALTER TABLE notion_pages ADD COLUMN page_properties TEXT;
    
    -- 建立索引(提升查詢效能)
    CREATE INDEX idx_page_name ON notion_pages(page_name);
    CREATE INDEX idx_database_id ON notion_pages(database_id);
    
    -- 安全性考量:
    -- 1. 執行前先備份資料庫
    -- 2. 驗證遷移結果
    -- 3. 提供回滾機制
    

2.2 Notion API 擷取邏輯更新

  • 目標: 從 Notion API 取得完整的頁面資訊
  • 需要的變更:
# 現在:只取基本資訊
page_data = {
    "page_id": response["id"],
    "page_name": extract_title(response),
    "database_id": response["parent"]["database_id"],
}

# 改為:取得完整資訊
page_data = {
    "page_id": response["id"],
    "page_name": extract_title(response),
    "page_url": response["url"],  # ✅ 新增
    "database_id": response["parent"]["database_id"],
    "properties": extract_all_properties(response["properties"]),  # ✅ 新增
    "created_time": response["created_time"],
    "last_edited_time": response["last_edited_time"]
}

2.3 SQLite 寫入邏輯重構

  • 目標:支援增量更新,確保資料完整性
  • 需要的變更:
    1. 將 insert_notion_page() 改為 upsert_notion_page()
    2. 實作 Delete + Insert 邏輯
    3. 加入交易處理確保原子性
    4. 加入錯誤處理和日誌

2.4 Chroma DB 寫入邏輯改進

  • 目標: 寫入向量時附帶完整的 metadata
  • 需要的變更:
    # 現在:只有基本 metadata
    metadata = {
        "block_id": chunk["block_id"],
        "page_id": chunk["page_id"]
    }
    
    # 改為:從 SQLite 查詢完整資訊
    page_info = query_page_info_from_sqlite(chunk["page_id"])
    metadata = {
        "block_id": chunk["block_id"],
        "page_id": chunk["page_id"],
        "page_name": page_info["page_name"],      # ✅ 新增
        "page_url": page_info["page_url"],        # ✅ 新增
        "category": extract_category(page_info),  # ✅ 新增
        "database_id": page_info["database_id"],
    }
    

2.5 Streamlit UI 優化

  • 目標: 正確顯示來源資訊與原始連結
  • 需要的變更:
# 現在:顯示預設值
page_name = metadata.get("page_name", "未命名筆記")
category = metadata.get("category", "未分類")

# 改為:顯示實際資訊 + 連結
page_name = metadata.get("page_name", "未命名筆記")
category = metadata.get("category", "未分類")
page_url = metadata.get("page_url")

with st.expander(f"📄 {page_name} — {category}"):
    st.markdown(f"> {doc[:300]}...")
    
    if page_url:
        st.markdown(f"[🔗 開啟 Notion 原始筆記]({page_url})")
    
    st.caption(f"建立時間: {metadata.get('created_time', 'N/A')[:10]}")

小結與下篇預告

今天我們深入診斷了系統中的四個關鍵問題,找出了每個問題的根本原因,並制定了完整的修正計畫。這些問題環環相扣,反映出在初期設計時低估了 Notion 資料的複雜性與動態性:

  1. 筆記參考來源顯示異常:Chroma DB 缺少完整的 metadata
  2. 缺少筆記原始連結:SQLite Schema 沒有 URL 欄位
  3. notion_pages Schema 不夠彈性:固定欄位無法應對動態的 properties
  4. Pipeline 缺少增量更新:無法安全地重複執行同步

明天(Day 28),我將按照今天制定的計畫,逐步帶大家實作所有的修正。從 Schema 遷移開始,一路修改到 Streamlit UI,讓 metadata 在整個系統中完整流動!


上一篇
【Day 26】用 Streamlit 打造會記憶的 AI 助理:對話記憶 × 來源追蹤實作
系列文
Notion遇上LLM:30天打造我的AI知識管理系統27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言