Ian Chou's Blog

用 LanceDB 儲存 JD 分析:避開 Arrow 巢狀結構的陷阱

用 LanceDB 儲存 JD 分析:避開 Arrow 巢狀結構的陷阱

在建立履歷知識庫系統時,我們需要將 JD(職位描述)的分析結果結構化儲存。本文分享使用 LanceDB + Python 實作 save-jd 功能時的設計決策與踩坑經驗。

問題背景

我們的系統需要:

  1. 接收使用者貼上的 JD 文字
  2. 用 LLM 提取結構化資訊(技能、職責、資歷等級)
  3. 儲存至本地向量資料庫,供後續履歷匹配使用

聽起來很簡單,但 LanceDB(基於 Apache Arrow)對巢狀資料結構有特殊限制。

Schema 設計

# jd_analysis 表結構
{
    "id": str,              # UUID 主鍵
    "company": str,         # 公司名稱
    "position": str,        # 職位名稱
    "raw_jd": str,          # 原始 JD 文字
    "focus_items": str,     # ⚠️ JSON 字串(非 object array)
    "required_skills": list[str],  # 技能清單
    "seniority_level": str, # Junior/Mid/Senior/Lead
    "domain": str,          # 產業領域
    "status": str,          # applied/interviewed/rejected/offered
    "created_at": str,      # ISO 時間戳記
}

關鍵設計:focus_items 序列化

原本我們想這樣儲存 focus_items

# ❌ 這樣會出問題
record["focus_items"] = [
    {
        "description": "distributed systems ownership",
        "skill_tags": ["Kafka", "AWS"],
        "responsibility": "設計分散式系統",
        "seniority_signal": "5+ 年經驗"
    },
    ...
]

但 LanceDB/Arrow 對 巢狀 object array 的處理有問題,會導致:

解決方案:JSON 字串化

# ✅ 正確做法:序列化為 JSON 字串
record["focus_items"] = json.dumps([
    {
        "description": "distributed systems ownership",
        "skill_tags": ["Kafka", "AWS"],
        ...
    }
])

讀取時再反序列化:

focus_items = json.loads(record["focus_items"])

核心實作流程

使用者輸入 JD
      ↓
┌─────────────────────────┐
│ 1. 產生 UUID            │
│    id = uuid.uuid4()    │
└─────────────────────────┘
      ↓
┌─────────────────────────┐
│ 2. LLM 提取結構化資訊   │
│    - focus_items        │
│    - required_skills    │
│    - seniority_level    │
│    - domain             │
└─────────────────────────┘
      ↓
┌─────────────────────────┐
│ 3. 序列化 focus_items   │
│    json.dumps([...])    │
└─────────────────────────┘
      ↓
┌─────────────────────────┐
│ 4. 補上 metadata        │
│    - status: "applied"  │
│    - created_at: now()  │
└─────────────────────────┘
      ↓
┌─────────────────────────┐
│ 5. 寫入 LanceDB         │
│    table.add([record])  │
└─────────────────────────┘
      ↓
回傳 { id: "<uuid>" }

Python 實作

import json
import uuid
from datetime import datetime

def save_jd(company: str, position: str, raw_jd: str, auto_extract: bool):
    # 1. 準備基本記錄
    record = {
        "id": str(uuid.uuid4()),
        "company": company,
        "position": position,
        "raw_jd": raw_jd,
        "focus_items": "[]",  # 預設空 JSON 陣列字串
        "required_skills": [],
        "seniority_level": "Mid",
        "domain": "",
        "status": "applied",
        "created_at": datetime.now().isoformat(),
    }
    
    # 2. AI 提取(可選)
    if auto_extract:
        analysis = extract_jd_analysis(raw_jd, company, position)
        # 3. 關鍵:序列化 focus_items
        record["focus_items"] = json.dumps([
            item.model_dump() for item in analysis.focus_items
        ])
        record["required_skills"] = analysis.required_skills
        record["seniority_level"] = analysis.seniority_level
        record["domain"] = analysis.domain
    
    # 4. 寫入 LanceDB
    db = lancedb.connect("data/lancedb")
    if "jd_analysis" in db.table_names():
        table = db.open_table("jd_analysis")
        table.add([record])
    else:
        db.create_table("jd_analysis", [record])
    
    # 5. 回傳 ID
    return {"id": record["id"]}

LLM 提取 Prompt

prompt = f"""Analyze this job description and extract:
1. Focus items (key responsibilities and requirements)
2. Required skills (technical and soft skills)
3. Seniority level (Junior/Mid/Senior/Lead)
4. Domain (e.g., fintech, e-commerce, SaaS)

Company: {company}
Position: {position}

JD:
{raw_jd}

Respond in JSON format with:
- focus_items: array of {description, skill_tags[], responsibility, seniority_signal}
- required_skills: array of skill names
- seniority_level: one of Junior/Mid/Senior/Lead
- domain: string
"""

CLI 使用方式

# 儲存 JD 並自動提取
uv run career-kb save-jd \
  --company "Google" \
  --position "AI PM" \
  --jd-file "./jd.txt" \
  --auto-extract

# 輸出
# ✓ Saved JD analysis: a1b2c3d4-5678-90ab-...
#   Company: Google
#   Position: AI PM
#   Seniority: Senior
#   Skills: Python, Kubernetes, Leadership, ...

後續流程

儲存 JD 後,可以用回傳的 id 進行:

# 驗證技能覆蓋
uv run career-kb verify --jd-id "a1b2c3d4-..." --with-strategy

# 生成客製化履歷
uv run career-kb generate --jd-id "a1b2c3d4-..."

經驗總結

問題 解決方案
Arrow 不支援巢狀 object array 將複雜結構序列化為 JSON 字串
Schema 推斷錯誤 使用簡單型別 (str, list[str])
讀取時型別問題 讀取後 json.loads() 反序列化

適用情境

這個模式適用於:

不適用情境

如果需要:

則應考慮:


Career Knowledge Base 是一個本地優先的履歷知識庫系統,使用 Python + LanceDB + Voyage AI 建構。