このブログの検索に、キーワードの一致だけでなく、意味の近さでも記事を探す機能を追加した。
例えば、「八の字結び」で「完璧なエイトノットの結び方」が出てくる。
今回の検索機能の実装・調整・動作確認は、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によるキーワード検索に、意味検索を組み合わせた。いわゆるハイブリッド検索である。
意味検索は、次の流れで動く。
- 記事を見出しごとに分け、長い部分は約800文字ずつに区切る。
- ブログの公開時に、それぞれをPLaMoでベクトル化して保存する。
- 検索時には、入力された検索語も同じモデルでベクトル化する。
- 記事とのコサイン類似度を計算し、近いものを候補にする。
記事のベクトルはファイルに保存し、比較はCloudflare Pages Functionsで行っている。導入時点では検索対象が99ページ・280断片なので、全件を比較する構成にした。
RRFによる検索結果の統合
キーワード検索のスコアと、意味検索のコサイン類似度は尺度が違う。そのため、結果をまとめる際には、それぞれの検索での「順位」を使う RRF (Reciprocal Rank Fusion) を採用した。
各検索での記事の順位を、次の式で点数に変換する。順位は1位から数える。
点数 = 1 / (60 + 順位)
キーワード検索と意味検索の点数を足し、合計が高い順に並べる。片方の検索に出てこなかった記事は、その検索からの加点が0になる。
例えば、次の2記事ならBが上に来る。
| 記事 | キーワード検索 | 意味検索 | 合計点 |
|---|---|---|---|
| A | 1位 | 候補外 | 1/61 ≈ 0.01639 |
| B | 3位 | 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が執筆し、人間によるレビュー・修正を加えて作成しました。