昨今流行りのAIブームに乗っかって、素人なりに関連検索機能を作ったのでそのアウトプット記事です。
力技が過ぎるせいか、はたまた用途がニッチすぎるせいか精度はイマイチ…。
なろう小説API
「小説家になろう」が提供しているAPIです。
主に同サイトに登録されたWEB小説情報の取得を行うことができます。
それを使って作ったサイトがこちら。
小説を探そう
https://yomou-db.shimo-codex.com/
【なろう小説API】PHPでなろうAPIを叩いて「小説家になろう」独自検索フォームを作成する
https://zenn.dev/saikofall/articles/2c0c723bd3363f
それは置いといて。
今回はここに関連検索みたいなのを入れてみたよってお話です。
今回使う単語集
text-embedding-3
OpenAIが提供しているモデルの一種で、文章をベクトル化してくれます。
ベクトル化については私もよく分かってないんで適当な説明になりますが、AIくんとかが理解してくれるように数値化する作業…みたいな?
日本語は人間が理解できる言語の1つなのでAIくんは理解不能ですが、数字にしてあげれば分かるよね、的な感覚。
で、そのベクトル化をしてくれるモデル(=埋め込みモデル)は複数あるんですが、今回はAPIって形でOpenAIが提供しているtext-embedding-3を使っています。
埋め込みモデルによって生成されるベクトルは違うと思いますが、私は今回がお初でtext-embedding-3しか触ったことないので、他がどういう風に値を出してくれるかは知りません。
text-embedding-3はsmallとlargeがあり、後者のほうが高額ですがより精度の高い文章ベクトルを生成してくれます。
後述しますが、今回はわりと長めの文章をベクトル化する必要があるので、金額的にお安いsmallを選択します。
トークン(金額)について
OpenAIでは1トークンにつき〇〇ドル、という形式での課金形態になります。
smallの場合は「$0.020/1M トークン」です。
分かりづらいですが、1トークンはほぼ1単語になります。
ただしここは言語的な壁があり、英語は文章の区切りがほぼそのままトークン数になります。
例えば、「I love you.」という文章をAPIに流し込むとしたら、そのまま4トークンになります。
これを日本語に翻訳したとして、「私はあなたが好きです。」の場合は6トークンになります。
なぜ増えるかというと、日本語は文章を名詞とか動詞とかに区切って(=形態素解析)判断する必要があるため「は」とか「が」も1トークンとカウントされる場合があるからです。
詳しくは公式がトークナイザーというものを用意しており、ある文章がどの程度のトークン数になるのかを示してくれています。
https://platform.openai.com/tokenizer
ということもあるため、やりたいことのためにsmallを選んでいます。
関連検索について
前置きが長くなりましたが本題です。
今回やりたいことの流れは以下の通り。
1.ユーザーは基準作品となる小説Aを選択する
2.特定の判断基準によって絞られた小説群の中から、基準作品(=作品A)との「あらすじ」一致率を掲載する
これだけです。
つまり、自分が好きなWEB小説と似たあらすじをもつ作品を探そう!というのがコンセプトです。
最終的に出力されるのはこういう感じのものです。

