目前你的查詢流程是純向量搜尋:
問題 → embedding → 找最近的 chunks
問題是:如果用戶問「前端要怎麼防 XSS?」,向量搜尋可能回傳後端相關的 chunk。
加入 metadata 後可以做 預過濾(pre-filter):
-- 先過濾 stack=frontend,再做向量相似度排序
SELECT * FROM chunks
WHERE stack = 'frontend'
ORDER BY embedding <=> $1
LIMIT 5;
這是 RAG 系統中最重要的優化模式之一(D20 會實作完整版,D12 先把資料打好標籤)。
目前你的 chunks 表只有 chunk_text,沒有 metadata。需要在 init_db() 裡加:
ALTER TABLE chunks ADD COLUMN IF NOT EXISTS category TEXT; -- A01~A10
ALTER TABLE chunks ADD COLUMN IF NOT EXISTS stack TEXT; -- frontend / backend / both
documents 表已有 category 欄位(D11 你已做好),但 chunks 要繼承下來,每個 chunk 都要帶著它父 document 的 metadata,這樣過濾才有意義。
你的 main.py 已經爬出 category: "A01" 這個欄位了,ingestion 也在 upsert_document() 裡存入 documents.category。
D12 要做的是:在 upsert_chunk() 時,把 document 的 category 一起寫入 chunk。這個部分相對簡單,主要是學習「父子資料繼承」的設計概念。
這是 D12 最有趣的地方。你需要設計一個函數,根據 chunk 的文字內容自動判斷是 frontend / backend / both。
方法:關鍵字規則(不用 LLM,快又便宜)
FRONTEND_KEYWORDS = ["javascript", "browser", "dom", "html", "react", "csrf token", "csp", "cookie", "xss"]
BACKEND_KEYWORDS = ["server", "api", "sql", "database", "injection", "authentication", "session", "jwt", "python", "java"]
def classify_stack(text: str) -> str:
t = text.lower()
is_fe = any(kw in t for kw in FRONTEND_KEYWORDS)
is_be = any(kw in t for kw in BACKEND_KEYWORDS)
if is_fe and is_be: return "both"
if is_fe: return "frontend"
if is_be: return "backend"
return "both" # 預設:兩者都適用
學習點:Rule-based vs LLM-based 的取捨
你的資料來源是 A01–A10,可以直接用 category 建立對應表增加語意:
| Category | 主要 Stack |
|---|---|
| A01 Broken Access Control | both |
| A02 Cryptographic Failures | both |
| A03 Injection (SQL/XSS) | both |
| A07 XSS | frontend |
| A08 Insecure Deserialization | backend |
這讓你認識到:一個知識庫的品質取決於資料打標的細緻度,不只是 embedding 好不好。
你現在的 upsert_chunk() 用 chunk_hash 做 ON CONFLICT DO NOTHING,這設計很好。D12 加 metadata 時要注意:如果 hash 相同但 stack 標籤邏輯改了,舊資料不會自動更新。
解法選項:
ON CONFLICT DO UPDATE SET stack = EXCLUDED.stack
make reindex-force 指令,清除後重建你的 chunks 表會長這樣:
| chunk_id | document_id | chunk_text | category | stack |
|---|---|---|---|---|
| 1 | 1 | "Use parameterized queries..." | A03 | backend |
| 2 | 1 | "Validate all input on client side..." | A03 | frontend |
| 3 | 5 | "Ensure HTTPS is enforced..." | A02 | both |
stack 欄位到 chunks 表make pipeline-local 驗證不需要修改 crawler,因為 category 資料已在 raw JSON 裡了。
本日實作
在 ingestion/main.py 修改建立chunks table時加上category和stack欄位
之前就有定義category了(A1 - A10)
主要的更新是在新增chunk時透過關鍵字判斷stack是 前端或後端 或兩者都是
FRONTEND_KEYWORDS = [
"javascript", "browser", "dom", "html", "react", "angular", "vue",
"csrf token", "csp", "content security policy", "cookie", "xss",
"client-side", "client side", "front-end", "frontend",
]
BACKEND_KEYWORDS = [
"server", "api", "sql", "database", "injection", "authentication",
"session", "jwt", "python", "java", "node", "back-end", "backend",
"query", "parameterized", "stored procedure", "orm",
]
def classify_stack(text: str) -> str:
t = text.lower()
is_fe = any(kw in t for kw in FRONTEND_KEYWORDS)
is_be = any(kw in t for kw in BACKEND_KEYWORDS)
if is_fe and is_be:
return "both"
if is_fe:
return "frontend"
if is_be:
return "backend"
return "both" # 預設:無明確關鍵字時視為兩者皆適用
主要更新commit:
因為向量化的job有更新 需要取代舊有的image
gcloud builds submit ingestion --tag gcr.io/chat-bot-484615/bank-vectorize:latest --project=chat-bot-484615
驗證 - chunks table有新增category 和 stack欄位
gcp去到 cloud sql 選擇建立的instance 以我的專案來說是 'bank-ai-db-instance’
從左側欄選擇 cloud sql studio

會經過user, password詢問 以專案的設定來說可以參考 terraform state裡的
DB_USER, DB_PASSWORD
登入後選擇可以輸入query的介面
使用query 查詢chunks的欄位
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'chunks'
ORDER BY ordinal_position;

可以看到category, stack欄位出現了
這裡使用gcp的cloud sql studio是因為 cloud sql是private ip
無法直接透過本地terminal的cloud sdk操作 所以進入gcp裡使用studio介面
額外心得
今天在分類知識時是透過keyword, 而不用經過LLM判斷, 會比較省費用, 不過假如內容是動態的, 那可能keyword判斷效果會不如LLM判斷
ps. 如果忘記先更新job檔而沒看到新增欄位 也可以在studio用query刪除table
更新job後
然後在本地再跑一次 make reindex 重建table