智聯招聘基於 Nebula Graph 的推薦實踐分享

語言: CN / TW / HK

本文首發於 Nebula Graph Community 公眾號

本文整理自智聯招聘資深工程師李世明在「智聯招聘推薦場景應用」的實踐分享

智聯招聘的推薦實踐分享

搜尋推薦架構

在講具體的應用場景之前,我們先看下智聯招聘搜尋和推薦頁面的截圖。 這是一個簡單的智聯搜尋頁面,登入到智聯招聘 App 的使用者都能看到,但是這個頁面背後涉及到的推薦、召回邏輯以及排序概念,是本文的重點。

功能矩陣

從功能上來說,從矩陣圖我們可以瞭解到做搜尋和推薦時,系統分為 Online 和 Offline 兩個部分。

在 Online 部分,主要涉及到實時操作,例如:搜尋某個關鍵詞、實時展示個人推薦。而這些功能性操作需要其他功能支援,比如:熱詞聯想,以及根據特定的輸入進行實體識別、意圖理解,或是個人使用者畫像的繪製。再下一步操作便是召回,利用倒排索引,根據文字、相似度匹配,以及引入 Nebula Graph 實現圖索引、向量索引,都是為了解決召回問題。最後,便是搜尋結果的展示——如何排序。這裡會有個粗排,比如常見的排序模型 TF/IDFBM25,向量的餘弦相似 等召回引擎排序。粗排後面是精排,即:機器學習的排序,常見的有線性模型、樹型模型、深度模型。

上述為線上 Online 流程,相對應的還有一套 Offline,離線流程。離線部分主要是整個業務的資料加工處理工作,把使用者的相關行為,例如:資料採集、資料加工,再把資料最終寫到召回引擎,像是上文提及過的倒排索引的 Solr 和 ES、圖索引的 Nebula Graph 以及向量索引的 Milvus,以提供線上的召回能力。

線上架構

線上架構

當一個使用者點選了智聯招聘的搜尋按鈕,會發生什麼呢?如上圖所示,經過一個 API 呼叫,再通過 Query DSL 的統一封裝加工,再進入三路(之前提過的倒排索引、圖索引和向量索引)召回,機器學習排序,最終將結果返回到前端進行展示。

離線架構

離線架構

如同上面功能矩陣方面介紹的那般,離線部分主要是資料的加工處理,將諸如 HBase、關係型資料庫 PostgreSQL、KV 資料庫 TiDB 之類的資料平臺通過資料鏈路進行加工,最終寫入到資料儲存層。

整體業務流程

將線上和離線架構進行整合,下圖細化了 API 請求的處理、快取、分頁、A/B Test、使用者畫像、Query Understanding、多路召回等流程。

整個架構

平臺架構

介紹了線上和離線的功能架構,現在來講下智聯招聘是如何支撐整個功能矩陣的。

平臺架構

從底層來說,智聯技術團隊是通過構建了這三個平臺來支撐整個功能矩陣的。

首先最上方就是我們整個的搜尋推薦架構平臺,分為資料處理、聚合層、機器學習三個模組。在資料處理模組,主要用來完成資料加工、資料同步、資料合併、格式轉換等資料層事項;聚合層則處理意圖識別、AB 測試、線上召回、排序模型;而機器學習模組,主要用來做特徵加工、特徵抽取、模型更新之類的事情。

在搜尋推薦架構平臺下方,便是搜尋召回引擎,由 Solr、Elasticsearch、Nebula Graph、Milvus 組成,分別負責倒排索引、圖索引和向量索引。

最下層,是大資料平臺,對接 Pulsar、Flink、HBase、HIVE、Redis、TiDB 等資料來源。

Nebula Graph 在推薦場景下的應用

智聯的資料規模

智聯這邊線上環境部署了 9 臺高配物理機,機器配置的話 CPU 核數大概在 64~72,256G 左右的記憶體。每臺機器部署 2 個 storaged 節點,一共有 18 個 storaged 節點,查詢 graphd 和元資料 metad 節點分別部署了 3-5 個。線上環境目前有 2 個 namespace,一共 15 個分片,三副本模式。

而測試環境,採用了 K8s 部署,後續線上的部署也會慢慢變成 K8s 方式。

說完部署情況,再來講下智聯招聘這塊的使用情況,目前是千萬級別的點和十億級別的邊。線上執行的話,最高 QPS 是 1,000 以上;耗時 P99 在 50 ms 以下。

