NO IMAGE

如果你對搜尋廣告,競價排序,或者Elastic Search技術感興趣,讀讀這篇文章或許多少能有所收穫。作者不是計算廣告領域的專家,如果作為讀者的你是這個方面的專家發現本文淺薄,希望留下你寶貴的意見。
因為ES版本升級很快,很多功能支援程度也伴隨版本的升級而改變,本文內容基於Elastic Search 5.4.1實現。

什麼是搜尋廣告

舉個最常見的例子,當我們在淘寶上購物搜尋時候,例如輸入“貓糧”

在搜尋結果的第一個,你會看到有個小小的廣告二字,這條返回結果就是搜尋廣告的“傑作”了。

不同的運營平臺會提供給商家後臺採買關鍵詞,設定出價和匹配模式等。當使用者發起搜尋時,根據規則,首先召回採買關鍵詞的商家,然後對這些召回商家排序,返回廣告商家。

一般來說,這類廣告的收費模式都是按照點選收費(CPC),所以排序肯定不能按照單純的價高者得。因為即使商家出價再高,但是由於相關度和商家質量問題,而無人點選,平臺依然沒有任何營收,既浪費了平臺流量,也沒有給商家貢獻轉化。普遍來說,對於CPC廣告,排序一般基於商戶出價Bid * 預估CTR(點選率)。排序在計算廣告中佔據著舉足輕重的地位,提高AUC,CTR等指標,也讓無數青年才俊掉了不少頭髮。不過排序並不是本文介紹的重點,如果你感興趣,可以搜尋LR,GBDT,FM,OCPC等關鍵詞,相信你會有很多的收穫。如果有機會,筆者也希望可以寫機器學習相關的文章,本文主要介紹搜尋廣告的召回部分的實現。

文件

每一條商戶關鍵詞的出價是一個文件,JSON描述如下:

{
"id":123456
"weight": 201, 
"biding": "天潤酸奶", 
"lon": 117.60715739693345, 
"shopId": 400, 
"matchMode": "SpitContain", 
"lat": 27.555006197000644, 
"open": true
}

其中 id 代表推廣計劃ID,weight 是商家出價,biding 是商戶出價的關鍵詞,lon,lat 描述商戶地理座標,open 描述店鋪當前狀態。matchMode 是商戶設定的匹配模式,匹配模式 的含義是,只有在使用者搜尋詞和出價的關鍵詞 之間的匹配滿足一定條件的時候,才會生效(不能僅僅一直想完全一樣的情況哦)。在不同業務場景下,文件需要的資料是不同的。

筆者提供了一個簡單的Python程式可以生成一些測試文件,並索引到ES中,需要的朋友可以到這裡下載 測試資料生成器,該程式會生成50萬商家的2500萬條採買記錄,關鍵詞詞庫含有2萬條關鍵詞。

匹配模式

筆者定義了四種搜尋詞和關鍵詞匹配模式:

具體定義如下:
設定 Q 為使用者搜尋(Query),K 為商戶出價的關鍵詞(Keyword)

精準匹配:Q = K
例:糯米飯(Q) = 糯米飯(K)

精準包含:Q 是 K的子串
例:芒果(Q)= 芒果糯米飯(K)

短語包含:T 是 K的子串,其中 T 是 Q 的任意分詞詞項(Term)
例:芒果糯米飯(Q) = 芒果西米露(K),芒果糯米飯 的分詞詞項:芒果、糯米飯,其中 芒果 是關鍵詞 芒果西米露 的子串。

模糊匹配:S是K的子串,其中S是T的同義詞
例:麻辣黃悶雞(Q)= 黃燜雞米飯(K),麻辣黃悶雞 分詞為:麻辣、黃悶雞,黃悶雞的同義詞為黃燜雞(S),而S是K的子串。

關於匹配模式的工作模式可以舉個例子

所以召回要解決兩個問題:

a. 支援基於關鍵詞文件的全文檢索

b. 支援匹配模式

基於Elastic Search的搜尋召回

