【Elasticsearch】使用 function score 在查詢時評分


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 之部份內容如下:

ElasticSearch 的 mapping

三、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 的函數圖形。

對數(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 軸為分數,讀者可一邊參照。

Elastic Search 衰減函數圖形
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
                        }
                    }
                }
            ]
        }
    }
}

此例的分數結果為:


VincentMarioDoraWinnie
conductScore (x)86837471
_score (y)0.7790.6410.2570.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,則此例的分數結果為:


MarioWinnieDoraVincent
englishIssuedDate2022-05-012021-12-012021-04-012021-01-01
_score10.8170.2360.112

為何筆者強調要在 mapping 將欄位的儲存類型設定為 date 呢?在上面的 request body 中,我們使用了「now」關鍵字代表現在,而「d」代表時間單位。這種做法只有在儲存類型為 date 時才能使用。以下是衰減函數支援的時間單位。

  • d:天
  • h:小時
  • m:分
  • s:秒
  • ms:毫秒
  • micros:微秒
  • nanos:奈秒

上一篇:【ElasticSearch 8】使用 DSL 撰寫查詢條件

下一篇:【ElasticSearch 8】導入到 Spring Boot 並實作測試 CRUD

延伸閱讀:【ElasticSearch 8】使用 Java API Client 實作 function score

留言