下圖為智聯自研的監控系統,用來看 Prometheus 的監控資料,檢視節點狀態、當前查詢的 QPS 和耗時,還有更詳細的 CPU 記憶體耗損等監控指標。

監控截圖

業務場景介紹

下面來簡單介紹下業務場景

推薦場景下的協同過濾

協同過濾模型

推薦場景下有個比較常見的業務是協同過濾,主要用來解決上圖左下角的 4 個業務:

  1. U2U:user1 和 user2 為相似使用者;
  2. I2I:itemA 和 itemB 為相似物品;
  3. U2I2I:基於物品的協同,推薦相似物品;
  4. U2U2I:基於使用者的協同,推薦相似使用者的偏好物品;

上面 U2U 是在建立 user to user 的某種關係,可能是矩陣(向量級別)相似,也可能是行為級別的相似。1. 和 2. 是基本的協同(相似性),把使用者和使用者、物品和物品建立好關係,基於這種基本協同再延伸出更復雜的關係,比如:通過物品的協同給使用者推薦相似物品,或是根據使用者的協同,推薦相似使用者的偏好物品。簡單來說,這個場景主要是實現使用者通過某種關係,可得到相關物品的相似推薦或者是相似使用者的關聯物品推薦。

下面來分析一波這個場景

協同過濾的需求分析

協同過濾具體模型

具體來說,招聘領域來說,CV(簡歷)和 JD(職位)之間存在關聯關係,聚焦到上圖的中間部分,CV 和 JD 之間存在使用者行為和矩陣相似關係,像使用者查看了某個職位、使用者投遞了某個職位,或者是企業端的 HR 瀏覽了某個簡歷這些使用者行為,或者是基於某種演算法,都會給 CV 和 JD 建立起某種關聯。同時,還要建立 CV 和 CV 之間的聯絡,也就是上文說到的 U2U;JD 和 JD 之間的關係,就是上面的 I2I。關聯建立之後,可以整點有意思的事情——通過使用者 A 檢視過 CV1(簡歷)推薦相似的 CV2(簡歷),使用者 B 瀏覽過職位,也可以根據職位的相似性,給他推薦另外的 JD…這裡再提下這個需求的“隱藏”重點,就是需要進行屬性過濾。什麼是屬性過濾呢?系統會根據 CV 的相似度來推薦 CV,這裡就要做相關的屬性匹配了:基於期望城市、期望薪資、期望行業進行屬性過濾。召回的實現一定要考慮上述因素,不能 CV1 的期望城市是北京,你推薦的相似 CV 期望城市卻是廈門。

技術實現

原先的技術實現——Redis

Redis 實現方案

智聯這邊最開始實現協同過濾的方式是用 Redis 將關係通過 kv 方式儲存起來,方便進行查詢。顯而易見的是,這樣操作是能帶來一定的好處:

  1. 架構簡單,能快速上線;
  2. Redis 使用門檻低;
  3. 技術相對成熟;
  4. Redis 因為是記憶體資料庫的原因,很快,耗時低;

但,與此同時,也帶來一些問題:

屬性過濾實現不了,像上面說到的基於城市、薪資之類的屬性過濾,使用 Redis 這套解決方案是實現不了的。舉個例子,現在要給使用者推 10 個相關職位,通過離線我們得到了 10 個相關職位,然後我們建立好了這個關聯關係,但如果這時候使用者修改了他的求職意向,或者是增加了更多的篩選條件,就需要線上來實時推薦,這種場景下是無法滿足的。更不用提上面說到過的複雜的圖關係,實際上這種查詢用圖來做的話,1 跳查詢就能滿足。

再嘗試倒排索引實現——ES 和 Solr

ES 實現方案

因為智聯在倒排索引這塊有一定的積累,所以後面嘗試了倒排索引的方式。基於 Lucene 角度,它有一個索引的概念。可以將關係儲存為子索引 nested,然後過濾這塊的話,子索引中存關係 ID,再通過 JOIN query 實現跨索引 JOIN,這樣屬性就可以通過 JOIN 方式進行過濾。這種形式相比較 Redis 實現的話,關係也能存上了,屬性過濾也能實現。但實際開發過程中我們發現了一些問題:

  1. 不能支援大資料量儲存,當關系很大時,相對應的單個倒排會特別大。對於 Lucene 來說它是標記刪除,先將標記的刪除了再插入新的,每次子索引都要重複該操作。
  2. 關係較多時 JOIN 效能不好,雖然實現了跨索引 JOIN 查詢,但是它的效能並不好。
  3. 關係只能全量更新,其實設計跨索引時,我們設計的方案是單機跨索引 JOIN,都在一個分片裡進行 JOIN 操作,但這種方案需要每個分片存放全量的 JOIN 索引資料。
  4. 使用資源較多,如果跨索引涉及到跨伺服器的話,效能不會很好,想要調好效能就比較耗資源。