召回的所有邏輯也可以使用Lucene拓展編寫,好處是可以高度整合業務邏輯到索引,缺點是開發成本高。本文將採用Elastic Search作為搜尋召回引擎,ES的預設配置,遠遠不能實現我們的需要的功能,所以需要做一些額外工作。

a. 中文和行業詞庫的擴充套件(本文采用美食詞彙)

b. 同義詞模糊匹配支援

c. 基於匹配模式的過濾器

1. 中文索引和行業詞庫擴充套件

這裡我們使用到了大名鼎鼎的 IK – Analyzer 外掛,IK的目錄中存在

config/custom/mydict.dic

檔案,把相關的行業詞彙放入其中即可。驗證如下:

curl -XGET 'localhost:9200/_analyze?pretty' -d '{
"analyzer":"ik_smart",
"text":"附近哪裡有黃燜雞米飯或者騰衝大救駕"
}'

分詞結果如下

{
"tokens" : [
#省略若干.......
{
"token" : "黃燜雞米飯",
"start_offset" : 5,
"end_offset" : 10,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "或者",
"start_offset" : 10,
"end_offset" : 12,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "騰衝大救駕",
"start_offset" : 12,
"end_offset" : 17,
"type" : "CN_WORD",
"position" : 5
}
]
}

可見 黃燜雞米飯 和 騰衝大救駕 已經作為單獨的詞彙被識別出來了。

2. 同義詞匹配支援

ES是支援同義詞邏輯的,不過需要一些配置,這個配置可以在建立索引的時候指定。

curl -XPUT 'http://localhost:9200/search_ad_index' -d '{
"settings": {
"analysis": {
"filter": {
"my_synonym_filter": {
"type": "synonym", 
"synonyms_path":"analysis/synonym.txt"
}
},
"analyzer": {
"ik_syno": {
"type":"custom",
"tokenizer": "ik_smart",
"search_analyzer": "ik_smart",
"filter": [
"lowercase",
"my_synonym_filter" 
]
}
}
}
}
}'

同時還要把同義詞庫定義在如下檔案

config/analysis/synonym.txt

為了說明問題,本文定義了一個很簡單的同義詞庫

黃悶雞,黃夢雞,huangmenjimifan,huangmenji,黃燜雞,黃燜雞米飯
Dongyingong,冬陰功,冬陰功湯

然後在 biding 欄位,配置支援同義詞的Analyzer

curl -XPOST 'http://localhost:9200/search_ad_index/shop_keyword/_mapping' -d '
{
"properties": {
"biding": {
"type": "text",
"analyzer": "ik_syno",
"search_analyzer": "ik_syno"
}
}
}'

現在我們索引一條文件,然後測試一下同義詞是否生效

curl -XPOST 'localhost:9200/search_ad_index/shop_keyword/1' -d '{
"weight" : 201,
"biding" : "黃燜雞米飯",
"lon" : 117.60715739693345,
"shopId" : 400,
"matchMode" : "SpitContain",
"lat" : 27.555006197000644,
"open" : true
}'

搜尋指令碼如下:

curl -XPOST 'localhost:9200/search_ad_index/shop_keyword/_search?pretty' -d '{
"query":{
"match":{
"biding":"huangmenji"
}
}
}'

召回結果如下:

{
"took" : 3,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 0.58874476,
"hits" : [
{
"_index" : "search_ad_index",
"_type" : "shop_keyword",
"_id" : "1",
"_score" : 0.58874476,
"_source" : {
"weight" : 201,
"biding" : "黃燜雞米飯",
"lon" : 117.60715739693345,
"shopId" : 400,
"matchMode" : "SpitContain",
"lat" : 27.555006197000644,
"open" : true
}
}
]
}
}

我們在同義詞庫定義了,huangmenji = 黃燜雞 = 黃燜雞米飯
用huangmenji做搜尋詞,召回了 biding = 黃燜雞米飯 的文件,說明同義詞已經被ES支援了。

3. 基於匹配模式的過濾器

既然ES已經支援了同義詞和行業詞彙分詞,那麼已經滿足了匹配模式中最廣泛的模糊匹配,基於模糊匹配的返回結果,把不滿足匹配模式的文件過濾掉,就獲得滿足業務的結果了。筆者用一個例子說明 多匹配模式 的支援過程。

