https://unsplash.com/photos/_DyUcJalGAc
在查詢時,ElasticSearch 會對查詢到的資料進行評分。分數越高,代表與當下的查詢條件越相符。在 ES 能夠以評分分數進行排序的情況下,我們可達到類似商業需求中「推薦」或是「依相關度排序」的效果。
本文將解說如何自行設計分數的計算方式,包含透過欄位值、條件、權重與衰減函數。
一、function score 的概念與構成
我們能夠在包含 should(OR 邏輯)或 match(關鍵字查詢)的查詢,觀察到越相符的 document,其分數會越高的現象。但這些分數是由 ES 算出來的,我們無法掌握其多寡。透過「function score」的功能,我們可讓 document 本身的欄位值納入評分標準,進一步決定怎樣的 document 應該更高分。
分數的計算方式,會在 request body 中定義好,所以是即時計算。以下是一個使用 function score 的 request body 結構。
{ "query": { "function_score": { "query": { "match_all": {} }, "functions": [], "score_mode": "sum", "boost_mode": "replace", "max_boost": 5 } } }
讀者可以看到,在第一層的 query 中包含了一個 function_score 物件,而在裡面又包含另一個 query。內層的 query 才是撰寫查詢條件的地方,在本文,筆者一律使用「match_all」(無條件),以取得所有 document,觀察它們的分數。
在 function_score 物件中還包含其他參數,下面將進行介紹。
(一)functions
我們會使用「函數」來計算分數,其定義會存放在 functions 陣列中。至於有哪些函數可用,後面小節會陸續介紹。筆者將函數計算的結果稱呼為「函數分數」,它會參與最後 document 分數的計算。
(二)score_mode
一個 document 可能被一至多個函數計算,得到多個函數分數。透過 score_mode 參數,能決定它們要如何運算,以產生最終的函數分數。可使用的參數值如下。
- multiply:相乘(預設)
- sum:相加
- avg:平均
- max:取最大值
- min:取最小值
- first:取第一個函數的分數
假設某 document 的函數分數有 1、2、5。則在選用 sum 時,最終函數分數為 8。以下是 request body 的示意內容。
{ "query": { "function_score": { "query": { "match_all": {} }, "functions": [ { // 算出 1 }, { // 算出 2 }, { // 算出 5 } ], "score_mode": "sum" } } }
(三)boost_mode
ES 原先可能會給予查詢到的 document 一個分數,筆者稱之為「查詢分數」。在使用 function score 後,我們可透過 boost_mode 參數,決定最終函數分數要如何與查詢分數運算,以做為 document 的分數。可使用的參數值如下。
- multiply:相乘(預設)
- sum:相加
- max:取最大值
- min:取最小值
- replace:直接將最終函數分數做為 document 分數
假設某 document 的查詢分數為 1.5,最終函數分數為 6。在選用 sum 時,document 分數為 7.5。
(四)max_boost
這個參數的目的在於限制最終函數分數的最大值,可用來避免某些 document 的分數太高,導致在按照分數排序後,經常「霸佔」最前面的位置。在商業需求上,能讓分數也蠻高的其他 document 有往前曝光的機會。
假設某 document 的查詢分數為 1.5,最終函數分數為 5,max_boost 為 4。在 boost_mode 選用 multiply 時,document 分數為 6。
二、範例資料介紹
由於筆者會利用 document 的各種欄位進行計分,因此將範例資料展示如下。
這些是描述學生資料的 document,以下為欄位說明。
其中 englishIssuedDate 雖然是字串,但 ES 能辨認出它是日期格式,因此會改用「date」類型來儲存。這部份能夠從 index 的「mapping」去確認。
Mapping 是一份資料,用來定義 ES 要如何儲存欄位值。透過如下的 API,我們可看到各欄位的 mapping。
GET /student/_mapping
得到的 response 之部份內容如下:
三、field_value_factor 函數
從這一節開始,讓我們認識 function score 的函數。本節要介紹的是 field_value_factor,它能夠將 document 的一個欄位值,傳入數學函數計算而得到分數。
以下範例是根據年級來計分,計算方式為 grade 欄位值 ×2。
{ "query": { "function_score": { "query": { "match_all": {} }, "functions": [ { "field_value_factor": { "field": "grade", "modifier": "none", "factor": 2, "missing": 1 } } ], "score_mode": "sum", "boost_mode": "replace" } } }
此例的分數結果為:Dora 8 分、Vincent 4 分、Mario 6 分、Winnie 2 分。
modifier 是傳入要使用的數學函數,可使用的值如下。
- none:不另外使用數學函數,直接使用欄位值,此為預設值
- square:平方
- sqrt:正平方根
- reciprocal:倒數
- log:log₁₀(欄位值)
- log1p:log₁₀(欄位值 + 1)
- log2p:log₁₀(欄位值 + 2)
- ln:ln(欄位值)
- ln1p:ln(欄位值 + 1)
- ln2p:ln(欄位值 + 2)
log 與 ln(即 logₑ)屬於數學上的對數函數,其數學定義筆者不多做解釋。不過它們的函數圖形較平緩,意即算出來的分數,變化不會太劇烈。下圖是 log 與 ln 的函數圖形。
而 factor 是用來先將欄位值乘上某個指定的值,再傳入數學函數計算,可做為調節分數之用。最後是 missing,它是在欄位值不存在時,用來替代的預設值。
四、使用 weight 給予權重
被查詢到的 document 能夠從函數得到函數分數。而使用 weight 符號則可進一步給予這些函數「權重」,調整它們函數對 document 分數的影響力。又或者可以達到「符合條件才給予分數」的目的。
以下範例是學生每升一個年級得 0.5 分;就讀「資訊管理」系者得 3 分;有修習「會計學」課程者得 2 分。加總即為 document 分數。
{ "query": { "function_score": { "query": { "match_all": {} }, "functions": [ { "field_value_factor": { "field": "grade", "factor": 1, "modifier": "none" }, "weight": 0.5 }, { "filter": { "term": { "departments.keyword": "資訊管理" } }, "weight": 3 }, { "filter": { "term": { "courses.name.keyword": "會計學" } }, "weight": 2 } ], "score_mode": "sum", "boost_mode": "replace" } } }
此例的分數結果為:Vincent 6 分、Mario 3.5 分、Winnie 2.5 分、Dora 2 分。
第一個 function 使用了上一節提到的 field_value_factor,但意義稍微不同。此處是在 field_value_factor 計算完畢後,再乘上 weight 的值,代表給予計算結果一個權重。
至於第二、三個 function 並未使用真正的數學函數,而是使用了「filter」符號,它是用來規定只有符合條件的 document 才能被計分。而所傳入的條件,與 ES 的查詢寫法相同。包括 term、range,甚至更複雜的 bool 都能使用。
若讀者想將 filter 與 field_value_factor 用在一起,也是可以的,如下:
{ "filter": { "range": { "grade": { "lte": 4 } } }, "field_value_factor": { "field": "grade", "factor": 1, "modifier": "none" }, "weight": 0.5 }
五、衰減函數(decay function)
(一)定義
衰減函數是一個特別的函數。它的特色之一是算出來的分數介於 0 到 1 之間。特色之二是當 document 的欄位值與我們指定的值越靠近,其分數就越高。下面以生活情境來舉幾個例子。
- 根據日期:小明通過某技能檢定的日期若越接近現在,他就越適合擔任訓練課程的小老師。
- 根據數值:商品價格若越接近使用者輸入的預算,則購物網站應該盡量推薦。即使價格超出預算一點點,可能也是很適合推薦的商品。
- 根據地理位置:店家距離使用者輸入的地點越近,人們前往的方便性就越高,更適合推薦。
接下來將介紹函數圖形,讀者可能要配合數學上「變化率」或「切線斜率」的概念,才較容易理解。
(二)函數圖形與參數
ES 的衰減函數包含「linear」(線性)、「exp」(指數)與「guass」(高斯)三種。以下是函數圖形的範例,x 軸為欄位值,y 軸為分數,讀者可一邊參照。
https://www.elastic.co/guide/en/elasticsearch/guide/current/decay-functions.html
我們可透過四個參數對衰減函數做調整。
- origin:x 的最佳值。當 document 的欄位值等於 origin,分數為 1。
- offset:用來擴大最佳值的範圍,讓 x 落在 origin ± offset 時,分數均為 1。預設值為 0。
- scale、decay:共同控制分數的變化率。當 x 遠離到 origin + offset + scale 或 origin - offset - scale 時,分數會下降至 decay。decay 預設值為 0.5。
在上面的範例圖形中,origin 為 40;offset 為 5;scale 為 5;decay 為 0.5。也就是說,x 落在 35 到 45 時,分數為 1。x 遠離到 30 或 50 時,分數為 0.5。
讓我們觀察這三種衰減函數的分數(y 值)變化率。
- linear:圖形在不同的 x 範圍中都各自是直線。意即 y 變化率一致,沒有遞增或遞減的情況。
- exp:當 x 從 origin ± offset 遠離,y 變化率是從劇烈到緩和。
- gauss:當 x 從 origin ± offset 遠離,y 變化率從 0 逐漸變劇烈。直到 y 小於 decay 後,變化率又開始緩和。意即座標(origin + offset + scale, decay)與(origin - offset - scale, decay)是「反曲點」。
這邊跟讀者分享一個工具,它能夠繪製出衰減函數的圖形。但它是以日期做為單位,在觀察上不太方便就是了。
https://codepen.io/xyu/full/MyQYjN/
(三)使用數值欄位計分
接下來讓我們實際對範例資料進行計分。以下範例是將 gauss 衰減函數用於學生的操性成績欄位。在設計上,我們規定 95 到 100 分為最優。而 80 分者,document 分數為 0.5。更低分者,分數變化率開始緩和(開始不敏感)。講口語一點,就是當欄位值低到一個程度,那些 document 都一樣差的概念。
{ "query": { "function_score": { "query": { "match_all": {} }, "functions": [ { "gauss": { "conductScore": { "origin": 100, "offset": 5, "scale": 15, "decay": 0.5 } } } ] } } }
此例的分數結果為:
Vincent | Mario | Dora | Winnie | |
---|---|---|---|---|
conductScore (x) | 86 | 83 | 74 | 71 |
_score (y) | 0.779 | 0.641 | 0.257 | 0.169 |
照理來說,當 x < 80 後,y 的變化率就會緩和,但到了 Dora 的 74 時,y 卻已經掉到 0.257,感覺似乎下降很快!這是因為 y 在 x = 80 時的變化率是最劇烈的,在 x 從 80 遠離到 74 的期間,y 的變化率雖然在下降中,但依然很大。
(四)使用日期欄位計分
在範例資料中的 englishIssuedDate 欄位,值看似是字串,但實際上是以 date 類型儲存的。若讀者在其他 index 想以數值(毫秒數)來儲存日期,可使用以下 API,在新增 document 前先設定 mapping,使欄位以 date 類型儲存。
PUT /{indexName}/_mapping { "properties": { "{fieldName}": { "type": "date" } } }
以下範例是將 gauss 衰減函數用於學生的檢定通過日欄位。在設計上,我們規定距今 90 天內通過者為最優。而距今 360 天就通過者,document 分數為 0.5。更早之前就通過者,分數變化率開始緩和。
{ "query": { "function_score": { "query": { "match_all": {} }, "functions": [ { "gauss": { "englishIssuedDate": { "origin": "now", "offset": "90d", "scale": "270d", "decay": 0.5 } } } ] } } }
若讀者想要在 origin 參數給予時間的毫秒數也是可以的。假設筆者的現在日期是 2022-07-24,則此例的分數結果為:
Mario | Winnie | Dora | Vincent | |
---|---|---|---|---|
englishIssuedDate | 2022-05-01 | 2021-12-01 | 2021-04-01 | 2021-01-01 |
_score | 1 | 0.817 | 0.236 | 0.112 |
為何筆者強調要在 mapping 將欄位的儲存類型設定為 date 呢?在上面的 request body 中,我們使用了「now」關鍵字代表現在,而「d」代表時間單位。這種做法只有在儲存類型為 date 時才能使用。以下是衰減函數支援的時間單位。
- d:天
- h:小時
- m:分
- s:秒
- ms:毫秒
- micros:微秒
- nanos:奈秒
上一篇:【ElasticSearch 8】使用 DSL 撰寫查詢條件
留言
張貼留言