上圖右側是一個具體的實現實錄,資料格式那邊是關係的儲存方式,再通過 JOIN JD 的資料進行屬性過濾,這個方案最終雖然實現了功能但是沒在線上執行。

圖索引——Nebula Graph

圖索引實現方案

經過我們調研,業界對 Nebula Graph 評價挺高,智聯這邊用了 Nebula Graph 來實現圖索引。像剛在 U2U 和 U2I 的場景,通過圖的方式把 CV 和 JD 儲存成點,邊則儲存關係。至於屬性過濾,如上圖所示將 JD 諸如所在城市、學歷要求、薪資要求、經驗要求等屬性儲存為點的屬性;而相關性的話,則在關係邊上存了一個“分”,最終通過分進行相關性排序。

新技術方案唯一的缺點便是新領域的學習成本,不過在熟悉圖資料庫之後就方便很多了。

基於 Nebula Graph 的推薦

圖索引實現方案

具體的 CV 推 CV、CV 推 JD、JD 推 JD、JD 推 CV 場景,都能滿足,像下面這條語句:

``` match (cv:CV_TAG)-[p]-(jd:JD_TAG)

where id(cv)==1 AND p.SALARY>2000

return jd.ID, jd.TITLE, p.score

ORDER BY p.score DESC

SKIP 0 LIMIT 1000; ```

便是一個 CV 推 JD 的具體 nGQL 語句:通過簡歷(CV)開始進行查詢,經過一些屬性過濾條件,比如:薪資,根據邊上的相似分進行 ORDER BY 排序,最終返回一個推薦 JD 資訊。

整個業務這塊,因為關係相對簡單,所以這裡一共涉及了 5 種 Tag 和 20+ 種邊關係,以及建立 100 多種索引,整個資料量在千萬級點和十億級別的邊。

Nebula Graph 使用過程中問題總結

資料寫入

圖索引實現方案

資料寫入這裡主要分為了 3 個方面:

首先 T+1 資料重新整理。展開來說,因為資料是提前加工的,要給線上業務使用的話,涉及了 T+1 資料重新整理問題。刷資料的話,一開始可能是個冷資料,或者是沒有資料,重新整理的時候是直接寫入關係資料,這個邊資料可能連起始點都沒有。整個邊資料重新整理之後,就需要將不存在的點插入。所以這裡有個改進點,我們先插入點資料之後再寫入邊資料,這樣關係能更好地建立起來。資料重新整理這塊還有個問題,就是邊資料是 T+1 跑出來的,所以前一天的資料已經失效了,這裡就需要把已經存在的關係刪掉,再將新的關係寫入。

再來講下資料格式轉換,之前我們使用了倒排索引或者是 KV 來儲存關係,在資料結構這塊,圖結構同之前略有不同。像剛才提到的關係,兩個點之間需要建立什麼關係邊,邊上儲存何種資料,都是需要重新設定的。智聯這邊當時開發了個內部工具,用來自定義 Schema,可以方便地將資料儲存為點,部分資料儲存為邊,可以靈活操作配置。即便有別的業務接入,有了這個小工具也無需通過 Coding 方式來解決 Schema 設定。

最後一個問題是資料持續增加帶來的資料失效。像常見的累積線上活躍使用者,經過一段時間,像是三個月之前的活躍使用者現在可能是個沉寂使用者了,但按照累積機制的話,活躍使用者的資料是會一直增加的,這無疑會給伺服器帶來資料壓力。因此,我們給具有時效性的特性增加 TTL 屬性,定期刪除已經失效的活躍使用者。

資料查詢

圖索引實現方案

資料查詢這塊主要也是有 3 個方面的問題:

  1. 屬性多值問題
  2. Java 客戶端 Session 問題
  3. 語法更新問題

