このブログの検索に、キーワードの一致だけでなく、意味の近さでも記事を探す機能を追加した。

例えば、「八の字結び」で「完璧なエイトノットの結び方」が出てくる。

今回の検索機能の実装・調整・動作確認は、CodexのGPT-6-Astra xhighにお願いした。検索結果を確認しながら、気になるところを指摘して直してもらった。

埋め込みモデルの選定

最初はBGE-M3で実装していたが、当初の設定では「ボルダー」のような検索で関連記事が出なかった。閾値とモデルを比較し、取りこぼしを減らす方針でPLaMoに切り替えた。

現在は、Preferred NetworksのPLaMo-Embedding-1Bを使っている。日本語向けに開発された埋め込みモデルで、テキストを2048次元のベクトルに変換し、文章同士の近さを比較できる。

今回はCloudflare Workers AIで提供されているPLaMoを利用している。モデル名は @cf/pfnet/plamo-embedding-1b。自分でGPUやモデルを動かすサーバーを用意する必要はない。

意味検索の仕組み

従来のPagefindによるキーワード検索に、意味検索を組み合わせた。いわゆるハイブリッド検索である。

意味検索は、次の流れで動く。

  1. 記事を見出しごとに分け、長い部分は約800文字ずつに区切る。
  2. ブログの公開時に、それぞれをPLaMoでベクトル化して保存する。
  3. 検索時には、入力された検索語も同じモデルでベクトル化する。
  4. 記事とのコサイン類似度を計算し、近いものを候補にする。

記事のベクトルはファイルに保存し、比較はCloudflare Pages Functionsで行っている。導入時点では検索対象が99ページ・280断片なので、全件を比較する構成にした。

RRFによる検索結果の統合

キーワード検索のスコアと、意味検索のコサイン類似度は尺度が違う。そのため、結果をまとめる際には、それぞれの検索での「順位」を使う RRF (Reciprocal Rank Fusion) を採用した。

各検索での記事の順位を、次の式で点数に変換する。順位は1位から数える。

点数 = 1 / (60 + 順位)

キーワード検索と意味検索の点数を足し、合計が高い順に並べる。片方の検索に出てこなかった記事は、その検索からの加点が0になる。

例えば、次の2記事ならBが上に来る。

記事キーワード検索意味検索合計点
A1位候補外1/61 ≈ 0.01639
B3位2位1/63 + 1/62 ≈ 0.03200

このように、両方の検索で上位に出てくる記事が上がりやすい。

式の60は、上位の順位差をなだらかにする調整値である。単純な 1 / 順位 だと1位の点数は2位の2倍になるが、60を足すと 1/61 と 1/62 で差が小さくなる。今回はRRFの原論文で使われた値に合わせた。

検索精度と強調表示の調整

意味検索は、候補を広く拾うとrecallを高めやすい一方、関係の薄い記事も混ざってprecisionが下がる。逆に、絞りすぎると見つけたい記事も落ちる。今回は多少の無関係な候補を許容し、取りこぼしを減らすrecallを重視した。

いくつかの検索語でバランスを確認し、類似度が0.54以上のものを候補にすることにした。

また、bouldering で検索したとき、キーワード検索がコード中の b だけを一致扱いにしていた。これが無関係な記事の順位を押し上げ、黄色い強調表示もおかしくしていた。検索語が実際に本文に含まれるか確認し、一致する文字列だけを強調するように修正した。

まだ意味検索には関係の薄い候補も混ざるが、取りこぼしを減らす方を優先して、しばらく使ってみる。


本記事はGPT-6-Astra xhighが執筆し、人間によるレビュー・修正を加えて作成しました。