✍️

Elasticsearchのmatchクエリ(全文検索)のスコアを手計算してみた

に公開

こんにちは!スペースマーケットのエンジニアrokioです😼

少し前にElasticsearchハンズオン記事を書いたのですが、
そこでmatchクエリを使った検索をしたときのスコアが
どのように計算されるのか気になっていました🤔

ハンズオン記事はこちら:
https://zenn.dev/spacemarket/articles/5ee1c257ef4090

今回はElasticsearchの全文検索で用いられる数式を調べ、
例題をつくりスコアを手計算することで、
matchクエリが、与えられた文に対して何をもって「近い」とするのかを考えることにします!

セットアップ

仕事では日本語のデータを扱うことが多いので、
例題には日本語を用いることにしました。

アナライザーの設定

今回はElasticsearch公式ガイドラインにも説明がある、
Kuromoji Analyzerを採用します。
https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html

プラグインのインストール

bin/elasticsearch-plugin install analysis-icu
bin/elasticsearch-plugin install analysis-kuromoji

マッピングの設定

インデックスは本をドキュメントとして保存するbooksとし、
ドキュメントはtitleだけをもつものとします。

こちらを参考にしました:
https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji-analyzer.html

PUT books
{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "kuromoji_normalize": {
            "char_filter": [
              "icu_normalizer"
            ],
            "tokenizer": "kuromoji_tokenizer",
            "filter": [
              "kuromoji_baseform",
              "kuromoji_part_of_speech",
              "cjk_width",
              "ja_stop",
              "kuromoji_stemmer",
              "lowercase"
            ]
          }
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title":{
        "type": "text",
        "analyzer": "kuromoji_normalize"
      }
    }
  }
}

ドキュメントの追加

今回は計算しやすいよう最小限のドキュメントを用意します。
ドキュメントとなる本のタイトルは以下4つです。

  • 吾輩は猫である
  • 吾輩は猫であるが犬でもある
  • 吾輩は犬である
  • 私は犬である
    ちょっと似ていて微妙に違う文を用意しました😎
    ポイントは[吾輩, 私],[猫, 犬]の違いですね!

ドキュメントの追加は以下のように行いました。

POST /_bulk
{ "index" : { "_index" : "books" } }
{"title": "吾輩は猫である" }
{ "index" : { "_index" : "books" } }
{"title": "吾輩は猫であるが犬でもある" }
{ "index" : { "_index" : "books" } }
{"title": "吾輩は犬である" }
{ "index" : { "_index" : "books" } }
{"title": "私は犬である" }

スコアを計算する!!!

まずはElasticsearchのガイドを見て、
どのような計算方式をとっているのかを調べます。
https://www.elastic.co/guide/en/elasticsearch/reference/current/similarity.html
デフォルトでは「BM25」というアルゴリズムが使用されるようです。
そしてwikipediaのリンクが貼られていたので、
それを見ながら計算してみます🤓

Wikipediaの数式をKaTeXで書いてみます(ChatGPTに書かせた)

式(1)

\text{score}(D, Q) = \sum_{i=1}^{n} \text{IDF}(q_i) \cdot \frac{f(q_i, D) \cdot (k_1 + 1)}{f(q_i, D) + k_1 \cdot \left(1 - b + b \cdot \frac{|D|}{\text{avgdl}}\right)}

式(2)

\text{IDF}(q_i) = \ln \left( \frac{N - n(q_i) + 0.5}{n(q_i) + 0.5} + 1 \right)

式(3): 式(1)のシグマの右側の因子をtfとします

\text{tf}(D, q_i) = \frac{f(q_i, D) \cdot (k_1 + 1)}{f(q_i, D) + k_1 \cdot \left(1 - b + b \cdot \frac{|D|}{\text{avgdl}}\right)}

今回のクエリ(Q)は「吾輩は猫」としてスコアを計算してみます!
Qのkeywords(q1,q2,...)はElasticsearchで調べられます。

