Ian Chou's Blog

中文 BM25:用 jieba 解決履歷搜尋的分詞問題

中文 BM25:用 jieba 解決履歷搜尋的分詞問題

當你的知識庫同時有中英文履歷素材時,BM25 關鍵字搜尋會失效。本文分享如何用 jieba 分詞 + 自訂詞典解決這個問題。

問題背景

Career RAG 使用 Hybrid Search:

Query → [BM25 Score] + [Vector Score] → Fusion (0.7v + 0.3b) → Rerank

向量搜尋對語意理解很強,但 BM25 對精確關鍵字匹配更準確。問題是:

預設英文 tokenizer 對中文無效

# 英文正常
"Kubernetes deployment".split()
# → ["kubernetes", "deployment"] ✅

# 中文悲劇
"台積電技術經理".split()
# → ["台積電技術經理"] ❌(整塊一個 token)

這導致:

解決方案:jieba 分詞

實作邏輯

import jieba
import re

def is_chinese(text: str) -> bool:
    """檢測文本是否包含中文"""
    return bool(re.search(r'[\u4e00-\u9fff]', text))

def tokenize(text: str) -> list[str]:
    text = text.lower()
    
    if is_chinese(text):
        # 中文用 jieba
        tokens = jieba.cut(text)
    else:
        # 英文用空白分割
        tokens = text.split()
    
    # 過濾短 token 和標點
    return [t.strip() for t in tokens if len(t.strip()) > 1]

效果

tokenize("台積電技術經理 Kubernetes 機器學習")
# → ["台積電", "技術經理", "kubernetes", "機器學習"]
輸入 預設 tokenizer jieba tokenizer
台積電技術總監 1 token 2 tokens
資深雲端架構師 1 token 3 tokens
Python ML 工程師 2 tokens 3 tokens

自訂詞典:解決專有名詞問題

jieba 預設詞庫對專有名詞效果差:

list(jieba.cut("聯發科技術經理"))
# 預設:["聯", "發", "科技", "術", "經理"] ❌
# 載入詞典後:["聯發科", "技術經理"] ✅

建立 user_dict.txt

# 格式:詞彙 詞頻 詞性

# 科技公司
台積電 10 nz
聯發科 10 nz
鴻海 10 nz
輝達 10 nz

# 職位名稱
技術經理 10 n
產品經理 10 n
資深工程師 10 n
首席工程師 10 n

# 技術詞彙
機器學習 10 n
深度學習 10 n
分散式系統 10 n
微服務 10 n

載入詞典

from pathlib import Path
import jieba

USER_DICT = Path("data/user_dict.txt")
if USER_DICT.exists():
    jieba.load_userdict(str(USER_DICT))

完整 BM25 實作

from rank_bm25 import BM25Okapi

class BM25Index:
    def __init__(self, documents: list[str] = None):
        self._documents = []
        self._tokenized = []
        self._bm25 = None
        
        if documents:
            self.add_documents(documents)
    
    def add_documents(self, documents: list[str]):
        self._documents.extend(documents)
        new_tokenized = [tokenize(doc) for doc in documents]
        self._tokenized.extend(new_tokenized)
        # 重建索引
        self._bm25 = BM25Okapi(self._tokenized)
    
    def search(self, query: str, top_k: int = 10) -> list[tuple[int, float]]:
        if not self._bm25:
            return []
        
        query_tokens = tokenize(query)
        scores = self._bm25.get_scores(query_tokens)
        
        # 取 Top-K
        indexed_scores = [(i, score) for i, score in enumerate(scores)]
        indexed_scores.sort(key=lambda x: x[1], reverse=True)
        return indexed_scores[:top_k]

Hybrid Score 融合

def hybrid_score(vector_score: float, bm25_score: float, 
                 vector_weight: float = 0.7) -> float:
    """
    向量分數權重 0.7,BM25 權重 0.3
    
    為什麼這個比例?
    - 向量對語意理解強("分散式系統" ≈ "distributed systems")
    - BM25 對精確匹配強(公司名、技術名詞)
    - 履歷搜尋需要兩者結合
    """
    return vector_weight * vector_score + (1 - vector_weight) * bm25_score

效果驗證

測試案例

docs = [
    "在台積電擔任技術經理,負責 AI 晶片設計團隊",
    "聯發科資深工程師,專注機器學習晶片優化",
    "鴻海專案經理,管理 10 人產品團隊",
]

index = BM25Index(docs)

# 搜尋 "技術經理"
results = index.search("技術經理")
# 結果:第 0 篇分數最高 ✓

# 搜尋 "聯發科"
results = index.search("聯發科")
# 結果:第 1 篇分數最高 ✓

# 搜尋 "機器學習"
results = index.search("機器學習")
# 結果:第 1 篇分數最高 ✓

改進前後對比

指標 改進前 改進後
中文分詞正確率 ~30% ~95%
BM25 召回率 ~40% ~90%
專有名詞匹配
中英混合處理

常見陷阱

1. 詞典路徑錯誤

# ❌ 錯誤:相對路徑可能失效
jieba.load_userdict("user_dict.txt")

# ✅ 正確:使用絕對路徑
from pathlib import Path
DICT_PATH = Path(__file__).parent / "data" / "user_dict.txt"
jieba.load_userdict(str(DICT_PATH))

2. 詞典格式錯誤

# ❌ 錯誤:用 Tab 分隔
台積電	10	nz

# ✅ 正確:用空白分隔
台積電 10 nz

3. 忘記過濾短 token

# ❌ 錯誤:保留所有 token
tokens = list(jieba.cut(text))
# 會有 "的"、"是"、"在" 等無意義詞

# ✅ 正確:過濾短詞和標點
tokens = [t for t in jieba.cut(text) if len(t) > 1 and t.isalnum()]

效能考量

操作 延遲
jieba 載入詞典 ~0.5s(啟動時)
分詞 1000 字 ~10ms
BM25 搜尋 100 docs ~1ms

jieba 預設使用「精確模式」,適合搜尋場景。如果需要更快,可以用 jieba.lcut() 代替 jieba.cut()(返回 list 而非 generator)。

擴展:中英混合索引

對於同時有中英文的文檔:

def tokenize(text: str) -> list[str]:
    text = text.lower()
    tokens = []
    
    # 混合策略:先用 jieba 切,再處理英文
    for token in jieba.cut(text):
        token = token.strip()
        if len(token) <= 1:
            continue
        # 中文直接保留
        if is_chinese(token):
            tokens.append(token)
        # 英文檢查是否有意義
        elif token.isalnum():
            tokens.append(token)
    
    return tokens

總結

問題 解決方案
中文無法分詞 用 jieba 替代空白分割
專有名詞被切錯 載入自訂詞典
中英混合 自動檢測語言,分別處理

正確的中文 BM25 讓 Hybrid Search 真正發揮「向量 + 關鍵字」的優勢。


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