合併重複職缺,同時不隱藏真正的機會
一週前開啟 freehire.me/companies/towa, 你會連續看到五次相同的職缺:
Senior Fullstack Engineer (m/w/d), Krakau
Senior Fullstack Engineer (m/w/d), Düsseldorf
Senior Fullstack Engineer (m/w/d), Bregenz
Senior Fullstack Engineer (m/w/d), München
Senior Fullstack Engineer (m/w/d), Wien 一個職務、五座城市、五張卡片。我們原本就有一層機制,專門合併這種情況—— 那它為什麼沒有生效?答案以及隨之展開的兩週追查,很適合說明「把重複資料刪掉就好」 這句話藏著更精確的問題:兩則刊登何時是同一個職缺,何時只是看起來相似?
原本已有的合併機制
FreeHire 會將同一職務的重複刊登集中在一張標準卡片下。判斷依據是 role_fingerprint——由公司、標準化職稱和職缺說明算出的雜湊值——而且刻意 不納入地點,因此同一職務若在其他城市重新刊登,會折疊成一張卡片,
下方再列出「跨城市開缺」清單。
問題是:這個指紋會雜湊職稱,而 Personio(Towa 使用的 ATS)把城市名稱 寫進職稱裡,例如 "… Engineer, Krakau"。五座城市、五個不同職稱、
五個不同指紋。原本為了忽略地點而設計的機制,卻因為地點混入它最信任的欄位而完全看不見。
修正方式看起來只要一行:計算雜湊前移除結尾的 , <city>。但它並不是一行就能解決,
因為最後一段文字不一定是城市。
結尾文字可能代表許多不同事物
在修改雜湊前,我們先對 398 萬筆有效職缺進行唯讀調查,確認最後一個分隔符號之後 實際放了什麼。結果五花八門:
- 地理位置:
USA、TX、Latin America、Chennai、Remote, Latin America - 專業領域:
Backend、Data Science、Platform、Infrastructure - 職級:
Senior、Vice President、AVP - 工作模式:
Remote、Hybrid、Onsite - 品牌/法律名稱:
Hollister、Inc. - 括號內幾乎從來不是城市:
(m/w/d)、(all genders)、(GCP)、(MAKO)
所以不能假設「結尾就是城市」。若一律移除,就可能把 Backend 與 Frontend 工程師, 或資深與 Staff 職務合併成一張卡片。這比重複資料更糟,因為它是個謊言。
讓積極合併仍然安全的防線
能讓這種移除方式保持安全的,是指紋原本就有的一項特性:職缺說明也是鍵值的一部分。 只有移除結尾後的職稱與職缺說明都相同,兩則刊登才會合併。因此,即使積極清理職稱, 也只能合併內文原本就完全相同的資料。
調查證實,這個條件在關鍵案例中成立。以 speechify 為例,它會在職稱後附加專業領域,
例如 Software Engineer, Data Infrastructure、Software Engineer, Platform、 Software Engineer, iOS Core Product,每種又刊登在約 129 座城市。移除結尾後,
它們全都變成 software engineer。這聽起來很可怕,但檢查職缺說明後會發現每個專業領域
都有自己的內容,所以指紋仍會依說明區分。只有純粹的城市版本——相同專業、相同內文——
會合併,結果正確無誤。
我們部署了這項修改。Towa 從五張卡片減為三張,但留下三張有兩種不同原因。 Kraków 正確地維持獨立,因為它包含波蘭專用內容(以波蘭茲羅提表示的薪資、當地語言說明), 文字確實不同,這正是防線應拒絕合併的情況。另外兩張 Bregenz 與 Wien 則是 相同的奧地利職缺——薪資 €54–80k、內文完全一致——只差一句提到 Vienna 的 奧地利團體協約條款。完全相符的鍵值仍將它們視為不同資料。這是刻意採取的保守策略: 我們寧可重複顯示一個真正的職缺,也不要把兩個不同職缺藏在同一張卡片後面; 但這組奧地利資料也顯示,這道防線現在過於嚴格。
繞路:為何看似理所當然的餘弦相似度其實不適用
從五張卡片減為三張已是進步,但仍留下同職務、說明卻依城市在地化的尾端問題。 整個目錄中的殘留數量很大,光 Amazon 的一個「Software Development Engineer」職稱, 就有 228 張不同卡片。
看似最誘人的修正是:我們已為每個職缺儲存語意嵌入向量,因此可以在公司+職稱的群組內, 依餘弦相似度分群。我們在撰寫正式程式碼前先做了實驗,結果一碰到真實資料就失效:
- speechify——同一專業在不同城市的餘弦分數為 0.994,但不同專業(Platform) 最高也達 0.992。兩者的區間完全重疊,沒有任何門檻能將它們分開。嵌入向量受到共用 樣板文字(公司介紹、福利、平等就業聲明)主導,因此專業領域的訊號被淹沒。
- Amazon SDE——那 228 張「重複」卡片彼此的分數為 0.83–0.97。它們並不是 同一職務的重複刊登,而是 AWS、零售、Alexa 等真正不同的職缺,只是共用一個通用職稱。 餘弦相似度說它們「不同」是正確的。
- 最致命的是:真正的重複資料(Towa Bregenz/Wien = 0.978)分數竟低於不同職務 (speechify Platform = 0.992)。單一餘弦門檻不可能同時合併重複資料,又保持不同職務分離。
最後一點直接否定了整套做法,也推翻了最初那個吸睛數字:所謂「可合併的 84.9 萬張卡片」 多數其實是共用通用職稱的不同工作,而不是重複資料。部署餘弦分群會隱藏真正的職缺。
真正有效的訊號
這次實驗不只淘汰一個想法,也指出正確方向。真正的重複資料(Towa Bregenz/Wien) 在約 11,000 個字元中只相差約 130 個,文字相同度超過 98%。因此,我們不用壓縮後的嵌入向量, 而改為測量單純的詞彙重疊率(標準化職缺說明的 Jaccard 相似度):
| 案例 | 嵌入向量餘弦相似度 | 詞彙重疊率 |
|---|---|---|
| Towa——相同職務、不同城市 | 0.978 | 0.98 |
| Towa——不同職務 | 0.964 | 0.48 |
| speechify——相同專業 | 0.968 | 0.95 |
| speechify——不同專業 | 0.966 | 0.45 |
| Amazon SDE——不同職缺 | 0.83–0.97 | 0.19 |
嵌入向量把所有資料壓縮到狹窄的 0.96–0.99 區間,詞彙重疊率卻拉開了寬廣且一致的差距: 真正的重複資料 ≥0.95,不同職務 ≤0.5。嵌入向量壓縮文字並遺失專業領域訊號; 計算共用詞彙則保留了它。這是下一項準備部署的修改,而且這一次是在寫程式碼前就驗證了訊號。
得到的教訓
「刪除重複職缺」一句話中,藏著兩種方向相反的失敗。合併不足,使用者就得連續滑過五次 同一個工作;合併過度,則會抹去他們永遠看不到的真正機會。安全的方法不是發明更聰明的 相似度分數,而是選擇能在「相同」與「只是相似」之間取得足夠寬裕差距的訊號, 並在差距不足時,預設保留分開顯示。
整套流程都是開放原始碼,包括指紋計算、合併邏輯,以及淘汰錯誤想法的實驗。 如果你也喜歡這類工作,在 GitHub 上給一顆星, 能幫助更多人找到這個專案。