はじめに
前回の「やさしい編」では、Elasticsearchがどのようにスコアを計算しているかを、数式を使わずに直感的なイメージで探りました。
-
Coordination Factor (
coord): マッチしたキーワードが多いほど高スコアに。 -
Field Length Norm (
norm): 短いフィールドでのヒットほど高スコアに。
これらの概念が、TF-IDFやBM25といった基本スコアに「味付け」をしている、という話をしましたね。
今回は「応用編」です。これらの「味付け」が、実際の数式の中でどのように表現され、スコアに影響を与えているのかを、具体的な計算を通して解き明かしていきます。
かつてElasticsearch/Luceneで標準的に使われていたTF-IDFベースのスコアリングアルゴリズム(Practical Scoring Function) を題材に、その美しい数式の構造を完全に理解しましょう。
1. 全てはここから:Practical Scoring Function
Elasticsearch/LuceneがTF-IDFをベースに実装していたスコアリング計算式は、公式に「Practical Scoring Function」と呼ばれています。その全体像は以下の通りです。
$$
\text{score}(q, d) = \text{coord}(q, d) \cdot \text{queryNorm}(q) \cdot \sum_{t \in q} \left( \text{tf}(t \in d) \cdot \text{idf}(t)^2 \cdot \text{t.getBoost()} \cdot \text{norm}(t, d) \right)
$$
一見すると複雑ですが、大丈夫。私たちはすでにこの式の多くのパーツを知っています。分解していきましょう。
-
score(q, d): 最終的なスコア。クエリqと文書dの関連度です。 -
coord(q, d): やさしい編で学んだ「マッチ数ボーナス」。 -
queryNorm(q): クエリ正規化因子。異なるクエリ間でのスコアを比較しやすくするためのおまじないのようなもので、今回の文書ランキングの理解では重要度は低いです。 -
Σ ( ... ): クエリに含まれる各単語tについて、(...)の中を計算し、それらを合計するという意味です。 -
tf(t in d): 単語tの文書dにおける出現頻度。ただし、単純な回数ではなく、デフォルトでは回数の平方根sqrt(freq)を使います。出現回数が増えるほどスコアの伸びを緩やかにする(収穫逓減)ための工夫です。 -
idf(t)²: 単語tのIDF。TF-IDFの時と考え方は同じですが、2乗されているのがポイントです。これにより、レアな単語の重要性がより一層強調されます。 -
t.getBoost(): クエリ時に特定の単語の重要度を開発者が手動で上げ下げできるブースト値です。 -
norm(t, d): やさしい編で学んだ「フィールド長ペナルティ」。今回の最重要パーツの一つです。
2. 数式で見るcoordとnorm
直感で理解したcoordとnormが、数式でどうなっているかを見てみましょう。
coord(q, d) の計算
これは非常にシンプルです。
$$
\text{coord}(q, d) = \frac{\text{文書d内でマッチしたクエリ単語数}}{\text{クエリの総単語数}}
$$
例えば、クエリが「A B C」の3語で、文書にAとBの2語がマッチした場合、coordは 2/3 となります。3語全てがマッチすれば 3/3 = 1 となり、最大のボーナスが得られます。
norm(t, d) の計算
フィールドが短いほどスコアが高くなる(=ペナルティが小さい)normは、以下のように計算されます。
$$
\text{norm}(t, d) = \frac{1}{\sqrt{\text{フィールドの総単語数}}}
$$
ここでも平方根sqrtが使われています。これは、フィールドが長くなるほどペナルティを大きくはしますが、その影響を緩やかにするための工夫です。
例えば、
- 10単語のフィールド: $$ \frac{1}{\sqrt{10}} \approx 0.316 $$
- 100単語のフィールド: $$ \frac{1}{\sqrt{100}} = 0.1 $$
100単語のフィールドは10単語のフィールドよりペナルティが大きい(値が小さい)ですが、長さが10倍だからといってスコアが1/10になるわけではない、という絶妙な調整がされています。
3. 実践!スコア計算
それでは、具体的な例でスコアを計算し、coordとnormの効果を体感しましょう。
【クエリ】
q = 「Python 解説」 (2単語)
【文書】
-
文書A:
- title (3単語): 「Python 徹底 解説」
- body (50単語): ...
-
文書B:
- title (4単語): 「Go言語入門」
- body (100単語): 「...この記事ではPythonについて...(※「解説」は出現しない)」
【前提情報】
- 全文書数を1000と仮定します。
-
df("Python")= 50 -
df("解説")= 20 - 簡単のため
queryNormとgetBoostは1とします。
Step 1: IDFを計算する
IDFは $ \text{idf}(t) = \log\left(\frac{N}{df(t)}\right) $ で計算します。
$$
\text{idf}(\text{"Python"}) = \log\left(\frac{1000}{50}\right) = \log(20) \approx 3.0
$$
$$
\text{idf}(\text{"解説"}) = \log\left(\frac{1000}{20}\right) = \log(50) \approx 3.9
$$
したがって、IDFの2乗は以下のようになります。
$$
\text{idf}(\text{"Python"})^2 \approx 9.0
$$
$$
\text{idf}(\text{"解説"})^2 \approx 15.2
$$
Step 2: 文書Aのスコアを計算する (titleフィールドに注目)
文書Aはtitleフィールドにクエリの2単語が両方とも出現しています。
-
TF:
$ \text{tf}(\text{"Python" in title}) = \sqrt{1} = 1 $
$ \text{tf}(\text{"解説" in title}) = \sqrt{1} = 1 $ -
Norm:
$ \text{norm}(\text{title}) = \frac{1}{\sqrt{3}} \approx 0.577 $
各単語のスコア(tf * idf² * norm)を計算:
$$
\text{score}(\text{"Python"}) = 1 \times 9.0 \times 0.577 \approx 5.193
$$
$$
\text{score}(\text{"解説"}) = 1 \times 15.2 \times 0.577 \approx 8.76
$$
合計スコア (Σ) を計算:
$$
\Sigma = 5.193 + 8.76 = 13.953
$$
coordを計算:
クエリ2単語中、2単語がマッチしたので、
$$
\text{coord} = \frac{2}{2} = 1.0
$$
最終スコア (合計スコア * coord) を計算:
$$
\text{score}(\text{A}) = 13.953 \times 1.0 = \bf{13.953}
$$
Step 3: 文書Bのスコアを計算する (bodyフィールドに注目)
文書Bはbodyフィールドに「Python」が1回だけ出現しています。
-
TF:
$ \text{tf}(\text{"Python" in body}) = \sqrt{1} = 1 $ -
Norm:
$ \text{norm}(\text{body}) = \frac{1}{\sqrt{100}} = 0.1 $
各単語のスコアを計算:
$$
\text{score}(\text{"Python"}) = 1 \times 9.0 \times 0.1 = 0.9
$$
「解説」は出現しないのでスコアは0です。
合計スコア (Σ) を計算:
$$
\Sigma = 0.9 + 0 = 0.9
$$
coordを計算:
クエリ2単語中、1単語がマッチしたので、
$$
\text{coord} = \frac{1}{2} = 0.5
$$
最終スコア (合計スコア * coord) を計算:
$$
\text{score}(\text{B}) = 0.9 \times 0.5 = \bf{0.45}
$$
結果の考察
- 文書Aのスコア: 13.953
- 文書Bのスコア: 0.45
圧倒的な差がつきました。この差を生み出した要因は、まさにcoordとnormです。
-
normの効果: 文書Aは短いtitleフィールドでヒットしたため、norm値が高く(0.577)、スコアが底上げされました。一方、文書Bは長いbodyフィールドでのヒットだったため、norm値が低く(0.1)、スコアが伸び悩みました。 -
coordの効果: 文書Aは全キーワードがマッチしたため、coordが1.0となり、計算されたスコアがそのまま最終スコアになりました。一方、文書Bは1つしかマッチしなかったため、coordが0.5となり、せっかく計算したスコアが半分に減らされてしまいました。
まとめ:そしてBM25へ
今回は、かつてElasticsearchで使われていたPractical Scoring Functionを解剖し、TF-IDFをベースとしながらも、coordやnormといった実践的な要素を加えることで、いかにして検索精度を高めていたかを学びました。
このアルゴリズムは非常に強力ですが、弱点も存在します。例えば、normはインデックス作成時に計算・保存されるため、後から柔軟な調整がしにくい、といった点です。
こうした背景もあり、現代のElasticsearch(バージョン5以降)では、デフォルトのスコアリングアルゴリズムがBM25に変更されました。 BM25は、より優れた理論的背景を持ち、$k_1$や$b$といったパラメータで柔軟なチューニングが可能です。
しかし、今回学んだ「マッチした単語数」や「フィールドの長さ」を考慮するという思想は、BM25にも色濃く受け継がれており、情報検索のスコアリングを理解する上で極めて重要な foundational knowledge(基礎知識)です。この一連の進化の流れを理解したことで、あなたの検索技術に対する解像度は、格段に上がったはずです。