Ian Chou's Blog

履歷素材入庫:精確去重與相似變體檢測

履歷素材入庫:精確去重與相似變體檢測

當你持續累積履歷素材時,如何避免重複?如何處理「幾乎一樣」的變體版本?本文分享 ingest 命令的去重策略與實作細節。

問題場景

隨著時間累積,你的素材庫會遇到:

  1. 無意間貼入重複內容 - 同一段文字入庫兩次
  2. 微調版本過多 - 同一個案例的 5 個不同寫法
  3. 搜尋結果充斥相似項 - 降低有效資訊密度

我們需要一個多層次去重策略來處理這些情況。

去重策略設計

輸入素材
    ↓
┌────────────────────────┐
│ Layer 1: 精確去重      │  text_hash (SHA256)
│ 完全相同 → 拒絕入庫    │
└────────────────────────┘
    ↓ 不同
┌────────────────────────┐
│ Layer 2: 相似檢測      │  cosine similarity ≥ 0.94
│ embedding 向量比對     │  AND
│ + 技能標籤重疊度       │  skills overlap ≥ 0.5
└────────────────────────┘
    ↓ 相似         ↓ 不相似
┌──────────────┐  ┌──────────────┐
│ 標記為變體   │  │ 正常入庫     │
│ parent_id    │  │              │
│ pending_review│  │              │
└──────────────┘  └──────────────┘

實作細節

Layer 1: 精確去重 (text_hash)

對文字做正規化後計算 SHA256:

def normalize_text(text: str) -> str:
    """正規化:小寫 + 壓縮空白"""
    return " ".join(text.lower().split())

def compute_text_hash(text: str) -> str:
    normalized = normalize_text(text)
    return hashlib.sha256(normalized.encode()).hexdigest()

為什麼用 SHA256 而非直接比對?

  1. 效率:32 bytes hash vs 數百 bytes 文字
  2. 索引友善:可以在 hash 欄位建立索引
  3. 正規化處理:忽略空白差異

Layer 2: 相似檢測

兩個條件同時滿足才判定為「相似」:

COSINE_SIMILARITY_THRESHOLD = 0.94  # 語意相似度
SKILLS_OVERLAP_THRESHOLD = 0.5      # 技能重疊度

2.1 Cosine Similarity

使用 embedding 向量計算語意相似度:

def distance_to_cosine_similarity(distance: float) -> float:
    """LanceDB L2 距離轉換為 cosine similarity"""
    # 對於正規化向量:cosine_sim ≈ 1 - (distance² / 2)
    return max(0.0, 1.0 - (distance ** 2) / 2)

# 搜尋 Top-5 相似項
similar_results = table.search(vector).limit(5).to_list()

for sim in similar_results:
    cosine_sim = distance_to_cosine_similarity(sim["_distance"])
    # 0.94 → 非常相似但不完全相同

為什麼是 0.94?

相似度 意義
1.00 完全相同(但通常會被 text_hash 攔截)
0.98 只改了幾個詞
0.94 同一件事的不同寫法
0.85 相關但不同的案例
0.70 主題相近

2.2 Skills Overlap

單靠向量相似度可能誤判,加入技能標籤驗證:

def compute_skills_overlap(skills1: list[str], skills2: list[str]) -> float:
    """Jaccard 係數:交集 / 聯集"""
    set1 = {s.lower() for s in skills1}
    set2 = {s.lower() for s in skills2}
    
    intersection = len(set1 & set2)
    union = len(set1 | set2)
    
    return intersection / union if union > 0 else 0.0

範例

素材 A: ["Python", "RAG", "LangChain"]
素材 B: ["Python", "RAG", "FastAPI"]

交集 = {"python", "rag"} = 2
聯集 = {"python", "rag", "langchain", "fastapi"} = 4
重疊度 = 2/4 = 0.5 ✓ 達門檻

變體處理

當判定為相似時,不是拒絕入庫,而是:

if cosine_sim >= 0.94 and skills_overlap >= 0.5:
    dedupe_action = "similar_variant"
    parent_id = similar["id"]           # 關聯到原始素材
    status = "pending_review"           # 需要人工審核

這樣做的好處:

  1. 保留變體 - 不同寫法可能各有優點
  2. 建立關聯 - 知道哪些是同一案例的不同版本
  3. 人工決定 - 讓使用者選擇保留哪個

回傳結構

{
  "status": "pending_review",
  "id": "new-uuid",
  "dedupe_action": "similar_variant",
  "parent_id": "original-uuid",
  "existing_id": "original-uuid",
  "text_hash": "sha256...",
  "skills": ["Python", "RAG"],
  "type": "project",
  ...
}

dedupe_action 類型

意義 處理方式
exact_match 精確重複 拒絕入庫
similar_variant 相似變體 入庫但標記 parent_id
none 全新素材 正常入庫

CLI 使用

# 入庫新素材
uv run career-kb ingest \
  --text "Built RAG system with 95% accuracy" \
  --type project \
  --skills "RAG,Python,LangChain" \
  --role Lead \
  --impact 4

# 輸出
# ✓ Added material: abc123
#   Status: pending_review

# 嘗試入庫相似內容
uv run career-kb ingest \
  --text "Built a RAG pipeline achieving 95% retrieval accuracy" \
  --type project \
  --skills "RAG,Python" \
  --role Lead \
  --impact 4

# 輸出
# Similar material found (cosine: 0.96, skills overlap: 0.67)
# Marking as variant of: abc123
# ✓ Added as variant: def456
#   Parent: abc123
#   Status: pending_review

進階應用

查詢變體樹

-- 找出某素材的所有變體
SELECT * FROM resume_chunks 
WHERE parent_id = 'abc123'

-- 找出有最多變體的素材(可能需要優化)
SELECT parent_id, COUNT(*) as variant_count
FROM resume_chunks
WHERE parent_id IS NOT NULL
GROUP BY parent_id
ORDER BY variant_count DESC

清理變體

# 人工審核後,保留最好的版本
# 刪除其他變體
table.delete(f"parent_id = '{chosen_id}' AND id != '{chosen_id}'")

門檻調整建議

場景 cosine_threshold skills_threshold
寬鬆(更多變體) 0.90 0.4
預設 0.94 0.5
嚴格(更激進去重) 0.97 0.6

根據你的使用習慣調整:

總結

層級 檢測方式 結果
Layer 1 text_hash 精確比對 拒絕入庫
Layer 2 cosine + skills 相似檢測 標記為變體
- 通過兩層 正常入庫

這個多層次策略確保:

  1. 不浪費儲存 - 完全相同的不重複存
  2. 保留變體價值 - 相似但不同的寫法都保留
  3. 建立關聯 - 可以追蹤和管理變體版本

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