「AIあらすじ一致率」と称されているものが表示されます。
全体の流れは下記のように構成しました。
1.基準作品をベクトル化(text-embedding-3を活用)
2.なろう小説APIによって取得した小説群をキーワード一致率や総合一致率で絞り込む(設定した閾値を越えない作品は除外する)
3.残った作品群をベクトル化
3-1.この時、作品群の中で既にベクトル化及びデータベース登録したものはそれを取得する
3-2.データベースにないものはベクトル化を行った上でデータベース登録する
4.「基準作品の文章ベクトル」と「取得作品群の文章ベクトル」からそれぞれコサイン類似度を取得
5.取得したコサイン類似度をパーセント表記で出力
テキスト埋め込み
/**
* テキストの埋め込みベクトルを取得する関数
* @param string $text テキスト
* @return array 埋め込みベクトル
*/
function getEmbedding($text) {
$apiKey = '***';
$url = 'https://api.openai.com/v1/embeddings';
$data = [
'model' => 'text-embedding-3-small',
'input' => $text
];
$options = [
'http' => [
'header' => [
"Content-type: application/json",
"Authorization: Bearer $apiKey"
],
'method' => 'POST',
'content' => json_encode($data),
],
];
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
if ($result === FALSE) {
die('Error occurred');
}
$response = json_decode($result, true);
return $response['data'][0]['embedding'];
}
基本的には引数$textをAPIにかけるだけです。
この関数を通すと、[0.45,0.22…]みたいなのが返却されます。
それが文章ベクトルで、例えば「異世界」を文章ベクトル=数値化したものが返される感じです。
あとはこれをデータベースに保存する関数もありますが、そっちは特にAI関係ないので割愛します。
コサイン類似度を計算
数学から逃げてきたのでよく分かってないんですが、「あらすじが似てるかどうか」を計算する必要があります。
そこで用いられるのがコサイン類似度計算です。
さっき取得したベクトルについて、「作品Aのベクトル」と「作品Bのベクトル」を比較するための計算です。
この2つのベクトル間で類似度を計算することで、1に近いほど類似性がある、という結果を取得することができます。
計算式はよく分からんので、知りたい人はコサイン類似度で調べてください。
具体的な数式が出てきますので。
/**
* コサイン類似度を計算する関数
* @param array $vectorA ベクトルA
* @param array $vectorB ベクトルB
* @return float コサイン類似度
*/
function cosineSimilarity($vectorA, $vectorB) {
$dotProduct = 0;
$magnitudeA = 0;
$magnitudeB = 0;
foreach ($vectorA as $key => $value) {
$dotProduct += $value * ($vectorB[$key] ?? 0);
$magnitudeA += pow($value, 2);
}
foreach ($vectorB as $value) {
$magnitudeB += pow($value, 2);
}
if ($magnitudeA == 0 || $magnitudeB == 0) {
return 0;
}
return $dotProduct / (sqrt($magnitudeA) * sqrt($magnitudeB));
}
この関数にベクトルAとBを渡すことで、コサイン類似度が返却されます。
本来はPythonなりでコサイン類似度を計算してくれるライブラリがあるのですが、現状環境だと結構面倒なのでPHPで作ってもらいました。
ここで返却される値が1に近いほど、両作品間は似ているということになります。
実際の精度について
ということで作ってみたのですが、小説のあらすじという特殊性もあり?そこまで精度は高くないように感じられました。
あとはシンプルに多種多様なので、似たあらすじというものが少ないこともあります。
また、「あらすじが似ている」というのは人によって解釈が異なる、というか求めているものが違う部分があります。
ある人は「境遇が似た作品を探したい」かもしれませんし、またある人は「文体が似た作品を探したい」ということなのかも…。
そもそもじゃあ「ベクトル化が何を基準にしてるか」が良く分かってないので、今回行った類似度確認が上記どちらか?はたまは別なのか?という問題もあるんですよね。
多分総合的に考えて、ということで文体や単語や文脈理解全体での一致をチェックしてるとは思うのですが。
コンセプトの問題
多分これに関しては考え方、コンセプトの違いがあるかなと考えてます。
例えば今回は「基準作品のあらすじ」をベースにして似た作品を探す、みたいな作り方をしました。
結果としてあんまりアテにならなくない?という結果に落ち着いたかな、と個人的には思ってます。
関連検索の真価は、「単語検索をしたときに、その単語に関連した文章を引っ張ってこれる」なのかなと。
例えばですが、「異世界モノが読みて~~~~」と考えた時、それだけでも下記のパターンが出てきます。
・現地系主人公モノ
・異世界転生モノ
・クラス転移モノ
みたいな。
で、検索にはきっと「異世界 現地主人公 ガールズラブ 残酷な描写」とか細かく入れるんですよね、大抵。
ではそれを「関連検索」すると、文章ベクトルにしているのでAIくんがきっと勝手に判断してくれます。
「異世界」…転生、異なる世界
「ガールズラブ」…GL、百合
って感じで、似たような単語で類推してくれるんじゃないかな~みたいな。
実際、企業とかで用いる場合にはこっちの活用法が大きいと思います。
チャットボットだったり、規程検索とか社内wiki検索とか。
幸い、小説家になろうをはじめとする小説系サイトは「キーワード」なり「タグ」なりがありますし、そっちをとっかかりにすることもできそう。
今はどちらかと言えばそんな感じを検討中です。
上手く行けばキーワード関連検索みたいにしたい所存。
使い道は無限大
今はわりと暇なのでこの辺を使って色々できないか試行錯誤してます。
実際便利すぎるので、AI業界の未来はわりと明るそう。
でも最近は性能が頭打ちに、みたいなことも聞くのでまたブレイクスルーが起きてくれないですかね。
例えば埋め込みモデルの最新版を出してくれたりしたら嬉しいですね。
私は趣味の範疇で行っていますが、結構会社でも応用が利くと思います。
そこまでコストが嵩む話でもないので、関連検索は色々な分野で使えそう。
また、社内に限った話で言えば先ほども出した社内規定の確認とかでも使えそうです。
あとは過去の案件確認とかで似た案件をやったことがないか確認する、とか。
私みたいな木っ端の素人プログラマー(?)でもこんなん作れるので、多分誰でも作れると思います。
今はcopilotにおんぶにだっこでもなんとかなりますし、暇なら遊んでみると結構楽しいです。
暫くはこれで遊ぶつもりなので、また何か作ったらここにアウトプット的な感じで投げます。