首先,使用ES的模糊搜尋,獲得所有匹配的文件,如 標記①所示,簡化文件記錄為:
商戶ID,商戶出價關鍵詞,商戶設定的匹配模式。例如第一條記錄商戶1002,Bid了關鍵詞芒果西米露,但是只有在使用者搜尋和關鍵詞精準匹配的時候,才生效。

基於①返回的文件,可以在記憶體中過濾,只不過筆者不希望ES返回太多文件,增加網路負擔也可能將有用的文件擷取掉,所以筆者用ES的Java Plugin實現了一個 PostFilter,可以在 ES 匹配文件後返回結果前,過濾掉不符合規則的文件,基於效能的考慮使用Java語言實現原生的Plugin。

這個PostFilter需要傳入使用者搜尋 Q ,和基於使用者搜尋的分詞列表 T_s ,他是分詞項 T 的陣列。PostFilter的虛擬碼如下:

IF doc.matchMode is 精準匹配 Then
return Q equalTo doc.biding
ELSE IF doc.matchMode is 精準包含 Then 
return doc.biding substring Q
ELSE IF doc.matchMode is 分詞包含 Then 
return doc.biding substring T
ELSE
return true

②展示了過濾器工作的結果,那些不滿足商戶匹配模式的關鍵詞條目被打分為 0, 將被過濾掉,而滿足條件的被打分 1, 將在結果中保留。

③是經過過濾器後,返回的最終文件集合。

筆者把 PostFilter 的程式碼託管在 GitHub 上,可以在這裡找到: MatchModePostFilter 需要實驗的朋友,可以 Maven package 生成 .zip 檔案,解壓後放入 ES_HOME/plugins目錄下即可生效。

將可以實現功能的上文所云,濃縮到一條搜尋指令碼如下:

POST /search_ad_index/_search?pretty

{
"size":100,
"query": {
"bool": {
"must": {
"match": {
"biding": "麻辣香鍋冒菜" #使用者查詢
}
}, 
"filter": [  #其他業務過濾器,可以自己定義
{
"range": {
"lat": {
"gt": 31.5, 
"lt": 32.6
}
}
}, 
{
"range": {
"lon": {
"gt": 118.3, 
"lt": 119.4
}
}
}, 
{
"term": {
"open": true
}
}
]
}
}, 
"post_filter": {
"script": {
"script": {
"inline": "match_mode_scoring", #指定原生指令碼名
"lang": "native", 
"params": {
"query": "麻辣香鍋冒菜",  #使用者查詢
"tokens": "麻辣香鍋;冒菜" #使用者查詢分詞
}
}
}
}
}

部分返回結果如下:

           {
"_index": "fuzzy_search_ad",
"_type": "shop_keyword",
"_id": "1",
"_score": 12.347956,
"_source": {
"weight": 139,
"biding": "麻辣香鍋冒菜",
"lon": 118.31,
"shopId": 122,
"matchMode": "Exact",
"lat": 31.51,
"open": true
}
},
{
"_index": "fuzzy_search_ad",
"_type": "shop_keyword",
"_id": "2660337",
"_score": 6.4009247,
"_source": {
"weight": 338,
"biding": "蛋蛋麻辣香鍋",
"lon": 119.21255552940255,
"shopId": 53206,
"matchMode": "Fuzzy",
"lat": 31.71452073144111,
"open": true
}
},
{
"_index": "fuzzy_search_ad",
"_type": "shop_keyword",
"_id": "3895216",
"_score": 6.3706484,
"_source": {
"weight": 196,
"biding": "蛋蛋麻辣香鍋",
"lon": 118.58495088376293,
"shopId": 77904,
"matchMode": "Fuzzy",
"lat": 31.94751527254444,
"open": true
}
}

結束語

到這裡,關於搜尋廣告文件召回就介紹到這裡了。基於ES或者Lucence(ES的倒排索引實現)我們可以很輕易的實現倒排索引,如果深入挖掘還能實現很多制定化的需求。

傳送門:https://zhuanlan.zhihu.com/p/28390635