具體來說,Nebula 本身不支援屬性多值,我們想到給點連線多條邊,但是這樣操作的話,會帶來額外的一跳查詢的成本。但,我們想了另外個易操作的方法來實現屬性多值問題,舉個例子,我們現在要儲存 3 個城市,其中城市 A 的 ID 是城市 B 的 ID 字首,這裡如果用簡單的文字儲存,會存在檢索結果不精準問題。像上面查詢 5 時便會把 530 這個城市也查詢出來,於是我們寫入資料時,給資料前後加入了識別符號,這樣進行字首匹配時不會誤返回其他資料。

第二個是 Session 管理問題,智聯這邊在一個叢集中建立了多個 Space,一般來說多 Space 的話是需要切換 Space 再進行查詢的。但是這樣會存在效能損耗,於是智聯這邊實現了 Session 共享功能。每個 Session 維護一個 Space 的連線,相同的 Session 池是不需要切 Space 的。

最後一個是語法更新問題,因為我們是從 v2.0.1 開始使用的 Nebula Graph,後來升級到了 v2.6,經歷了語法迭代——從最開始的 GO FROM 切換到了 MATCH。本身來說,寫業務的同學並不關心底層使用了何種查詢語法。於是,這裡智聯實現了一個 DSL,在查詢語言上層抽象一層進行語法轉換,將業務的語法轉換成對應的 nGQL 查詢語法。加入 DSL 的好處還在於場景的查詢語句不再拘泥於單一的語法,如果用 MATCH 實現效果好就用 MATCH,用 GO 實現好就採用 GO。

統一 DSL 的實現

圖索引實現方案

上圖便是統一 DSL 的大概想法,首先從一個點(CV)出發(上圖上方藍色塊),去 join 某條邊(上圖中間藍色塊),再落到某個點上(上圖下方藍色塊),最終通過 select 來輸出欄位,以及 sort 來進行排序,以及 limit 分頁。

實現來說,圖索引這塊主要用到 match、range 和 join 函式。match 用來進行相等匹配,range 是用來進行區間查詢,比如說時間區間或者是數值範圍。而 join 主要實現一個點如何關聯另外一個點。除了這 3 個基本函式之外,還搭配了布林運算。

通過上面這種方式,我們統一了 DSL,無論是 Nebula 還是 Solr、還是 Milvus 都可以統一成一套用法,一個 DSL 便能呼叫不同的索引。

智聯 Nebula Graph 的後續規劃

更有意思更復雜的場景

圖索引實現方案

上面講的業務實現是基於離線加工的資料,後面智聯這邊將處理線上實時關係。像上圖所示,對於一個使用者和一個職位而言,二者存在的關係可以很複雜。比如它們都同某個公司有關係,或者是職位所屬的公司是某個使用者之前任職過的,使用者更傾向於求職某一個領域或者行業,職位要求使用者熟練掌握某種技能等等,這些構建成一個複雜的關係網路。

圖索引實現方案

而智聯的下一步嘗試便是構建起這種複雜的關係網路,再做些比較有意思的事情。例如,某個使用者在公司 A 就職過,於是通過這個關係查詢出他的同事,再進行相關性推薦;或者是使用者同校的上一屆學生 / 下屆學生傾向投遞某個職位或者公司,這種都可以進行相關推薦;像使用者投遞的這個職位曾經和誰聊過天這種行為資料,這個使用者的求職意向要求:薪資水平、城市、領域行業等資訊,便可以通過線上的方式進行關聯推薦。

更方便的資料展示

目前資料查詢都是通過特定的查詢語法,但是下一步操作便是讓更多的人低門檻的查詢資料。

更高的資源利用率

圖索引實現方案

目前來說,我們的機器部署資源利用率不高,現在是一個機器部署兩個服務節點,每臺物理機配置要求較高,這種情況下 CPU、記憶體使用率不會很高,如果我們將它加入 K8s 中,可以將所有的服務節點打散,可以更方便地利用資源,一個物理節點掛了,可以藉助 K8s 快速拉起另外一個服務,這樣容災能力也會有所提升。還有一點是現在我們是多圖空間,採用 K8s 的話,可以將不同 Space 進行隔離,避免圖空間之間的資料干擾。

更好用的 Nebula Graph

最後一點是,進行 Nebula Graph 的版本升級。目前,智聯用的是 v2.6 版本,其實社群釋出的 v3.0 中提到了對 MATCH 進行了效能優化,這塊我們後續將會嘗試進行版本升級。


交流圖資料庫技術?加入 Nebula 交流群請先填寫下你的 Nebula 名片,Nebula 小助手會拉你進群~~