一般情況下,類似關系型數據庫中影響返回結果的方式,在elasticsearch中也存在,包括sort、size等。但是這都是簡單粗暴的對返回結果進行直接處理,很大的可能會影響返回文檔的相關度。比如,通過sort操作對搜索結果按時間排序,這時排在前面的文檔很可能相關度非常小,而相關度大的文檔則因為時間排序被放在了下面。這顯然不是我們想要的結果。elasticsearch提供了方法,允許我們用除了搜索之外的其他因素影響返回文檔的順序,同時兼顧文檔的相關度。
簡單粗暴地評分
首先先說一下elasticsearch的搜索評分邏輯。
查詢的權重基于三個因素:詞頻、逆向文檔頻率和字段長度歸一值。
- 詞頻:查詢詞在該文檔中出現的頻率。頻率越高,權重越高。
- 逆向文檔頻率:查詢詞在所有文檔中出現的頻率。頻率越高,權重越低。可以降低日常使用的高頻率詞的權重。
- 字段長度歸一值:查詢字段的長度。字段長度越長,查詢詞權重越高,反之越低。
不同詞對搜索結果的影響基本取決于以上三個因素,這里不列出詳細的計算公式。
如何影響文檔評分
首先,影響文檔評分的操作推薦是查詢的時候進行,這樣靈活性更好。這里不介紹簡單的評分提升或者降低,直接介紹elasticsearch中控制文檔評分的終極武器:function_scor。
function_score
function_score是query結構的一個子集,它對每一個符合查詢的文檔應用一個或一組函數,達到影響甚至替換原始查詢評分的目的。這個操作可以很方便的實現復雜的查詢邏輯。
Elasticsearch 預定義了一些函數:
- weight:為每個文檔應用一個簡單而不被規范化的權重提升值:當 weight 為 2 時,最終結果為 2 * _score 。
- field_value_factor:使用這個值來修改 _score ,如將 popularity 或 votes (受歡迎或贊)作為考慮因素。
- random_score:為每個用戶都使用一個不同的隨機評分對結果排序,但對某一具體用戶來說,看到的順序始終是一致的。
- 衰減函數 —— linear 、 exp 、 gauss:將浮動值結合到評分 _score 中,例如結合 publish_date 獲得最近發布的文檔,結合 geo_location 獲得更接近某個具體經緯度(lat/lon)地點的文檔,結合 price 獲得更接近某個特定價格的文檔。
- script_score:如果需求超出以上范圍時,用自定義腳本可以完全控制評分計算,實現所需邏輯。
以上函數開箱即用,一般情況下,elasticsearch提供的函數就可以滿足需求,如果有特殊要求,也可以使用最后一個script_score自己寫控制評分腳本。但是script_score對性能有較大影響,能不用就不用。
下面簡單說明幾個需求,來看看elasticsearch是如何通過function_score實現的。
點擊數影響評分
如果想要在原來搜索的基礎上,加入點擊數的影響,即將點擊數高的文檔放到搜索結果靠上的位置,但是搜索的評分仍然是主要的依據。
PUT /blogposts/post/1
{
"title": "About popularity",
"content": "In this post we will talk about...",
"votes": 6
}
這是一篇文章的文檔,其中保存了點擊數。這里可以通過field_value_factor實現相關需求。
GET /blogposts/post/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes"
}
}
}
}
function_score嵌入了一個query查詢中,然后內部又設定了一個query,內部的query查詢是主查詢。在function_score內部還有一個field_value_factor函數,這個函數會對符合每一個主查詢的文檔使用。每個文檔的最終評分都會進行如下的計算:new_score = old_score * number_of_votes
默認的field_value_factor計算是線性的,votes
的原始值直接用來計算,這通常不會產生好的結果。一般情況下,通過對數計算取代現行計算,可以取得更平滑的結果。
GET /blogposts/post/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes",
"modifier": "log1p"
}
}
}
}
將field_value_factor設為對數計算,計算公式:new_score = old_score * log(1 + number_of_votes)
。
field_value_factor提供了眾多參數,設置相關計算的各種參數,這里不再一一列舉。如果想要細致的調整搜索結果的話,參考官方文檔進行。
對查詢結果進行隨機評分
作為網站的所有者,總會希望讓廣告有更高的展現率。在當前查詢下,有相同評分 _score 的文檔會每次都以相同次序出現,為了提高展現率,在此引入一些隨機性可能會是個好主意,這能保證有相同評分的文檔都能有均等相似的展現機率。
我們想讓每個用戶看到不同的隨機次序,但也同時希望如果是同一用戶翻頁瀏覽時,結果的相對次序能始終保持一致。這種行為被稱為一致隨機。
random_score
函數會輸出一個 0 到 1 之間的數,當種子 seed
值相同時,生成的隨機結果是一致的。例如:
GET /blogposts/post/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"random_score": {
"seed": "userID"
}
}
}
}
使用每個用戶的id作為seed
傳入,可以使每個用戶的隨機保持一致,同時在不同用戶之間保持不同的隨機性。
時間、空間上的“越近越好”
一般來說,搜索要求具有時間相關性。也就是說,用戶只想看到時間較近的文檔,而時間較遠的文檔,就算相關度較高,用戶也不想看到。空間上也有相關的性質,比如以某個點為中心,周圍一定距離以內的文檔排在返回結果的前面,而超過一定距離的文檔就算相關度較高也排在后面。elasticsearch提供了衰減函數,可以對文檔的相關性按某個維度進行衰減。
這里不能直接使用sort。因為如果直接使用sort,會讓相關度較低的文檔排在前面,維度近了但是相關度很差,達不到相關度較高、同時某個維度較 近 的需求。
elasticsearch提供了三個衰減函數,分別是linear、exp和gauss(線性、指數和高斯函數),它們可以操作數值、時間以及經緯度地理坐標點這樣的字段(一般衰減也沒有用字符串做衰減的)。所有三個函數都能接受以下參數:
- origin:中心點 或字段可能的最佳值,落在原點 origin 上的文檔評分 _score 為滿分 1.0 。
- scale:衰減率,即一個文檔從原點 origin 下落時,評分 _score 改變的速度。(例如,每 £10 歐元或每 100 米)。
- decay:從原點 origin 衰減到 scale 所得的評分 _score ,默認值為 0.5 。
- offset:以原點 origin 為中心點,為其設置一個非零的偏移量 offset 覆蓋一個范圍,而不只是單個原點。在范圍
-offset <= origin <= +offset
內的所有評分 _score 都是 1.0 。
衰減曲線
比如,想要搜索某條博客,同時發表時間近的排在前面,引入基于時間的衰減函數。
GET /blogposts/post/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"gauss" :{
"timestamp": {
"origin": "now timestamp",
"offset": "5d",
"scale": "10d"
}
}
}
}
}
按時間和地理空間做衰減,offset 和 scale 必須加上單位。
這里我們確定以當前的時間為衰減函數的中心, 5 天以內的文檔,相關度不做處理;5 天到 15 天,衰減系數逐漸降低到0.5;15 天之外,系數繼降低。
比如,想將地理空間和價格的影響引入搜索,可以這樣實現:
GET /_search
{
"query": {
"function_score": {
"functions": [
{
"gauss": {
"location": {
"origin": { "lat": 51.5, "lon": 0.12 },
"offset": "2km",
"scale": "3km"
}
}
},
{
"gauss": {
"price": {
"origin": "50",
"offset": "50",
"scale": "20"
}
},
"weight": 2
}
]
}
}
}
這里地理空間類型,在衰減函數的參數里要加上單位,而普通的數值類型不用加單位。
通過衰減函數,可以在相關度為主的前提下,引入時間、空間、數值等其他因素,從而影響搜索的返回結果。比起簡單粗暴的sort,衰減函數可以保證相關度高的依然排在靠前的位置。
終極武器script_score
elasticsearch支持用戶自己寫groovy腳本,來自定義復雜的評分影響邏輯。因為日常中不太實用,同時腳本對性能影響較大,所以這里不做介紹。