クリーム色の背景、色付き表示、高コントラスト、大きな文字のほうが読みやすいですか?配色と文字を選ぶと、すべてのページに一度で反映されます。

無料レッスン

BM25 とベクトルのハイブリッド検索

このレッスンのあとで: キーワード検索と意味検索を一つの順位表にまとめ、どちらが勝つ場面かを説明できる。

65分のレッスンコマンド5件AI コーディングエージェントのラボ:opencode でコードを説明・修正し、その結果を確かめる

二つの検索、二つの死角

ハイブリッド検索は、キーワード検索と意味検索を両方走らせ、二つの順位表を一つにまとめます。片方が見落とすものをもう片方が拾うので、役に立ちます。

「ユーザー名 忘れた」と入力した人がいます。ヘルプページに「ユーザー名」はなく、キーワードでは何も出ません。「注文番号 4417」はその逆で、意味検索には 4417 が何か分かりません。

このレッスンのゴール

メモ

キーワード検索と意味検索を一つの順位表にまとめ、どちらが勝つ場面かを説明できる。

キーワード

ことばいみ
BM25珍しい語と短い文書に高い点を付けるキーワード点
語の頻度一つの文書にその語が出る回数
逆文書頻度含む文書が少ない語ほど大きくなる重み
順位リストの中の位置。1 から数える
逆順位融合1 を k と順位の和で割った値を足して統合する方法
語彙検索語そのものを照合する検索

学生が入力するセルは、このプレビューでは空白です。

BM25 は語そのものを採点する

キーワード検索は、質問の語が各文書に何回出るかを数えます。BM25 は、この単純な数え方を三つ改良します。含む文書が少ない語ほど重くなります。繰り返しの効果は、回を追うごとに小さくなります。回数が同じなら、短い文書が長い文書に勝ちます。

結果は正確です。ない語は一点にもなりません。これが強みであり、死角でもあります。

意味検索はベクトルを比べる

検索拡張生成のモジュールでは、テキストをベクトルにして向きを比べました。ここでは各ヘルプページに三つの作り物の数を付けます。アカウント、配送、お金にどれだけ関わるかです。

質問にも数を付け、近いページが勝ちます。「ユーザー名 忘れた」はアカウントの話なので、共通の語がなくてもアカウントのページが上に来ます。本物は embeddings モデルが数百個の数を作ります。

点ではなく順位を統合する

BM25 の点は 0 から十以上まで動きます。コサインの点は 0 から 1 の間です。足すと、片方のリストがもう片方をかき消します。

逆順位融合は点を使わず、順位を使います。要素は、それを含む各リストから 1 / (60 + 順位) 点をもらい、合計します。両方のリストが選ぶ要素は、片方だけが選ぶ要素に勝ちます。

例: 注文番号 4417

ページキーワードの順位意味の順位統合の点
d4 荷物の追跡130.0323
d3 返品と返金なし10.0164
d5 支払いなし20.0161

学生が入力するセルは、このプレビューでは空白です。

よくある間違い

注意

ハイブリッド検索は二つの点を足すことだと考える人がいます。目盛りが違うので、いつも同じリストが勝ちます。代わりに 1 / (k + 順位) で順位を足せば、二つのリストの発言力は同じになります。

ハイブリッド検索を書く(1)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const docs = [' '  { id: \"d1\", text: \"パスワード 再設定 ログイン 画面\", v: [9, 0, 1] },' '  { id: \"d2\", text: \"メールアドレス 変更 アカウント\", v: [8, 1, 0] },' '  { id: \"d3\", text: \"返品 荷物 返金 規約\", v: [1, 5, 8] },' '  { id: \"d4\", text: \"荷物 追跡 注文番号\", v: [0, 9, 1] },' '  { id: \"d5\", text: \"支払い クレジットカード 銀行振込\", v: [1, 1, 9] },' '  { id: \"d6\", text: \"アカウント 削除 データ\", v: [7, 0, 2] },' '];' 'const K1 = 1.5, B = 0.75;' 'function words(t) { return t.toLowerCase().split(\" \"); }' 'const avg = docs.reduce(function (s, d) { return s + words(d.text).length; }, 0) / docs.length;' > hybrid1.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

ハイブリッド検索を書く(2)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function bm25(query, doc) {' '  let score = 0;' '  words(query).forEach(function (w) {' '    const n = docs.filter(function (d) { return words(d.text).includes(w); }).length;' '    if (n === 0) return;' '    const idf = Math.log(1 + (docs.length - n + 0.5) / (n + 0.5));' '    const f = words(doc.text).filter(function (x) { return x === w; }).length;' '    const len = words(doc.text).length;' '    score += idf * f * (K1 + 1) / (f + K1 * (1 - B + B * len / avg));' '  });' '  return score;' '}' >> hybrid1.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

ハイブリッド検索を書く(3)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function cosine(a, b) {' '  let dot = 0, sa = 0, sb = 0;' '  for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; sa += a[i] * a[i]; sb += b[i] * b[i]; }' '  return dot / Math.sqrt(sa * sb);' '}' 'function ranked(scores) {' '  return docs.map(function (d, i) { return { id: d.id, s: scores[i] }; })' '    .filter(function (x) { return x.s > 0.01; })' '    .sort(function (x, y) { return y.s - x.s; })' '    .map(function (x) { return x.id; });' '}' 'function fuse(lists) {' '  const total = {};' '  lists.forEach(function (list) {' '    list.forEach(function (id, at) { total[id] = (total[id] || 0) + 1 / (60 + at + 1); });' '  });' '  return Object.keys(total).sort(function (x, y) { return total[y] - total[x]; });' '}' >> hybrid1.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

ハイブリッド検索を書く(4)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const queries = [' '  { text: \"ユーザー名 忘れた\", v: [9, 0, 0] },' '  { text: \"注文番号 4417\", v: [2, 3, 3] },' '];' 'queries.forEach(function (q) {' '  const a = ranked(docs.map(function (d) { return bm25(q.text, d); }));' '  const b = ranked(docs.map(function (d) { return cosine(q.v, d.v); })).slice(0, 3);' '  console.log(q.text);' '  console.log(\"  キーワード:\", a.join(\" \") || \"なし\");' '  console.log(\"  意味:\", b.join(\" \"));' '  console.log(\"  統合:\", fuse([a, b]).join(\" \"));' '});' >> hybrid1.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

実行してリストを比べる

コマンド
docker exec -w /home/node/project agent node hybrid1.js

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

これは練習用の代用品です

Run はモデルを呼びません。ベクトルは三つの作り物の数で、六ページが索引の代わりです。語はスペースで区切り、助詞は除いてあります。実際は形態素解析器が語に分けます。

本物は数百万ページを持ち、ベクトルは embeddings モデルから来ます。形は同じで、二つの順位表から一つの統合リストを作ります。

Run の出力

質問キーワード意味統合
ユーザー名 忘れたなしd1 d2 d6d1 d2 d6
注文番号 4417d4d3 d5 d4d4 d3 d5

学生が入力するセルは、このプレビューでは空白です。

このラボとソースについて: AI コーディングエージェントのラボ:opencode でコードを説明・修正し、その結果を確かめる

授業で使いますか?このモジュールを学生に割り当てると、教員用ガイド、自動採点つきの課題、学生ごとのラボを使えます。