← 部落格
文章

合併重複職缺,同時不隱藏真正的機會

一週前開啟 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 萬筆有效職缺進行唯讀調查,確認最後一個分隔符號之後 實際放了什麼。結果五花八門:

  • 地理位置: USATXLatin AmericaChennaiRemote, Latin America
  • 專業領域: BackendData SciencePlatformInfrastructure
  • 職級: SeniorVice PresidentAVP
  • 工作模式: RemoteHybridOnsite
  • 品牌/法律名稱: HollisterInc.
  • 括號內幾乎從來不是城市: (m/w/d)(all genders)(GCP)(MAKO)

所以不能假設「結尾就是城市」。若一律移除,就可能把 Backend 與 Frontend 工程師, 或資深與 Staff 職務合併成一張卡片。這比重複資料更糟,因為它是個謊言

讓積極合併仍然安全的防線

能讓這種移除方式保持安全的,是指紋原本就有的一項特性:職缺說明也是鍵值的一部分。 只有移除結尾後的職稱與職缺說明都相同,兩則刊登才會合併。因此,即使積極清理職稱, 也只能合併內文原本就完全相同的資料。

調查證實,這個條件在關鍵案例中成立。以 speechify 為例,它會在職稱後附加專業領域, 例如 Software Engineer, Data InfrastructureSoftware Engineer, PlatformSoftware 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.9780.98
Towa——不同職務0.9640.48
speechify——相同專業0.9680.95
speechify——不同專業0.9660.45
Amazon SDE——不同職缺0.83–0.970.19

嵌入向量把所有資料壓縮到狹窄的 0.96–0.99 區間,詞彙重疊率卻拉開了寬廣且一致的差距: 真正的重複資料 ≥0.95,不同職務 ≤0.5。嵌入向量壓縮文字並遺失專業領域訊號; 計算共用詞彙則保留了它。這是下一項準備部署的修改,而且這一次是在寫程式碼前就驗證了訊號。

得到的教訓

「刪除重複職缺」一句話中,藏著兩種方向相反的失敗。合併不足,使用者就得連續滑過五次 同一個工作;合併過度,則會抹去他們永遠看不到的真正機會。安全的方法不是發明更聰明的 相似度分數,而是選擇能在「相同」與「只是相似」之間取得足夠寬裕差距的訊號, 並在差距不足時,預設保留分開顯示。

整套流程都是開放原始碼,包括指紋計算、合併邏輯,以及淘汰錯誤想法的實驗。 如果你也喜歡這類工作,在 GitHub 上給一顆星, 能幫助更多人找到這個專案。

要針對這個職缺調整履歷嗎?

目前無法檢查您與這個職缺的符合程度;請先將履歷加入個人檔案,下次即可查看。

A new version of freehire is available