Redis 實戰 —— 10. 實現內容搜尋、定向廣告和職位搜尋

滿賦諸機發表於2021-01-29

使用 Redis 進行搜尋 P153

通過改變程式搜尋資料的方式,並使用 Redis 來減少絕大部分基於單詞或者關鍵字進行的內容搜尋操作的執行時間。 P154

基本搜尋原理 P154

倒排索引 (inverted indexes) 是網際網路上絕大部分搜尋引擎使用的底層結構,它類似於書本末尾的索引。倒排索引從每個被索引的文件裡面提取一些單詞,並記錄包含每個單詞的文件集合。 P154

示例

假設有三個文件:

  • R = "it is what it is"
  • S = "what is it"
  • T = "it is a banana"

我們就能得到下面的倒排索引集合:

  • "a": {2}
  • "banana": {2}
  • "is": {0, 1, 2}
  • "it": {0, 1, 2}
  • "what": {0, 1}

檢索的條件 "what", "is" 和 "it" 將對應這個集合:{0,1} ∩ {0,1,2} ∩ {0,1,2} = {0,1}

可以發現 Redis 的集合和有序集合非常適合處理倒排索引。

基本索引操作

從文件裡面提取單詞的過程通常被成為語法分析 (parsing) 和標記化 (tokenization) ,這個過程可以產生一系列用於表示文件的標記 (token) ,有時又被成為單詞 (word) 。 P155

標記化的一個常見的附加步驟就是移除非用詞 (stop word) 。非用詞就是那些在文件中頻繁出現卻沒有提供相應資訊量的單詞,對這些單詞進行搜尋將返回大量無用的結果。 P155

本書中實現方向索引的邏輯非常簡單:

  1. 將文件劃分為單詞,並移除一個字元的單詞
  2. 對於每個單詞獲取或建立對應的集合,將當前文件的唯一標識放入集合中

如果需要支援中文等,就不能簡單進行英文分詞,需要分詞器進行處理。第一次接觸倒排索引是在 Elasticsearch 中,感興趣的可以瞭解 Elasticsearch 中倒排索引的實現以及 IK 中文分詞器。

基本搜尋操作

在索引裡面查詢一個單詞是非常容易的,只需要獲取單詞集合裡面的所有文件即可。根據多個單詞查詢文件時,就需要根據條件處理對應的集合,再從最終集合中獲取所有文件。 P156

可以使用 Redis 的集合操作完成對不同條件的處理:

  • SINTER / SINTERSTORE: 找出同時包含所有指定單詞的文件集合
  • SUNION / SUNIONSTORE: 找出至少包含一個指定單詞的文件集合
  • SDIFF / SDIFFSTORE: 找出包含某個單詞且包含其他某些單詞的文件集合

通過以上三類命令,我們基本能實現條件大部分的與或非操作。

分析並執行搜尋

我們使用的查詢語句進行分詞後具有以下特徵:

  • 以 + 開頭的單詞:表示這個單詞是前一個單詞的同義詞,需要取並集
  • 以 - 開頭的單詞:表示這個單詞不希望包含在文件中,需要取差集
  • 其他普通單詞:表示使用者需要查詢這個單詞,需要取交集

即: "connect +connection chat -proxy -proxies" 表示查詢的文件需要包含 "connect" 或 "connection" ,同時也要包含 "chat" ,並且不能包含 "proxy" 和 "proxies" 。

實際處理時,先對同義片語分別取並集,然後與需要查詢的單詞一起取交集,最後與不希望包含的單詞取差集,這樣所得到的集合就是使用者查詢的結果集。

對搜尋結果進行排序和分頁 P160

上述搜尋功能以及能夠搜尋出使用者查詢的所有文件唯一標識的集合,現在我們將根據這個文件唯一標識集合以及每個文件的具體資訊進行排序分頁。

  • 文件唯一標識集合: 儲存每個文件的唯一標識,例如: {1, 2, 276}
  • 每個文件的具體資訊: 資料結構為 HASH, 以 doc_{id} 為鍵,內部儲存對應文件的相關資訊,例如: "doc:276": {"id": 276, "created": 1324114412, "updated": 132562777, "title": "Troubleshooting...", ...}