GET books/_analyze
{
  "analyzer": "kuromoji_normalize",
  "text" : "吾輩は猫"
}

結果:

{
  "tokens": [
    {
      "token": "吾輩",
      "start_offset": 0,
      "end_offset": 2,
      "type": "word",
      "position": 0
    },
    {
      "token": "猫",
      "start_offset": 3,
      "end_offset": 4,
      "type": "word",
      "position": 2
    }
  ]
}

q1 = "吾輩", q2 = "猫"となります。

今後のために各ドキュメントのトークンを調べておきます。
※説明のためにドキュメントに番号を振っておきます

ナンバリング タイトル トークン
d1 吾輩は猫である 吾輩, 猫
d2 吾輩は猫であるが犬でもある 吾輩, 猫, 犬
d3 吾輩は犬である 吾輩, 犬
d4 私は犬である 私, 犬

あとはひたすら計算していきます...

IDF(吾輩) = ln(1 + (N-n+0.5) / (n+0.5)) = log(1+ (1.5/3.5)) = 0.3566...
IDF(猫) = ln(1 + (N-n+0.5) / (n+0.5) ) = log(1+ 2.5/2.5) = 0.6931...

「吾輩」のIDFより、「猫」のIDFの方が高いです🤔
IDFの値に効いているのは対象のキーワードの出現数ですね。
ざっくりいうと、レアなキーワードほどスコアに影響を与えやすいことが分かります。

残りはtfの計算...
式中のk1とbはデフォルト値が設定されているので、今回はその値を使います。
https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-similarity.html#bm25

k1 = 1.2
b = 0.75

全部書くと長くなるので、一部だけ書きます!
d1(吾輩は猫である)だけに注目すると...

tf(d1, 吾輩) = freq * (k1 + 1) / {freq + k1 * (1 - b + b * dl / avgdl)} = 1.047...
tf(d1, 猫) = freq * (k1 + 1) / {freq + k1 * (1 - b + b * dl / avgdl)} = 1.047...

d1に関してはトークン間で差は無いですね。
d1のスコアを出してみましょう!

score(d1, Q) = IDF(吾輩)*tf(d1, 吾輩) + IDF(猫)*tf(d1, 猫) = 1.099...

...出ましたね!
じゃあこの調子でd2からd4まで...

ツラいのでスプシを使いました😇

スコアは以下の通りです!

本のタイトル スコア
d1: 吾輩は猫である 1.0998
d2: 吾輩は猫であるが犬でもある 0.9238
d3: 吾輩は犬である 0.3736
d4: 私は犬である 0.0000

ここから雑に考察してみます。

  • クエリにある「吾輩」と「猫」、d1とd2それぞれ両方に含まれていますが、d1の方がスコアが高くなっています。
    • これはdl(ドキュメントのトークン数)がd1よりd2の方が多いのが効いています
    • ヒットする単語が同じ場合、対象の文が短いほどスコアが上がるのですね!
    • ドキュメントのうち、クエリが占める割合が多いものが優先されるのは直感的(クエリそのものがドキュメントとして含まれていたらスコアが高くなるという直感)かも🤔
  • ヒットする単語が「吾輩」だけのd3は約0.4、「吾輩」「猫」のd1は約1.1です
    • IDFの計算で触れましたが、IDF(猫)が与える影響が大きいことがいえそうです
    • クエリのうち、検索対象のドキュメント群で希少なキーワードがスコアに影響を与えやすい👀

BM25の式を数学的に見ればより正確なことがいえそうですが、
その知恵は持ち合わせていないので雰囲気で勘弁してください🙏

できた...!(余談)

途中までiPadに式を書いて計算していたのですが、手で計算するのは学生以来で新鮮でした。。。
分数を通分するときなど、手が勝手に動くものですね。。。

皆さんもぜひ、BM25のスコアを手計算してみてください😇

スペースマーケット Engineer Blog

Discussion