研究論壇搜索如何綜合時間和TF/IDF權重。
自定義權重計算的效率問題javascript
假設有一個論壇的搜索
字段包括:html
subject:標題
message:內容
dateline:發佈時間
tagid:論壇idjava
{ //爲每一個全文索引字段定義highlight(高亮)格式 "highlight": { "fields": { "subject": {}, "message": {} } }, //不返回所有數據 "_source": false, //只返回subject字段 "fields": [ "subject" ], //一個查詢語句 "query": { //自定義score(分數) "function_score": { "query": { //帶過濾器的查詢須要用filtered 包裹 "filtered": { //過濾器部分 "filter": { "term": { "tagid": 1 } }, //查詢語句部分(全文索引) "query": { //要查的部分有2個,用or鏈接起來(should 相似or) "bool": { "should": [ { //match來按照全文索引來查 "match": { //查標題 "subject": { //標題關鍵字 "query": "手榴彈", //權重boost會作個乘法 "boost": 5 } } }, //內容字段權重較低,配置基本相同 { "match": { "message": { "query": "手榴彈", "boost": 1 } } } ] } } } }, //額外的發佈時間權重,時間越大,權重越大,也是乘法(默認) //可是因爲log在輸入值巨大的狀況下(時間戳)y軸增加緩慢,幾乎沒法影響到score,因此下面這個配置,思想是好的,結果是廢的 "field_value_factor": { "field": "dateline", //log(1 + dateline) "modifier": "log1p", "factor": 0.1, "missing": 1//沒有這個字段的處理方式,返回分數1 } } } }
note: 你可能注意到我用了手榴彈一詞,由於咱們論壇中幾乎不會出現這個詞,因此在測試中能夠方便測試詞頻、標題(subject),內容(message)的權重問題,而減小其餘用戶數據干擾算法
若是你使用比較新版本的ES,好比>=2.0,你可能須要先配置一下服務器以便支持groovy語句
_score是ES經過TF/IDF和其餘自定義算法計算獲得的一個分數,用來表達和搜索預期的接近程度,值越大越接近理想的結果。經過控制這個值,就能夠改變搜索的排序結果。上面的boost是其中一種,經過設置boost,獲得 _score = _score * boost的效果,至關於咱們喜歡使用的「權重」。
而上面的field_value_factor的控制方式爲:express
_score = oldscore * log(1 + dateline * 0.1)
其實這個語句的目的就是爲了讓時間大的(靠近如今)的數據排序靠前一點,好比新聞什麼的,時間越近也是越好的,然而這個分數在dateline有巨大差別的狀況下,只有萬分之幾的變化,不能知足要求,一個簡單的方法就是 1 /(當前時間 - 發佈時間),因爲這個分數的底數是從0開始算起的,而 f(x) = 1/x 靠近1的部分,數值差別比較大,遠離的部分(舊數據,趨於0)。這個公式還有個問題,就是底數多是0,若是你有N臺服務器,而服務器之間有必定的時間差,就可能遇到這個問題。
改成下面這樣:apache
1 / (當前時間 - 發佈時間 > 0 ? 當前時間 - 發佈時間 : 1)json
可是若是舊數據時間趨於0也會致使一個新問題就是,基於TF/IDF的分數失效了,也不是咱們想要的,因此簡單辦法就是給這個分數 + 1
還有一個問題就是當時間差別爲2秒的時候,數值已經降低了50%,這也太快了一點,經過一個因數控制一下降低速度服務器
1 / (當前時間 - 發佈時間 >= 1000 ? (當前時間 - 發佈時間)/ 1000 : 1) + 1數據結構
1000秒的效果
elasticsearch
8小時的效果,基本上在10天后,分值趨近於1,也就是徹底由TF/IDF決定
這樣確保告終果在(1,2]之間變化,而分值衰減50%須要2000秒之後纔會達到,最近1000秒內的數據分值相同,他們是平等的(除非新聞專題,半小時內發佈的東西對於用戶來講,前後的重要程度並不高,若是是論壇更是如此,在論壇中我可能會增長到4-8小時)。
{ "highlight": { "fields": { "subject": {}, "message": {} } }, "query": { "function_score": { "query": { "filtered": { "filter": { "term": { "tagid": 1 } }, "query": { "bool": { "should": [ { "match": { "subject": { "query": "手榴彈", "boost": 1, "operator": "or" } } }, { "match": { "message": { "query": "手榴彈", "boost": 1, "operator": "or" } } } ] } } } }, //這裏是差別的部分 "functions": [ { "script_score": { "script": "return 1 /( now - doc['dateline'].value > 1000 ? ( now - doc['dateline'].value ) / 1000 : 1);", "params": { "now": 1448806722 } } } ] } } }
在實際應用的時候,咱們數據量大概是 1400 萬條,約12G。
測試機器硬件配置:
Intel(R) Xeon(R) CPU E5506 @ 2.13GHz 4核
內存16G
集羣設置:
這個查詢跑下來須要3000 ms 以上,關鍵的問題就是groovy,這個數據不是預先計算好的,而是每次從新計算。
其實除了groovy,ES還支持多種腳本語言。
目前測試下來比較快的是expression ,內置,不須要插件,速度快,只支持數值運算,符合咱們的須要。
//function 部分改成以下,注意:不須要return,這是個算數表達式。不須要分號結尾。 "functions": [ { "script_score": { "lang": "expression", "script": "1 /( now - doc['dateline'].value > 259200 ? ( now - doc['dateline'].value ) / 259200 : 1)", "params": { "now": $time } } } ]
groovy 3000ms 以上
expression 35 - 50 ms 間浮動
官方說法是expression的速度是能夠匹敵原生java語言的插件。可是表達式的各個部分,只能是數字。
有關script_score的官方文檔
https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html#_lucene_expressions_scripts
lucene expression 官方文檔
http://lucene.apache.org/core/4_9_0/expressions/index.html?org/apache/lucene/expressions/js/package-summary.html
Es權威指南第二部分,Search in Depth - script-score https://www.elastic.co/guide/en/elasticsearch/guide/current/script-score.html