ElasticSearch 論壇搜索查詢語句

概述

研究論壇搜索如何綜合時間和TF/IDF權重。
自定義權重計算的效率問題javascript

數據結構

假設有一個論壇的搜索
字段包括:html

subject:標題
message:內容
dateline:發佈時間
tagid:論壇idjava

直接經過註釋一個查詢語句來直觀瞭解如何使用json來查詢數據。

{
//爲每一個全文索引字段定義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)的權重問題,而減小其餘用戶數據干擾算法

使用groovy語言來控制排序用的score字段

若是你使用比較新版本的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
            }
          }
        }
      ]
    }
  }
}

2015年12月03更新:groovy 效率極低

在實際應用的時候,咱們數據量大概是 1400 萬條,約12G。
測試機器硬件配置:
Intel(R) Xeon(R) CPU E5506 @ 2.13GHz 4核
內存16G
集羣設置:

  1. 單節點
  2. 分片5,備份 0 分詞插件爲 ik

這個查詢跑下來須要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 間浮動

  • 速度相差約 60 倍 *

官方說法是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

相關文章
相關標籤/搜索