はじめに
本記事では、課題一覧 の
課題No.3 外部知識に対する検索の仕組みに紐付け
Part2 で示した 質問から回答までの処理フロー で実施している「検索 から ルーティング判定」までの仕組みを説明します。
本記事で説明すること
- BM25検索の仕組み
- ベクトル検索の仕組み
- RRF統合の仕組み
- ルーティング判定に使用する
しきい値の求め方 - BM25検索 と ベクトル検索 のハイブリッド方式とした理由
- BM25検索を LIKE検索 で代替しない理由
- 用語解説
- まとめ
- おわりに
本記事の説明に使用するデータ
本記事ではわかりやすく説明することを優先して、以下の質問と コーパス をデータとして使用します。
- 質問
| 質問番号 | カテゴリ | 質問 | 質問のトークン |
|---|---|---|---|
| 1 | ねこ | ネコと遊ぶ道具は何ですか? | ["ネコ", "遊ぶ", "道具", "何"] |
| 2 | usererror | 7428 番のエラーで止まったサーバーのリセット方法は? | ["7428", "番", "エラー", "止まっ", "サーバー", "リセット", "方法"] |
| 3 | usererror | ユーザー名やパスワードを入れ替え、稼働中のソフトウェアを立ち上げ直すには? | ["ユーザー", "名", "パスワード", "入れ替え", "稼働", "中", "ソフトウェア", "立ち", "上げ", "直す"] |
コーパス
| コーパスレコード番号 | カテゴリ | 回答 | 回答のトークン | 回答文書長 |
|---|---|---|---|---|
| 1 | ねこ | 猫じゃらしは、ネコと遊ぶ玩具です。 | ["猫じゃらし", "ネコ", "遊ぶ", "玩具"] | 4 |
| 2 | ねこ | ネコの持ち運びはネコ用キャリーバッグ。 | ["ネコ", "持ち運び", "ネコ", "用", "キャリーバッグ"] | 5 |
| 3 | ねこ | ネコの住処はキャットハウス。 | ["ネコ", "住処", "キャットハウス"] | 3 |
| 4 | ねこ | 猫の肥満体は6Kg以上です。 | ["猫", "肥満", "体", "6", "kg", "以上"] | 6 |
| 5 | ねこ | ねこの健康体は2Kg程度です。 | ["ねこ", "健康", "体", "2", "kg", "程度"] | 6 |
| 6 | ねこ | ネコがネコを呼んで、ネコが集まる。 | ["ネコ", "ネコ", "呼ん", "ネコ", "集まる"] | 5 |
| 7 | usererror | E7421 の復旧手順は、ロックファイルを削除し、プロセスを起動することです。 | ["e", "7421", "復旧", "手順", "ロック", "ファイル", "削除", "プロセス", "起動"] | 9 |
| 8 | usererror | E7422 の復旧手順は、サービスを再起動することです。 | ["e", "7422", "復旧", "手順", "サービス", "再", "起動"] | 7 |
| 9 | usererror | E7423 の復旧手順は、設定ファイルを修正してサービスを再起動することです。 | ["e", "7423", "復旧", "手順", "設定", "ファイル", "修正", "サービス", "再", "起動"] | 10 |
| 10 | usererror | E7424 の復旧手順は、キャッシュを削除してサービスを再起動することです。 | ["e", "7424", "復旧", "手順", "キャッシュ", "削除", "サービス", "再", "起動"] | 9 |
| 11 | usererror | E7425 の復旧手順は、接続先を確認してサービスを再起動することです。 | ["e", "7425", "復旧", "手順", "接続", "先", "確認", "サービス", "再", "起動"] | 10 |
| 12 | usererror | E7426 の復旧手順は、認証情報を更新してサービスを再起動することです。 | ["e", "7426", "復旧", "手順", "認証", "情報", "更新", "サービス", "再", "起動"] | 10 |
| 13 | usererror | E7427 の復旧手順は、ディスク容量を確保してサービスを再起動することです。 | ["e", "7427", "復旧", "手順", "ディスク", "容量", "確保", "サービス", "再", "起動"] | 10 |
| 14 | usererror | E7428 は、排他マーカーの破棄後に常駐タスクを開始すると解消します。 | ["e", "7428", "排他", "マーカー", "破棄", "後", "常駐", "タスク", "開始", "解消"] | 10 |
コーパスレコード とは
コーパスにおける1行分のデータを指します。トークン とは
- 文書を
トークナイズ(分かち書き)した単語を指します。- 本機能 では kuromoji.js を使用して文書を
トークナイズ(分かち書き)します。- 本機能における
トークナイズ(分かち書き)の仕様は、「助詞、助動詞、記号、接続詞」および ストップワード ["する", "こと", "し"] などを除外します。回答文書長 とは
- 回答のトークン の トークン数 を指します。
- データの使用箇所
| 節 | 質問番号 | コーパスレコード番号 |
|---|---|---|
| BM25検索の仕組み | 1 | 1 ~ 6 |
| ベクトル検索の仕組み | 1 | 1 ~ 6 |
| RRF統合の仕組み | 1 | 1 ~ 6 |
ルーティング判定に使用する しきい値 の求め方 |
1 | 1 ~ 6 |
| BM25検索 と ベクトル検索 のハイブリッド方式とした理由 | 2 と 3 | 7 ~ 14 |
検索 から ルーティング判定 までの処理フロー
ワーカースレッド
- Part2 の 質問から回答までの処理フロー で示した ワーカースレッド は、別の課題に紐付けて説明するため本記事では割愛します。
設定ファイルとコーパスの概要Part1 の データモデル を参照してください。
推論本機能における
推論の仕様は Part4 の 最小構成の プラットフォーム で 推論 するための技術スタック を参照してください。
BM25検索の仕組み (語彙検索)
BM25(Best Match 25)とは、Okapi という情報検索システム内で検証された 25番目 のバリエーションを指します。(Okapi)
語彙検索とは、「質問と回答」に含まれる「トークン」の一致を手掛かりとして、該当する回答を見つける検索方式です。
本機能では質問に含まれる単語が、コーパスレコードの回答 にどのように出現するかを評価してスコアを付けます。
スコアは以下の計算式で求め、スコアの高い回答が正解候補になります。
$$
\mathrm{BM25}(D,Q)=
\sum_{t\in Q}
\ln\left(1+\frac{N-\mathrm{df}(t)+0.5}{\mathrm{df}(t)+0.5}\right)
\cdot
\frac{\mathrm{tf}(t,D)(k_1+1)}
{\mathrm{tf}(t,D)+k_1\left(1-b+b\frac{|D|}{\mathrm{avgdl}}\right)}
$$
0.5 は平滑化するための補正値です。
この計算式は、以下の3つの要素に整理できます。
- 逆文書頻度(IDF)
- どのくらい珍しい
単語か
- どのくらい珍しい
- 文書長による補正項(分母 denom)
- 回答の長さに比べて
単語が適切に含まれているか
- 回答の長さに比べて
- 加点倍率
- 回答の中に
単語が何回出現したか
- 回答の中に
単語とはトークナイズ(分かち書き)されたトークンを指します。
これらを組み合わせて算出した値が BM25関連度スコア です。
以降に、それぞれの要素が何をしているのかを説明します。
逆文書頻度(IDF) とは
逆文書頻度(IDF) とは、回答に出現した 単語 がどれほど珍しいかを評価して重みづけする指標です。
$$\text{IDF}(t) = \ln \left( 1 + \frac{N - \text{df}(t) + 0.5}{\text{df}(t) + 0.5} \right)$$
この計算式を日本語に落とし込むと、以下となります。
逆文書頻度(IDF) = log((単語 を含まないコーパスレコード数 + 0.5) / (文書頻度(DF) + 0.5) + 1 )
単語を含まない コーパスレコード数 とは
単語が出現しない コーパスレコード数です。- 本記事では、質問の
単語が出現しないコーパス内の「カテゴリで絞り込まれた コーパスレコード数」を指します。文書頻度(DF) とは
単語が出現する コーパスレコード数です。- 本記事では、質問の
単語が出現するコーパス内の「カテゴリで絞り込まれた コーパスレコード数」を指します。log(自然対数) のカッコ内で 1 を足す理由
- 1 を足さずに log(自然対数) を算出すると負の値になり「質問の単語が含まれているのに BM25関連度スコア が減点」されます。
- これを防ぐために、1 を足して補正します。
- この思想は Okapi BM25アルゴリズムを実装した全文検索ライブラリ Apache Lucene でも応用されています。
逆文書頻度(IDF) の計算結果は、以下となります。
| コーパスレコード番号 | ヒットした質問の 単語
|
単語 を含まない コーパスレコード数 |
文書頻度(DF) | 逆文書頻度(IDF) |
|---|---|---|---|---|
| 1 | ネコ | 2 | 4 | 0.4418327522 |
| 1 | 遊ぶ | 5 | 1 | 1.5404450409 |
| 2 | ネコ | 2 | 4 | 0.4418327522 |
| 3 | ネコ | 2 | 4 | 0.4418327522 |
| 6 | ネコ | 2 | 4 | 0.4418327522 |
ヒットしないデータ
質問の
単語
- 質問の ["道具"] と ["何"] は
コーパス内に存在しないため、この表から除外しています。コーパスレコード
- コーパスレコード番号4 の ["猫"] は質問の ["ネコ"] と文字表記が異なることから
コーパス内に一致しないため、この表から除外しています。- コーパスレコード番号5 の ["ねこ"] は質問の ["ネコ"] と文字表記が異なることから
コーパス内に一致しないため、この表から除外しています。
逆文書頻度(IDF) は珍しい単語の一致を重みづけします
少数の回答にしかない単語ほど、分母が小さく、分子が大きくなります。
そのため少数の回答にしかない単語ほど 逆文書頻度(IDF) が大きく珍しくなります。
この例では ["ネコ"] より ["遊ぶ"] の一致の方が「珍しい」と評価して重みづけし、コーパスレコード ごとに算出する BM25関連度スコア に影響を与えます。
文書長による補正項(分母 denom) とは
単語 の出現数を、回答の長さを考慮して評価します。
回答の長さを考慮する理由は、長い回答は全体の単語数が多いため、短い回答に比べて同じ 単語 が偶然多く出現しやすく、単純な出現回数だけでは長い回答が不当に有利になるからです。
補正項 とは誤差や特定の条件による歪みを修正し、本来あるべき適切な値に近づけるための 項 のことです。
$$
\text{denom} = \text{tf} + k_1 \cdot \left( 1 - b + b \cdot \frac{|D|}{\text{avgdl}} \right)
$$
この計算式を日本語に落とし込むと、以下となります。
文書長による補正項(分母 denom) = 単語頻度(TF) + k1 * (1 - b + b * (回答文書長 / 平均文書長))
単語頻度(TF) とは
単語が文書中に出現する回数です。- 本記事では、質問の
単語が対象文書中に出現する回数を指します。k1 とは
- TF飽和パラメータ を指し、
単語の出現回数の増加に伴う BM25関連度スコア の頭打ち度合いを決める定数です。b とは
- 文書長ペナルティパラメータ を指し、文書の長さに応じて BM25関連度スコア を割り引く度合いを決める定数です。
平均文書長 とは
- 平均文書長 とは、文書長の平均です。
- 本記事では、カテゴリで絞り込まれた 回答文書長 の平均を指します。
回答ごとの 文書長による補正項(分母 denom)
| コーパスレコード番号 | 質問の 単語
|
単語頻度(TF) | k1 | b | 回答文書長 | 平均文書長 | 文書長による補正項(分母 denom) |
|---|---|---|---|---|---|---|---|
| 1 | ネコ | 1 | 1.2 | 0.75 | 4 | 4.8333333333 | 2.0448275862 |
| 1 | 遊ぶ | 1 | 1.2 | 0.75 | 4 | 4.8333333333 | 2.0448275862 |
| 2 | ネコ | 2 | 1.2 | 0.75 | 5 | 4.8333333333 | 3.2310344827 |
| 3 | ネコ | 1 | 1.2 | 0.75 | 3 | 4.8333333333 | 1.8586206896 |
| 6 | ネコ | 3 | 1.2 | 0.75 | 5 | 4.8333333333 | 4.2310344827 |
※この表から省かれている内容は ヒットしないデータ を参照してください。
文書長による補正項(分母 denom) は 文書の密度評価 をします
- 回答文書長 が 平均文書長 より長い場合
- (回答文書長 / 平均文書長) の値は 1 より大きくなり、denom は大きくなります。
- denom が大きくなると、加点倍率が小さくなり BM25関連度スコア が割り引かれます。
- 回答文書長 が 平均文書長 より短い場合
- (回答文書長 / 平均文書長) の値は 1 より小さくなり、denom は小さくなります。
- denom が小さくなると、加点倍率が大きくなり BM25関連度スコア が高くなります。
このように 文書長による補正項(分母 denom) は、「短い回答に出現する 単語」を「長い回答に多く出現する 単語」より高く評価します。
加点倍率 とは
回答に 単語 が何回出現したかに応じて、BM25関連度スコア を何倍に引き上げるかを決める倍率のことです。
$$
\text{boost} = \frac{\text{tf} \cdot (k_1 + 1)}{\text{denom}}
$$
boostは実装上の変数名です。
この計算式を日本語に落とし込むと、以下となります。
加点倍率 = (単語頻度(TF) * (k1 + 1)) / 文書長による補正項(分母 denom)
回答ごとの 加点倍率
| コーパスレコード番号 | 質問の 単語
|
単語頻度(TF) | k1 + 1 | 文書長による補正項(分母 denom) | 加点倍率 |
|---|---|---|---|---|---|
| 1 | ネコ | 1 | 2.2 | 2.0448275862 | 1.0758853288 |
| 1 | 遊ぶ | 1 | 2.2 | 2.0448275862 | 1.0758853288 |
| 2 | ネコ | 2 | 2.2 | 3.2310344827 | 1.3617929562 |
| 3 | ネコ | 1 | 2.2 | 1.8586206896 | 1.1836734693 |
| 6 | ネコ | 3 | 2.2 | 4.2310344827 | 1.5599022004 |
※この表から省かれている内容は ヒットしないデータ を参照してください。
加点倍率 は 単語頻度(TF) を飽和させます
単語頻度(TF) が増えれば分子も増えますが、分母に 文書長による補正項(分母 denom) が入っているので分母(割る数)も増えます。
これをわかりやすく説明するために、以下のデータで説明します。
- 質問
| カテゴリ | 質問 | 質問のトークン |
|---|---|---|
| ねこ | ネコ | ["ネコ"] |
コーパス
| コーパスレコード番号 | カテゴリ | 回答 | 回答のトークン | 単語頻度(TF) | 回答文書長 | 平均文書長 | 文書長による補正項(分母 denom) | 加点倍率 |
|---|---|---|---|---|---|---|---|---|
| 1 | ねこ | ネコ、〇、〇 | ["ネコ", "〇", "〇"] | 1 | 3 | 3 | 2.2000000000 | 1.0000000000 |
| 2 | ねこ | ネコ、ネコ、〇 | ["ネコ", "ネコ", "〇"] | 2 | 3 | 3 | 3.2000000000 | 1.3750000000 |
| 3 | ねこ | ネコ、ネコ、ネコ | ["ネコ", "ネコ", "ネコ"] | 3 | 3 | 3 | 4.2000000000 | 1.5714285714 |
単語頻度(TF) が 1 から 3 へ増えていますが、加点倍率の増加は 3倍 ではなく 約1.57倍(約57%増) です。
つまり加点倍率は、同じ単語の出現回数の多さを、線形(正比例)に BM25関連度スコア へ直結させない仕組みです。
このように、出現回数が増えるほど追加の加点が小さくなり、頭打ちに近づく性質を 飽和 と呼びます。
BM25関連度スコア 算出
最後に BM25関連度スコア を算出します。
$$
\text{BM25}(D,Q)=\sum_{t\in Q}\text{IDF}(t)\times\text{boost}(t,D)
$$
この計算式を日本語に落とし込むと、以下となります。
BM25関連度スコア += 逆文書頻度(IDF) * 加点倍率
回答ごとの BM25関連度スコア 算出
| コーパスレコード番号 | 回答 | 逆文書頻度(IDF) | 加点倍率 | BM25関連度スコア |
|---|---|---|---|---|
| 1 | 猫じゃらしは、ネコと遊ぶ玩具です。 | 0.4418327522 | 1.0758853288 | 0.4753613759 |
| 1 | 猫じゃらしは、ネコと遊ぶ玩具です。 | 1.5404450409 | 1.0758853288 | 1.6573422194 |
| 2 | ネコの持ち運びはネコ用キャリーバッグ。 | 0.4418327522 | 1.3617929562 | 0.6016847298 |
| 3 | ネコの住処はキャットハウス。 | 0.4418327522 | 1.1836734693 | 0.5229857067 |
| 6 | ネコがネコを呼んで、ネコが集まる。 | 0.4418327522 | 1.5599022004 | 0.6892158825 |
コーパスレコード番号1 の BM25関連度スコア は質問の["ネコ"] と ["遊ぶ"] がヒットするので、次の合計になります。
["ネコ"] の BM25関連度スコア + ["遊ぶ"] の BM25関連度スコア で合計 2.1327035954
最終結果
BM25関連度スコア で降順にソートした 上位5件 の回答を BM25検索 の最終結果とします。
| ソート順 | コーパスレコード番号 | 回答 | BM25関連度スコア |
|---|---|---|---|
| 1位 | 1 | 猫じゃらしは、ネコと遊ぶ玩具です。 | 2.1327035954 |
| 2位 | 6 | ネコがネコを呼んで、ネコが集まる。 | 0.6892158825 |
| 3位 | 2 | ネコの持ち運びはネコ用キャリーバッグ。 | 0.6016847298 |
| 4位 | 3 | ネコの住処はキャットハウス。 | 0.5229857067 |
コーパスレコード番号1 は、["ネコ"] が1回出現、["遊ぶ"] が1回出現して 逆文書頻度(IDF) で高評価となり 1位 になります。
コーパスレコード番号6 は、["ネコ"] が3回出現するので 加点倍率 が高くなり 2位 になります。
コーパスレコード番号2 は、["ネコ"] が2回出現するので 3位 になります。
コーパスレコード番号3 は、["ネコ"] が1回出現するので 4位 になります。
ベクトル検索の仕組み (意味検索)
ベクトル検索(意味検索) とは文書の ベクトル値 を取得し、ベクトル空間上で 向き(意味) が近いデータを抽出する検索方法です。
本機能では、推論 によって「コーパスレコードの回答 にあらかじめ埋め込んだ ベクトル値」と「質問の ベクトル値」の コサイン類似度 からデータを抽出します。
この ベクトル検索 は、以下の2つの要素に整理できます。
-
推論-
ONNXモデルとライブラリにより 質問と回答のベクトル値を取得する仕組みです。
-
-
コサイン類似度スコア
- 質問 と 回答の
ベクトル値の向き(意味)がどの程度近いかを判定する値です。
- 質問 と 回答の
推論 とは
本機能における 推論 とは Part4 で説明した通りですが、ここではもう少し噛み砕いて説明します。
トークナイズ(分かち書き)
| 質問 | 質問のトークン |
|---|---|
| ネコと遊ぶ道具は何ですか? | [<s>, ▁que, ry, :, ▁, "ネ", "コ", "と", "遊", "ぶ", "道具", "は何", "ですか", "?", </s>] |
BM25検索 で使用した kuromoji.js とは別の方式で @xenova/transformers が質問を
トークナイズ(分かち書き)します。
<s>と</s>はONNXモデルが使う特殊なトークンで、▁はトークナイザ内の空白を表す記号です。
▁que, ry, :は質問文であることをONNXモデルに伝えるために先頭に付与するプレフィックス(query:)です。
- 平均化
- トークンごとに 384次元 の
ベクトル値を取得します。 - 取得した
ベクトル値を平均化して、一つの文書のベクトル値(384次元)にまとめます。
- トークンごとに 384次元 の
- L2正規化(L2 Normalization)
- 一つの文書の
ベクトル値(384次元)の向き(意味)を保ったまま、ベクトル値の長さを 1 に揃えます。
- 一つの文書の
※ 実際の
ベクトル値は 384次元 ですが、図ではイメージしやすいように 2次元平面(x, y) に置き換えて表現しています。
※ ベクトルの長さは、三平方の定理を多次元に拡張した計算で求めます。
※ベクトルの長さ = √(x軸の値² + y軸の値²)左が「
トークナイズ(分かち書き)から 平均化」までの質問の 向きと長さ で、右が「L2正規化」後の質問の 向きと長さ です。
ベクトル検索においては、「トークナイズ(分かち書き)から L2正規化」までが @xenova/transformers、onnxruntime-node、model_quantized.onnx の仕事で
RAGアプリ側で「トークナイズ(分かち書き)から L2正規化」を実装する必要はありません。
コサイン類似度 とは
コサイン類似度 の算出はRAGアプリ側で実装します。
コサイン類似度 とは「質問 の ベクトル値 の 向き(意味)」と「コーパスレコードの回答 の ベクトル値 の 向き(意味)」がどれくらい近いかを数値化したものです。
本機能では コサイン類似度 を コサイン類似度スコア として以下の計算式で求めます。
$$
\text{CosineSimilarity}(Q, D) = \frac{Q \cdot D}{\Vert{}Q\Vert{} \Vert{}D\Vert{}}
$$
この計算式を日本語に落とし込むと、以下となります。
コサイン類似度スコア = 内積 ÷ (質問のベクトル値の長さ × 回答のベクトル値の長さ)
コサイン類似度スコア 算出
質問の ベクトル値 の長さと 回答の ベクトル値 の長さは、L2正規化 により 1 に揃えられているので
(質問の ベクトル値 の長さ × 回答の ベクトル値 の長さ) の部分は 1 となります。
内積 は「同じ位置にある数値を掛け、その結果を足す」計算です。
よって最終的な計算式は、以下となります。
// 各次元の積を加算
for (let i = 0; i < 質問のベクトル値の次元数; i++) {
コサイン類似度スコア += 質問のベクトル値[i] * 回答のベクトル値[i];
}
回答ごとの コサイン類似度スコア 算出
| コーパスレコード番号 | 回答 | コサイン類似度スコア |
|---|---|---|
| 1 | 猫じゃらしは、ネコと遊ぶ玩具です。 | 0.8903327296 |
| 2 | ネコの持ち運びはネコ用キャリーバッグ。 | 0.8601923745 |
| 3 | ネコの住処はキャットハウス。 | 0.8506313404 |
| 4 | 猫の肥満体は6Kg以上です。 | 0.7672267236 |
| 5 | ねこの健康体は2Kg程度です。 | 0.8085226429 |
| 6 | ネコがネコを呼んで、ネコが集まる。 | 0.8558816175 |
BM25検索(語彙検索) は単語の一致を基準に評価するため、コーパスレコード番号4 と コーパスレコード番号5 の
"猫"と"ねこ"を含む回答はヒットしませんでした。
ベクトル検索(意味検索) の場合は 意味の近さ(類似性) を評価するため、コーパスレコード番号4 と コーパスレコード番号5 の"猫"と"ねこ"を含む回答もヒットします。
最終結果
コサイン類似度スコア で降順にソートした 上位5件 の回答を ベクトル検索 の最終結果とします。
| ソート順 | コーパスレコード番号 | 回答 | コサイン類似度スコア |
|---|---|---|---|
| 1位 | 1 | 猫じゃらしは、ネコと遊ぶ玩具です。 | 0.8903327296 |
| 2位 | 2 | ネコの持ち運びはネコ用キャリーバッグ。 | 0.8601923745 |
| 3位 | 6 | ネコがネコを呼んで、ネコが集まる。 | 0.8558816175 |
| 4位 | 3 | ネコの住処はキャットハウス。 | 0.8506313404 |
| 5位 | 5 | ねこの健康体は2Kg程度です。 | 0.8085226429 |
長さを 1 に揃えた単位円上では、質問のベクトルを (1, 0) に置くと、各回答ベクトルの x座標 がそのまま コサイン類似度スコア になります。
図中の矢印はラベルの重なりを防ぐため コーパスレコード番号1のみ座標を表示しており、内訳は以下の通りです。
- 質問
- コーパスレコード番号1
- コーパスレコード番号2
- コーパスレコード番号3
- コーパスレコード番号4
- コーパスレコード番号5
- コーパスレコード番号6
コーパスレコード番号4(破線) は図中に示していますが、コサイン類似度スコア の上位5件に入らないため、最終的な検索結果には含まれません。
RRF統合の仕組み (逆順位統合)
RRF統合 とは、異なる検索方式でランク付けしたデータを一つにまとめて再ランキングする方式です。
具体的には BM25関連度スコア と コサイン類似度スコア、それぞれのスコアそのものを使うのではなく
「それぞれの検索方式で何位だったか(ランク)」だけを使うことで、異なる検索方式のランキングを公平に統合します。
これをわかりやすく説明するために、一旦ここまでの説明を離れて以下のデータで説明します。
| 生徒 | 国語のランキング | 数学のランキング | ランキングの傾向 |
|---|---|---|---|
| Aさん | 1位 | 3位 | Aさんは、国語は1位ですが数学は3位です。 |
| Bさん | 2位 | 1位 | Bさんは、両方の科目で安定して上位にいます。 |
| Cさん | 3位 | 2位 | Cさんは、Aさん と Bさん より少しランクが低いです。 |
RRF統合スコア は以下の計算式で求めます。
$$\text{RRF_Score}(d) = \sum_{m \in M} \frac{1}{k + \text{rank}_m(d)}$$
上記データを例に、この計算式を日本語に落とし込むと以下となります。
Aさん、Bさん、Cさん個々の RRF統合スコア = 1 ÷ (60 + 国語のランク) + 1 ÷ (60 + 数学のランク)
$M$
- 使用する 検索方式 の集合です。(上記データを例にした場合、国語と数学のランキング)
$\text{rank}_m(d)$
- 検索方式 $m$ におけるランクになります。(1位なら $1$、2位なら $2$)
$k$
- 平滑化定数です。(Smoothing Constant)
- 1位と2位の差が極端に開きすぎるのを防ぐための定数で、一般的に $60$ が採用されます。(RRFの原論文)
上記データの RRF統合スコア は、以下となります。
| 総合ランキング | 生徒 | 国語のスコア | 数学のスコア | RRF統合スコア |
|---|---|---|---|---|
| 1位 | Bさん | 0.0161290322 | 0.0163934426 | 0.0325224748 |
| 2位 | Aさん | 0.0163934426 | 0.0158730158 | 0.0322664584 |
| 3位 | Cさん | 0.0158730158 | 0.0161290322 | 0.0320020481 |
この例では、Bさん は 国語で2位、数学で1位のため、2つの科目を合わせた総合評価では最も高くなります。
本機能では異なる検索方式(BM25検索 と ベクトル検索)のランキングを RRF統合スコア によりランク付けしています。
最終結果
本記事の説明に使用するデータ を元に
BM25関連度スコア と コサイン類似度スコア を RRF で統合した RRF統合スコア は以下となり
降順にソートした 上位5件 を最終結果とします。
| 総合ランキング | コーパスレコード番号 | 回答 | BM25検索のRRFスコア | ベクトル検索のRRFスコア | RRF統合スコア |
|---|---|---|---|---|---|
| 1位 | 1 | 猫じゃらしは、ネコと遊ぶ玩具です。 | 0.0163934426 (1位) | 0.0163934426 (1位) | 0.0327868852 |
| 2位 | 6 | ネコがネコを呼んで、ネコが集まる。 | 0.0161290322 (2位) | 0.0158730158 (3位) | 0.0320020481 |
| 3位 | 2 | ネコの持ち運びはネコ用キャリーバッグ。 | 0.0158730158 (3位) | 0.0161290322 (2位) | 0.0320020481 |
| 4位 | 3 | ネコの住処はキャットハウス。 | 0.015625 (4位) | 0.015625 (4位) | 0.03125 |
| 5位 | 5 | ねこの健康体は2Kg程度です。 | 0 (ヒットしない) | 0.0153846153 (5位) | 0.0153846153 |
コーパスレコード番号1
- BM25検索 と ベクトル検索 の両方で 1位 のため、RRF統合スコア は 1位 となります。
コーパスレコード番号6(BM25検索: 2位、ベクトル検索: 3位) と コーパスレコード番号2(BM25検索: 3位、ベクトル検索: 2位)
- RRF統合スコア が同点(
0.0320020481)になりますが、本機能ではBM25検索の結果を先に集計マップへ登録するため、登録順が維持されて コーパスレコード番号6が 2位、コーパスレコード番号2が 3位 になります。コーパスレコード番号5
- BM25検索ではヒットしないため BM25検索のスコアは
0として扱われ、ベクトル検索(5位)のスコアのみが加算されて 5位 にランクインします。
ルーティング判定に使用する しきい値 の求め方
本機能では Augmentation (拡張) に相当する部分を
質問に対する回答が、コーパス に「あるか/ないか」を判定する ルーティング判定 として実装しており
コーパス に 回答が「あるか/ないか」を判定するために、しきい値 を使用します。
しきい値 とは
BM25関連度スコア と コサイン類似度スコア を RRF により統合すると、RRF統合スコア によって回答に順位が付きます。
決めておきたい要件は、どのくらいの RRF統合スコア なら コーパス から抽出した回答を使うかという 境界値 です。
この 境界値 を本機能では しきい値 としています。
しきい値 を求めるための データモデル
※ データモデル の概要は データモデル を参照してください。
しきい値 を求めるために使用する データモデル は 代表クエリ と コーパス になります。
代表クエリ には、カテゴリ と 質問 と 回答 をあらかじめ設定しておきます。
| 代表クエリレコード番号 | カテゴリ | 質問 | 回答 |
|---|---|---|---|
| 1 | ねこ | ネコと遊ぶ道具は何ですか? | 猫じゃらしは、ネコと遊ぶ玩具です。 |
| 2 | ねこ | ネコの持ち運びには何を使いますか? | ネコの持ち運びはネコ用キャリーバッグ。 |
| 3 | ねこ | ネコの住処は何ですか? | ネコの住処はキャットハウス。 |
| 4 | ねこ | 猫の肥満体は何キロからですか? | 猫の肥満体は6Kg以上です。 |
| 5 | ねこ | ねこの健康体は何キロですか? | ねこの健康体は2Kg程度です。 |
| 6 | ねこ | ネコが集まる現象は? | ネコがネコを呼んで、ネコが集まる。 |
代表クエリレコード とは
代表クエリにおける1行分のデータを指します。
しきい値 を求めるための 処理フロー
評価レポート作成スクリプト により、代表クエリレコードの カテゴリと質問 で
コーパスに対して BM25検索、ベクトル検索、RRF統合 を実行します。
代表クエリレコード の回答と RRF統合スコア に紐付く回答が一致した場合、その RRF統合スコア を保持します。
保持した RRF統合スコア の最小値を推奨しきい値として評価レポートに出力します。
最小値とする理由は、推奨しきい値を「正解」の最低ラインの境界値とするためです。
最終結果
代表クエリレコードごとの「カテゴリと質問」と「コーパス」の BM25検索、ベクトル検索、RRF統合 の実行結果は以下となります。
| 代表クエリレコード番号 | BM25関連度スコア | コサイン類似度スコア | RRF統合スコア |
|---|---|---|---|
| 1 | 2.1327035954 | 0.8903327296 | 0.0327868852 |
| 2 | 2.1207016327 | 0.9290309870 | 0.0327868852 |
| 3 | 2.3463696327 | 0.9440148163 | 0.0327868852 |
| 4 | 3.7410913843 | 0.9524691793 | 0.0327868852 |
| 5 | 3.7410913843 | 0.9550991058 | 0.0327868852 |
| 6 | 2.2082327853 | 0.9122421833 | 0.0327868852 |
この結果は、代表クエリレコードそれぞれに対する 1位 の BM25関連度スコア、コサイン類似度スコア、RRF統合スコア です。
この例で RRF統合スコア がすべて 0.0327868852 となるため、推奨しきい値は 0.0327868852 になります。
本機能では、この値を小数点以下6桁に丸め(この例では 0.032787)評価レポートの推奨しきい値として出力します。
なお、評価レポートの推奨しきい値の運用方法は 検索精度の評価・調整 を参照してください。
BM25検索 と ベクトル検索 のハイブリッド方式とした理由
ハイブリッド方式とした理由は
これらの検索方式が、お互いを補完するケースがあるからです。
ではどのようなケースで補完するのかを、以下に説明します。
質問番号2のケース
- 想定する RRF統合スコア の 1位
- E7428 は、排他マーカーの破棄後に常駐タスクを開始すると解消します。
BM25検索 の検索結果
| ソート順 | コーパスレコード番号 | 回答 | BM25関連度スコア |
|---|---|---|---|
| 1位 | 14 | E7428 は、排他マーカーの破棄後に常駐タスクを開始すると解消します。 | 1.7441906337 |
語彙検索(7428) により コーパスレコード番号14 のみヒットして 1位 になります。
ベクトル検索 の検索結果
| ソート順 | コーパスレコード番号 | 回答 | コサイン類似度スコア |
|---|---|---|---|
| 1位 | 13 | E7427 の復旧手順は、ディスク容量を確保してサービスを再起動することです。 | 0.8914062327 |
| 2位 | 11 | E7425 の復旧手順は、接続先を確認してサービスを再起動することです。 | 0.8908948972 |
| 3位 | 12 | E7426 の復旧手順は、認証情報を更新してサービスを再起動することです。 | 0.8887737172 |
| 4位 | 10 | E7424 の復旧手順は、キャッシュを削除してサービスを再起動することです。 | 0.8871443447 |
| 5位 | 14 | E7428 は、排他マーカーの破棄後に常駐タスクを開始すると解消します。 | 0.8852482336 |
意味検索では、質問の「サーバーのリセット」と意味が近い「サービスを再起動」を含む コーパスレコード番号13・11・12・10 の評価が高くなるため、「サービスを再起動」を含まない コーパスレコード番号14 は上位5件中の最下位(5位)になります。
RRF統合
| 総合ランキング | コーパスレコード番号 | 回答 | RRF統合スコア |
|---|---|---|---|
| 1位 | 14 | E7428 は、排他マーカーの破棄後に常駐タスクを開始すると解消します。 | 0.0317780580 |
| 2位 | 13 | E7427 の復旧手順は、ディスク容量を確保してサービスを再起動することです。 | 0.0163934426 |
| 3位 | 11 | E7425 の復旧手順は、接続先を確認してサービスを再起動することです。 | 0.0161290322 |
| 4位 | 12 | E7426 の復旧手順は、認証情報を更新してサービスを再起動することです。 | 0.0158730158 |
| 5位 | 10 | E7424 の復旧手順は、キャッシュを削除してサービスを再起動することです。 | 0.015625 |
コーパスレコード番号14 の RRF統合スコア は、BM25検索 と ベクトル検索 の両方の検索結果に含まれるため
0.0317780580 = 1 ÷ (60 + BM25検索 のソート順(1位) ) + 1 ÷ (60 + ベクトル検索 のソート順(5位) ) で 1位 になります。
よって RRF統合スコア の 1位 は E7428 は、排他マーカーの破棄後に常駐タスクを開始すると解消します。 になります。
質問番号3のケース
- 想定する RRF統合スコア の 1位
- E7426 の復旧手順は、認証情報を更新してサービスを再起動することです。
BM25検索 の検索結果
| ソート順 | コーパスレコード番号 | 回答 | BM25関連度スコア |
|---|---|---|---|
| - | - | - | - |
「質問と回答」に含まれる「トークン」が一致しないため、ヒットしません。
ベクトル検索 の検索結果
| ソート順 | コーパスレコード番号 | 回答 | コサイン類似度スコア |
|---|---|---|---|
| 1位 | 12 | E7426 の復旧手順は、認証情報を更新してサービスを再起動することです。 | 0.8463983900 |
| 2位 | 13 | E7427 の復旧手順は、ディスク容量を確保してサービスを再起動することです。 | 0.8342800377 |
| 3位 | 8 | E7422 の復旧手順は、サービスを再起動することです。 | 0.8313923606 |
| 4位 | 11 | E7425 の復旧手順は、接続先を確認してサービスを再起動することです。 | 0.8310487512 |
| 5位 | 9 | E7423 の復旧手順は、設定ファイルを修正してサービスを再起動することです。 | 0.8289633048 |
意味検索(パスワード ≒ 認証 と 入れ替え ≒ 更新) により コーパスレコード番号12 が 1位 になります。
RRF統合
| 総合ランキング | コーパスレコード番号 | 回答 | RRF統合スコア |
|---|---|---|---|
| 1位 | 12 | E7426 の復旧手順は、認証情報を更新してサービスを再起動することです。 | 0.0163934426 |
| 2位 | 13 | E7427 の復旧手順は、ディスク容量を確保してサービスを再起動することです。 | 0.0161290322 |
| 3位 | 8 | E7422 の復旧手順は、サービスを再起動することです。 | 0.0158730158 |
| 4位 | 11 | E7425 の復旧手順は、接続先を確認してサービスを再起動することです。 | 0.015625 |
| 5位 | 9 | E7423 の復旧手順は、設定ファイルを修正してサービスを再起動することです。 | 0.0153846153 |
BM25検索 はヒット件数 0件 ですが、コーパスレコード番号12 は ベクトル検索 で 1位 のため
0.0163934426 = 0 + 1 ÷ (60 + ベクトル検索 のソート順(1位) ) で 1位 になります。
よって RRF統合スコア の 1位 は E7426 の復旧手順は、認証情報を更新してサービスを再起動することです。 になります。
結論
この二つのケースから BM25検索 と ベクトル検索 のハイブリッド方式を採用した理由は
「語彙検索による方式」と「意味検索による方式」を組み合わせることで、お互いの弱点を補い合うことです。
BM25検索を LIKE検索 で代替しない理由
LIKE検索とBM25検索は、検索に対する目的が違います。
目的の違い
- LIKE検索
-
LIKE '%文字列%'のようなパターンを使い、指定した文字列が回答に含まれるかを判定します。
-
- BM25検索
- 質問 と 回答 に含まれるトークンの一致を基に、逆文書頻度(IDF)、文書長による補正項(分母 denom)、加点倍率を用いて BM25関連度 をスコア化し、ランク付けします。
LIKE検索 と BM25検索 の比較
| LIKE検索 と BM25検索 の比較項目 | LIKE検索 | BM25検索 |
|---|---|---|
| 検索方式 | 文字列パターン照合 | 語彙検索 |
| 検索対象 | 文字列 | トークン |
| 評価方法 | パターンに一致するか | 質問 と 回答 に含まれる トークン が一致するか |
| 関連度の計算 | デフォルトではしない | 逆文書頻度(IDF)、文書長による補正項(分母 denom)、加点倍率、BM25関連度スコア |
| ランク付け | デフォルトではしない | BM25関連度スコア に基づく |
後続処理である RRF統合 との組み合わせ
後続の RRF統合 では検索結果の「ランク」を用いるため
関連度順のランク付けを行わない LIKE検索 では RRF統合 と組み合わせることができません。
結論
LIKE検索 をベースに BM25検索 のスコアリングシステムを実装することは、技術的には可能です。
しかし、それはもはや「LIKE検索」ではなく、RDBMS上に構築した「BM25検索」そのものと言えます。
語彙検索において、関連度に応じたランク付け(スコアリング)が必要なユースケースでは
BM25検索 を採用する方が 適材適所 であるため、LIKE検索 の採用を除外しました。
用語解説
| 用語 | 役割と意味 |
|---|---|
| 語彙検索 (Lexical Search) | 質問と検索対象の文書に含まれる単語(トークン)の一致を基準に、関連性を評価する検索方式です。 |
| IDF (Inverse Document Frequency / 逆文書頻度) | 特定の単語が文書集合(コーパス)全体の中でどれだけ珍しいかを表す指標です。多くの文書に共通して現れる一般的な単語の重みを下げ、少数の コーパスレコード にしか現れない特徴的な単語の重みを高くする役割を持ちます。 |
| DF (Document Frequency / 文書頻度) | 検索対象の文書集合(コーパス)全体の中で、特定の単語が少なくとも1回以上出現する コーパスレコード数です。 |
| TF (Term Frequency / 単語頻度) | 検索対象の1つの文書内に、特定の単語が何回出現したかを表す回数です。 |
| 飽和 (Saturation) | 同じ単語の出現回数(TF)が増えても、スコアが正比例して無限に増え続けるのではなく、一定の値(上限 $k_1 + 1$)に向かって増加幅が緩やかになり頭打ちになる性質です。 |
| 平滑化 (Smoothing) | 分母が $0$ になる(ゼロ除算)ことや極端な値の変動を防ぐために、分子・分母に小さな定数(BM25のIDF式では $0.5$)を加える統計的な補正手法です。 |
| BM25関連度 | 本機能においては、「質問の トークナイズ(分かち書き)」と「コーパス に含まれる トークナイズ(分かち書き)」を比較し質問のトークンがどの程度一致しているかを数値化します。 |
| 意味検索 (Semantic Search) | 質問と文書を ONNXモデル でベクトル化し、ベクトル空間上での近さを評価することで意味的に類似した文書を検索する方法です。 |
| L2正規化 (L2 Normalization) | ベクトルの向き(各次元の比率)を保ったまま ベクトルの長さを三平方の定理(各次元の2乗和の平方根)で 1 になるように揃える処理です。 |
| 内積 (Dot Product) | 2つのベクトルの同じ位置(次元)にある数値をそれぞれ掛け合わせ、すべて足し合わせた値です。L2正規化済みのベクトル同士では、内積がそのまま コサイン類似度 と一致します。 |
| コサイン類似度 (Cosine Similarity) | 本機能においては、「質問に対する ベクトル値」と「コーパス に埋め込まれる ベクトル値」の 向き(意味) がどの程度近いかを数値化します。 向き(意味) が近いほど、質問と コーパス の意味が近いと判断します。 |
まとめ
Part5 では 質問から回答までの処理フロー で実施している「検索 から ルーティング判定」までの仕組みを説明しました。
Part6 では、本機能におけるセキュリティ対応を 課題一覧 の 課題No.4 脆弱性対応 と紐付けて説明する予定です。
おわりに
長年携わった Java Web アプリケーション(DB CRUD) と違って
本機能で特に重要だと感じたのは、検索(BM25検索 + ベクトル検索 + RRF統合)であり
コア作業は「生成AIに何を回答させ、そのために何を外部知識として持たせるのか」を決めることだと思います。
つまり定型的なアプリケーション構築ではなく、正解が決まっていないアプリケーション構築である印象です。
投稿する Qiita記事も「数式」だけを書くのではなく「なんの目的で、なぜこうしたのか」が重要で
これを抽象化して記事に落とすことはハードルが高い、と改めて感じました。
いい歳なんですけど、毎日が修行です。(笑)






