← 部落格
文章

把一百萬筆職缺資料轉化為市場數據

我們匯入的每一個職缺都會經過標準化並加上標籤:類別、年資層級、一組技能、地點,以及(若職缺有揭露的話)薪資。直到最近,使用者只能一次查看一個職缺的這些分類,或是把它們當作搜尋篩選條件。其實回答整個目錄層級問題所需的資料早就齊備了——「哪些職務招募最熱絡」、「哪些技能正在崛起」、「資深後端職務的實際薪資是多少」——我們只是從未以彙總的方式對外公開。

全新的 Insights API 正是為了這件事而設計,而 /insights 頁面則為它打造了親和的使用介面。

預先計算,不要在查詢時即時彙整

最直覺的做法是在每次請求時,對職缺資料表執行一條大型的 GROUP BY。但我們並沒有這麼做。這些端點是公開且不需要驗證的,而每次請求都對整個目錄進行彙總,不僅速度緩慢,也容易讓爬蟲對資料庫造成巨大負擔。

我們改用一個小型 worker,每幾小時重新計算四張彙總表——職務需求、技能需求、招募速度與薪資區間——並以一次原子性的交換完成更新。讀取時就只是對小型資料表進行低成本的查詢。頁面載入時不會執行任何計算;它只讀取幾百筆預先彙總好的資料列。

兩個設計上的選擇,讓這些彙總成為目前資料的純函式,這代表重新執行它們永遠是安全的,絕對不會出現資料漂移:

  • 不靠歷史資料的成長計算。 「這個職務成長得有多快」需要前後對比。我們不保留歷史快照,而是直接從每筆職缺的 created_atclosed_at 推導出「在某個日期仍開啟中」的狀態。現在開啟中與三十天前開啟中的比較,只需對今天的資料表執行一次查詢——不需要維護快照,也就沒有不同步的問題。
  • 絕不混搭的薪資計算。 薪資有不同的幣別與發放週期。我們以 (currency, period) 為單位計算百分位數,絕不跨單位計算;若某個區間所依據的已揭露薪資樣本太少,我們也會予以隱藏——一方面是因為三個樣本算出來的中位數只是雜訊,另一方面是因為只有一個樣本的區間可能會指向特定個人。透過一條 SQL CUBE,我們就能在一次執行中,同時取得該職務本身的薪資、整個類別的薪資區間,以及介於兩者之間各年資層級的薪資。

以今天的後端職務來看,在約四百筆有揭露薪資的職缺中,年薪中位數約為 195,000 美元。這個數字是計算出來的,不是憑空猜測的。

值得被索引收錄的頁面

API 是困難的部分;登陸頁面才是收成的時刻。/insights/salary/backend/insights/skills/backend/insights/roles/backend——採用伺服器端渲染,讓搜尋引擎與 AI 爬蟲能在初始 HTML 中就看見數據,搭配一句由資料驅動的開場說明、內部連結導覽列,以及 Dataset + BreadcrumbList 的結構化資料。

程式化產生的頁面最常見的陷阱是內容過於單薄:為每個可能的網址產生頁面,結果大部分都是空白內容,而 Google 會懲罰這種做法。因此每個類別都會經過一道 資料品質門檻——必須累積到一定程度的真實需求後,才會產生頁面(以及對應的網站地圖項目)。資料中存在但規模過小的類別,則會回傳 404,而不是空白頁面,而涵蓋的範圍是即時計算出來的,因此會隨著目錄的增減自動調整。

唯有在上線環境才會出現的 Bug

在本地端與測試環境中一切正常。然而當我們用實際擁有 175 萬列的目錄執行彙總 worker 時,招募速度的重新計算竟耗時超過十分鐘,而且整個過程中磁碟 I/O 一直居高不下。

原因其實很平凡:速度查詢分別掃描了職缺資料表八次——每個分類軸各一次,新增與移除各一次。在小型測試資料表上完全看不出來;但實際的資料量下,它就成為效能瓶頸。我們採取了兩項修正。第一,批次處理 worker 確實需要比預設(為小型交易調校的)更多的可用記憶體,因此我們在那一次交易中提高了它的記憶體上限。接著我們重寫了查詢,讓它只掃描一次資料表,並在過程中把每筆職缺展開成對應的事件。速度計算步驟從超過五分鐘縮短到不到一分鐘,也讓這個長時間的批次交易不再干擾例行的資料表清理作業。

這是一個很好的提醒:有些成本只有在真實規模下才能測量——而上線部署本身就是測試的一環。

它的用途

這是一個更大構想中的資料層:讓 freehire 成為科技職場市場的可信資料來源,而不只是一個尋找單一職缺的地方。這些相同的彙總資料已經開放於 API,因此代理人(agent)同樣可以查詢。接下來將推出的,是以被搜尋到為目標設計的頁面——「最高薪職務」、「成長最快的技能」——每一個都是通往目錄的入口。

歡迎前往 /insights 一探究竟。

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

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

A new version of freehire is available