これは何か?
Movable Typeの管理画面の検索で「自然言語で検索」を選択できるようにするプラグインです。
OpenAI の text-embedding-3-large で記事やコンテンツデータをベクトル化して、先にベクトル検索で絞り込んだうえで、その結果から Jev を使って自然言語でのフィルタリングとリランキングを行う形で実現しています。(ベクトルは既存のDBに保存し、類似度の計算はPerl側で行っています。数千件程度のデータであれば実用になりそうなので、いまはここまでにしていますが、さらに件数が増える場合にはベクトル検索に対応したDBを使うなどの工夫が必要になると思われます。)
Jev への入力トークンは若干大きめになっているので、らしさを十分に引き出せているかどうかは微妙なところはあるものの、それでも追加学習なしで、低コストかつ高速(そして割と一貫性のある結果)でフィルタリングとリランキングを実現できていて、まあまあ実用的なものになっている気がします。
動作デモ
とあるアニメーションの感想記事(内容は全てダミー)を入れて、プラグインを使って検索したときのデモが以下になります。
- 映画/テレビアニメ:各50件
- ネタバレあり/なし:各50件
- 肯定的/否定的:各50件
「直接のキーワード指定」や「専用の構文」を使うことなしに、検索ができていることが分かります。
実装の概要
最初の段階では、検索のたびに全ての記事を Jev に渡して条件を満たすかどうかを判定する方法を試してみて、結果は期待したものになってはいました。しかし 1記事5,000トークンとして、5,000記事を評価すると、検索1回で$1というコストになり、それは実験的なプラグインとはいえちょっとコストが嵩みすぎるという感じがしたので、ベクトル検索で先に絞り込むことにしました。
記事ごとのベクトルは、データから MT で検索対象となる項目を取り出し、HTML を取り除き、項目名と値をまとめた JSON を text-embedding-3-large に渡して生成します。(それ以上の細かい分割や要約は行わない)
検索時には、入力された自然文も同じモデルでベクトル化します。検索対象の保存済みベクトルを100件ずつまとめて取得し、検索文とのコサイン類似度を Perl で計算していく感じです。(とても素朴な実装)
ベクトル検索で上位50件を抽出し、この候補を Jev に渡してそれぞれについて2種類の質問をします。
Noul:検索条件を満たしているか。0〜1の値を返してもらう。Score:探している情報との関連度はどのくらいか。0〜4の値を返してもらう。
結果を受け取ったら、Noul がしきい値以上のものだけを残し、Score の高い順に並べます。
Jev への送信は、入力サイズの制限と、判定に関係のない情報が増えることによる精度への影響を考慮して、デフォルトで5件ずつにしています。入力サイズの制限に収まれば、候補50件を10リクエストに分けて処理することになります。この API の呼び出しはパフォーマンスのボトルネックになるので、最大5リクエストを並列に実行するような感じにしています。
検索毎のコストの試算
検索1回あたりのコストを試算してみます。(記事毎のベクトルを初回に生成するコストや、記事更新時の再生成のコストは省略します。)
- 1記事あたり5,000トークンと仮定
- ベクトル検索で上位50件を抽出して、Jev で評価する
記事の入力は、5,000トークン × 50件 = 250,000トークン となります。
Jev の公式料金は、入力100万トークンあたり0.042ドル(出力は無料)なので、計算すると 250,000 ÷ 1,000,000 × $0.042 = $0.0105 です。(検索文のベクトル化の費用はとても小さいのでここでは無視。)100回検索して $1.05 で済む計算になります。
これは、入力単価で比較すると、GPT-5.4 nano の約5分の1、GPT-5.4 mini の約18分の1に相当します。
