如果你對搜索廣告,競價排序,或者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需要傳入用戶搜索 ,和基於用戶搜索的分詞列表 ,他是分詞項 的數組。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的倒排索引實現)我們可以很輕易的實現倒排索引,如果深入挖掘還能實現很多制定化的需求。