對於這種情況我們可以使用 Redis 的 SORT 命令對文件唯一標識集合通過引用每個文件的具體資訊進行排序分頁。 (05. Redis 其他命令簡介

有序索引 P162

上面介紹了使用 Redis 進行搜尋,並通過引用儲存在 HASH 裡面的資料對搜尋結果進行排序分頁。接下來將介紹利用集合和有序集合實現基於多個分值的複合排序操作,它能提供比 SORT 命令更高的靈活性。 P162

對多個數值欄位進行排序 P162

假設我們目前需要根據文件對更新時間和得票數進行排序,為此我們需要用兩個有序集合儲存相關資訊。這兩個有序集合的成員都是文件唯一標識,成員的分值則分別是文件的更新時間和得票數。

設經過搜尋後滿足搜尋條件的文件唯一標識集合為 filtered_doc_ids ,文件唯一標識及其更新時間對應的有序集合為 doc_ids_with_update ,文件唯一 標識及其得票數對應的有序集合為 doc_ids_with_votes 。那麼可以通過 ZINTERSTORE 命令對這三個集合求交集,最後得出的滿足搜尋條件的文件唯一標識及其排序分值對應的有序集合,再使用 ZRANGE, ZREVRANGE 進行分頁獲取即可。 P162

示例: ZINTERSTORE filtered_doc_ids_with_sort_score 3 filtered_doc_ids doc_ids_with_update doc_ids_with_votes WEIGHTS 0 {update_weight} {vote_weight}

其中:

  • filtered_doc_ids_with_sort_score 為結果有序集合
  • filtered_doc_ids 的權重為 0 ,僅用做篩選結果,不用於排序
  • doc_ids_with_update, doc_ids_with_votes 的權重可以進行設定,為 0 時表示不用於排序;為其他數時,表示對應欄位對最終排序分所佔的權重,正數相當於該欄位需要正序排序,負數相當於該欄位需要倒序排序。

所思

這種利用分值的方法很巧妙,基本可以實現多欄位排序,但是優先順序可能難以掌控,難以做到先按照某一欄位排序,再按照另一欄位排序的形式。因為每個欄位對應的分值的數量級可能差別比較小,這個時候如果需要有排序欄位的優先順序,那麼可能需要對每個權重進行精巧地設計才行。

對非數值欄位進行排序 P164

上面介紹了使用有序集合對多個數值欄位進行排序,由於有序集合的分值只能是浮點數,所以非數值欄位不能直接用於排序,需要轉換成對應的浮點數。但由於雙精度浮點數只有 64 個二進位制位,實際能使用 63 個二進位制位,所以能表示的字串並不多,只能使用字串的前幾個字元進行分值估計,不足指定字元數的需要補齊到指定字元數。當然如果字符集縮小的話,可以重新進行編碼計算,進而可以對更長的字串進行分值估計。 P165

當這個分值特別大的時候,可能會引發最終計算的分值溢位而出錯的問題。

廣告定向 P166

接下來將介紹使用集合和有序集合構建出一個幾乎完整的廣告服務平臺 (ad-serving platform) 。 P166

對廣告進行索引 P167

針對廣告的索引操作和針對其他內容的索引操作並沒有太大的不同,被索引的的廣告通常都擁有像位置、年齡和性別這類必需的定向引數,並且往往只會返回單個廣告。 P167

廣告的價格 P167

  • 按展示次數計費 (cost per view) :這種廣告又稱 CPM 廣告或按千次計費 (cost per mille) ,每展示 1000 次就需要收取固定的費用
  • 按點選次數計費 (cost per click) :這種廣告又稱 CPC 廣告,根據被點選的次數收取固定費用
  • 按動作執行次數計費 (cost per action) :又稱按購買次數計費 (cost per acquisition) ,這種廣告又稱 CPA 廣告,根據使用者在廣告的目的地網站上執行的動作收取不同的費用

為了儘可能簡化廣告價格的計算方式,將對所有型別的廣告進行轉換,使得它們的價格可以基於每千次展示進行計算,產生出一個估算 CPM (estimated CPM, eCPM) 。 P168

  • CPM 的 eCPM 價格可以直接使用 CPM 價格
  • CPC 的 eCPM 價格可以通過將廣告的每次點選價格乘以廣告的點選通過率 (click-through rate, CTR) ,然後再乘以 1000 得到
  • CPA 的 eCPM 價格可以將廣告的點選通過率、使用者在廣告投放者的目標頁面上執行動作的概率、被執行動作的價格這三者相乘起來,然後再乘以 1000 得到

將廣告插入倒排索引 P169

我們基本可以複用上面提到的搜尋功能,除了會將廣告的關鍵詞插入倒排索引,還會將廣告的定向引數(位置、年齡和性別等)插入倒排索引中,並記錄廣告的型別、基本價格和 eCPM 價格。 P169

執行廣告定向操作 P170

當系統收到廣告定向請求的時候,它要做的就是在匹配使用者定向引數的一系列廣告裡面,找出 eCPM 最高的那一個廣告。同時,程式還會記錄頁面內容與廣告內容的匹配程度,以及不同匹配程度對廣告點選通過率的影響等統計資料。通過使用這些統計資料,廣告中與頁面相匹配的那些內容就會作為附加值被計入 CPC 和 CPA 的 eCPM 價格,使得那些包含了匹配內容的廣告能夠更多地被展示出來。 P170

計算附加值
計算附加值就是基於頁面內容和廣告內容兩者之間匹配的單詞,計算出應該給廣告的 eCPM 價格加上多少增量。每個單詞都有一個有序集合,成員為廣告 id ,成員的分值為當前單詞對這則廣告的 eCPM 的附加值。 P171

在尋找合適的廣告時,我們首先會過濾出匹配定位位置且至少包含一個頁面單詞的廣告,然後通過計算附加值的方法替代搜尋,以便實現每次投放價值最高的廣告,並能夠根據使用者的行為學習。同時由於每個廣告匹配的內容不同,最優方式應該是使用加權平均值來計算單詞部分的附加值,但限於 Redis 本身的命令,我們最終採取 (max + min) / 2 的形式計算單詞部分的附加值(max 表示所有匹配單詞的最大附加值, min 表示所有匹配單詞的最小附加值),採用如下命令即可: ZUNIONSTORE final_score 3 base max min WEIGHTS 1 0.5 0.5

從使用者行為中學習 P175

首先需要儲存使用者的瀏覽記錄,包括三部分:(每 100 次就主動更新一次 eCPM ) P175

  • 被定向至給定廣告的單詞(即:內容中單詞和給定廣告單詞的交集)
  • 給定廣告被定向的次數
  • 廣告中的某個單詞被用於計算附加值的次數

其次需要儲存使用者的點選和動作記錄,用於計算 點選通過率 = 點選量或動作次數 / 廣告展示次數。(每次都更新 eCPM) P176

最後就是更新 eCPM ,包括兩部分:

  • 廣告的 eCPM :根據廣告的實際價格和當前廣告的點選通過率,計算出最新的 eCPM
  • 廣告單詞的 eCPM 附加值:根據廣告的基本價格和每個單詞的點選通過率,計算出每個單詞最新的 eCPM 附加值
改進方案 P179
  • 隨時間流逝:可以仿照 03. Redis 簡單實踐 - Web應用 文章的 RescaleItemViewedNum 函式進行定期降低廣告的展示次數和點選次數(或者動作執行次數)
  • 增加計數值:可以考慮前一天、前一星期或者其他時間段的點選技術,並基於時間段的長短給予不同的權重
  • 使用第二價格拍賣 (second-price auction) 的方式來決定廣告位的費用
  • 給予低價廣告一定曝光量:部分時間內,獲取收益排名前 100 的廣告,基於它們的 eCPM 的相對值來挑選廣告,而不是挑選 eCPM 最高的廣告
  • 優化新廣告初始 eCPM :
    • 初期使用同型別廣告的平均點選資料
    • 在同型別廣告的平均點選通過率和當前實際點選通過率之間,構建一種簡單的反線性關係 (inverse linear relationship) 或者反 S 關係 (inverse sigmoid relationship) ,直到廣告有足夠的展示次數為止
    • 人為提高點選通過率,保證有足夠多的流量學習真正 eCPM
  • 考慮使用真正的貝葉斯統計、神經網路、關聯規則學習、聚類計算或者其他技術來計算附加值
  • 將記錄資訊的邏輯變為非同步(可以利用 09. 實現任務佇列、訊息拉取和檔案分發 中的任務佇列實現),提高響應效率

職位搜尋 P180

接下來將使用集合和有序集合實現職位搜尋功能,並根據求職者擁有技能來為他們尋找合適的職位。 P180

遍歷合適的職位 P180

第一反應肯定是直接對每一個求職者搜尋所有的崗位,從而找到求職者合適的崗位。但這種方法效率極低(大部分崗位肯定是技能對不上的),而且無法進行效能擴充套件。 P181

搜尋合適的崗位 P181

使用類似上面提到的附加值形式,每次新增一個崗位時,在對應的技能集合中新增這個崗位的 id (SADD idx:skill:{skill} {job_id}),再在崗位有序集合中進行新增,成員為崗位 id ,成員的分值為所需的技能數量 (ZADD job_required_skill_count {job_id} {required_skill_count})。搜尋的時候就先對求職者所有技能對應的集合使用 ZUNIONSTORE 操作計算每個公司匹配的技能數量 (ZUNIONSTORE matched {n} idx:skill:{skill} ... WEIGHTS 1 ...),然後再與崗位有序集合求交集,並讓公司有序集合的權重為 -1 (ZINTERSTORE result 2 job_required_skill_count matched WEIGHTS -1 1),最後獲取分值為 0 的所有崗位即可完成搜尋。 P181

所思

書上的這個方法比較麻煩,其實可以使用文章最開始的無序倒排索引,崗位相當於要搜尋的文件,崗位所需的技能相當於單詞。

本文首發於公眾號:滿賦諸機(點選檢視原文) 開源在 GitHub :reading-notes/redis-in-action

相關文章