自社ブログにAI検索を実装した記録|WordPressの記事をRAGで「質問できる」ようにする

ブログを長く続けていると、「記事は増えてきたけれど、読者は目的の情報にちゃんとたどり着けているのか?」と気になってきます。特に、検索窓にキーワードを入力する形では、言葉にできない「ふわっとした質問」をうまく拾いきれません。例えば「製造業でAIを使って作業を自動化したい。参考になる記事はある?」といった感じの質問です。

そこで今回、このブログ(業務システム.com)の全記事を対象に、AIが関連する記事を探して、記事へのリンク付きで答えてくれる仕組みを実装しました。全ページの右下に表示されるボタンから、チャットを呼び出し、どなたでも利用することができます。

本記事は、そのAI検索を開発した際の開発記録です。検索チャットの仕組みや、投稿・更新した記事の自動登録、一般公開するにあたっての安全対策まで、「なぜそういう設計にしたのか?」を中心に、順に説明していきます。

目次

AIによる検索チャット

まず、今回作ったものについて簡単にまとめます。

  • ブログの全記事(59記事)を対象にした、AIによる検索チャットです。
  • 質問すると関連する記事を探して、出典リンク付きで回答します。
  • 記事を公開・更新・削除すると、自動でその内容が検索に反映されます。
  • すべてのページで右下にボタンとして表示されます。

この検索は、サイト全体の記事を横断して探して答えるものです。今、開いている記事の詳細について答えるチャットではありません。

「RAG」という仕組とは?

生成AIに質問をすると、それらしい答えは返ってくるものの、「その答えはどこから拾ってきたのか」という根拠がわかりません。これではビジネスに関わる重要なケースでは不安が残ります。

そういったときに採用されるのが RAG(Retrieval-Augmented Generation)という仕組みです。少し噛み砕いて説明すると、「自分の記事の中から関連する部分を探し出し、その内容だけを根拠にAIに回答してもらう」というやり方です。こうすることで、「どの記事に基づいた答えなのか」をはっきりと示せます。

これは最近のコーポレートサイトやブログでは既に一般的な手法で、自社で書きためた記事を資産として活かすなら、一番素直な形です。

ここで使われているのが、意味検索(セマンティック検索)と呼ばれる技術です。従来のキーワード検索は「入力した単語と同じ言葉が含まれる記事」を探しますが、意味検索では「言葉そのものが一致していなくても、意味的に近い言葉が含まれている記事」を探し出せます

例: 「従業員の作業を自動化」で質問 ⇨「省人化」「AIエージェント」といった別の言葉で書かれた記事もヒットする

使用した技術は、以下の通りです。

役割使ったもの選んだ理由
画面とAPINext.js(Vercel)画面と処理をひとつにまとめられて、公開が簡単
検索用のデータベースSupabase(pgvector)データ保存と意味検索を1か所で管理できる
文章の数値化text-embedding-3-small日本語でも安定して動き、費用も抑えられる
生成AIClaude(Anthropic)日本語の要約や引用が自然だから
記事の取得WordPress REST API既存のブログに手を入れず、本文を取り出せるから

仕組みの全体像

処理は大きく分けて、「記事を検索できるようにする」段階と、「質問に答える」段階の2つに分かれます。

記事を探せるように準備する

記事をそのまま保存しただけでは、意味検索は機能しません。あらかじめ文章を意味を表す数値(ベクトル)に変換して保存しておく必要があります。

記事を丸ごと1つの数値に変換してしまうと、その記事に含まれる複数の話題が混ざり合って、「ぼんやりした意味」になってしまいます。すると、具体的な質問をしたときに、ぴったりの箇所が見つかりにくくなります。

そこで記事を、見出しなどの小さなまとまり(チャンク)に分け、まとまりごとに数値化しました。こうすると、質問に一番近い部分だけを取り出せるので、検索の精度が上がります。今回は見出し単位で区切り、長い部分はさらに一定の大きさで分割したうえで、文脈が途切れないように前後を少しだけ重ねています。

読者の質問に答える

読者の方が質問を入力すると、その質問も同じように数値化して、意味の近い記事のまとまりを探します。

そして見つかった記事の本文とURLをClaudeに渡し、「この内容にもとづいて、出典番号をつけて回答してください」と依頼する、というのが質問に対する回答の流れです。

開発のハイライト

ここでは、実装の中でも特に重要だった4つの部分を、少し具体的に紹介します。技術的な話が続きますので、開発者で無い方は読み飛ばしていただいても大丈夫です。

Supabase:記事と数値をしまうDB

まず、記事の内容とその「意味の数値」をどこに保存するかですが、今回はSupabaseを使いました。

Supabase は、データベース(PostgreSQL)をクラウドで手軽に使えるサービスです。特に今回の決め手になったのが、pgvectorという拡張機能で、これを有効にすると、ふつうのデータベースに「意味の数値(ベクトル)」をそのまま保存し、しかも「意味が近いものを探す」検索まで、この1か所でカバーできます

データ保存用と検索用にサービスを分ける必要がなく、構成がとてもすっきりするのが利点でした。無料の枠から始められるのも、個人で試すには助かりました。

記事のチャンクを収納しているテーブル

保存の形はシンプルで、記事のまとまりひとつにつき1行、という単位で納められています。ひとつの行には、そのまとまりの「元の文章」「意味の数値」、そして「どの記事のどこか」を示す情報(記事の番号・タイトル・URL)を一緒に持たせています。

ここでひとつ、重要なポイントがあります。意味の数値だけでなく、元の文章もそのまま保存していることです。意味の数値は「探す」ためには使えますが、そこから元の文章を復元することはできません。最終的に読者への回答は元の文章を基に作るので、数値と文章の両方をセットで持っておく必要があるのです。

そして、記事へのリンクを各行に持たせているおかげで、回答の下に「参照した記事」として出典リンクを表示できています。この出典表示こそ、今回いちばん実現したかったことでした。

記事を「意味の数値」に変える(embeddings)

RAGの心臓部が、文章を意味を表す数値の並びに変換するembeddings(埋め込み)の処理です。

今回は OpenAI のtext-embedding-3-small というモデルを使いました。このモデルは、ひとつの文章のまとまりを1536個の数字の並びに変換します。人には意味のない数字の羅列に見えますが、この数字が近いもの同士は「意味が近い」という関係になっていて、この数字の近さを基に意味検索の対象を探しています。

コードとしては、記事のまとまりをまとめて渡し、一度で数値化しています。

TypeScript
const res = await openai.embeddings.create({
  model: 'text-embedding-3-small', // 1536次元
  input: texts, // 1記事分のまとまりをまとめて渡す
});

ここで大切なのは、記事を保存するときも、質問を検索するときも、必ず同じモデルで数値化することです。ものさしが違うと意味の比較ができなくなるためで、もし将来このモデルを変える場合は、過去の全記事を新しいモデルで作り直す必要があります。

Next.js:検索と回答の心臓部

画面と処理は Next.js(TypeScript)で制作しました。

処理の中心となるのが、質問を受け取って回答を返すパートです。流れとしては、「質問を数値化 → 意味の近い記事を検索 → その本文をClaudeに渡して回答」という3ステップで、これをひとつの処理としてつないでいます。

検索の部分は、Supabase(pgvector)に「近い記事を6件探してください」とお願いするだけで済みます。データベース側が意味の近さの計算を引き受けてくれるので、コードはとてもすっきりします。

TypeScript
const { data: matches } = await supabaseAdmin.rpc('match_doc_chunks', {
  query_embedding: qEmbedding, // 数値化した質問
  match_count: 6,              // 近い記事を6件
});

そして、見つかった記事の本文とURLをClaudeに渡すときに、「この内容だけを根拠に、出典番号をつけて答えてください。書かれていないことは推測しないでください」とお願いしています。この一言があることで、根拠のない“それらしい回答”を防ぎ、記事に基づいた回答を返すことができます。

回答が少しずつ流れて表示されるのも、この部分でClaudeの返事を一気に待つのではなく、生成された順に文章を受け取って画面に送る作りにしているためです。

WordPress側:公開・更新を通知する

記事を公開・更新した際には、その記事をDBに追加する必要があります。

この「記事の更新に自動で追いつく」仕組みを支えているのが、WordPress側に置いた小さなコードです。記事の公開状態が変わったときに、その記事の番号を処理側(Next.js)へ知らせる、という役割を担っています

PHP
add_action('transition_post_status', function ($new, $old, $post) {
    if ($post->post_type !== 'post') return;
    if (wp_is_post_revision($post->ID)) return;

    if ($new === 'publish') {
        rag_notify($post->ID, 'upsert');   // 公開・更新されたら登録
    } elseif ($old === 'publish') {
        rag_notify($post->ID, 'delete');   // 非公開になったら登録を削除
    }
});

今回はWordPress本体のコードには手を入れず、この通知用のコードだけを足す形にしました。設置には専用のプラグイン経由でスニペットとして追加し、万が一書き間違えてもサイト全体が止まらないようにしました。

この「本体には触れず、外付けで安全に拡張する」という考え方は、運用中のブログサイトに機能を追加するときの基本だと思っています。

公開するための安全対策

ブログは社内で使うツールと違い、誰でもアクセスできます。

誰でも使える状態で公開しているので、検索チャットも無防備には出来ません。

今回は、次の3つの対策を入れました。

  • 質問回数の制限
    同じユーザからの短時間での連続質問や、1日あたりの質問回数に上限を設けました。1回質問されるたびにAIの利用料が発生するため、対策をしないと費用が青天井になりかねないからです。
  • 費用の上限
    サイト全体での1日あたりの質問数の上限と、入力できる文字数の制限を設け、費用が膨らみすぎないようにしました。
  • 不正な指示への対策
    「これまでの指示を無視して〜」のように、AIを乗っ取ろうとする入力に従わないよう、あらかじめ守りを固めました。

特に3つ目については、セキュリティの観点から重要です。これについては実際に悪意のある質問を投げてテストしました。「内部の設定を教えて」「別の役割になって」といった典型的な入力に対して、記事のQ&Aの範囲から外れず、内部情報も漏らさないことを確認しています。

そもそもパスワードのような秘密の情報は、生成AIに渡す情報に一切含めていません。ですから、仮に言葉巧みに誘導されても、AIはそれを知らないので答えようがない。「そもそも渡さないことで守る」という考え方は、AIの判断だけに頼るより確実です。

快適なユーザ体験のための工夫

機能が動いただけでは十分ではありません。今回はAI検索にユーザビリティを高めるための工夫を込めました。

  • 出典リンク
    回答の下に、参考にした記事へのリンクを並べました。関連する記事をすぐに開いて読み進めていただけます。
  • 回答を少しずつ表示する
    回答が返ってくるのを何も表示せず待つのではなく、生成された文字から順に実際にタイプしているかのように出力されるようにしました。
  • レイアウトを見やすくした
    見出しや箇条書き、リンクを整えて表示しています。プロフェッショナルに見た目にこだわっています。

まとめ

自社のブログにAI検索を組み込んでみて、書きためた記事が「読める資産」から「質問できる資産」に変わっていく感覚がありました。RAG構築はスキルとしても、読者の側から見た実用性の面でもとても意義のある機能です。

今回の記事が同じようにブログや業務システムのAI活用に取り組まれている方の、参考になればうれしいです。

実装したコードはGitHubでも公開しています。よければ覗いてみてください。
👉 https://github.com/neurosynch-jp/gyomu-system-rag

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

1981年生まれ、名古屋出身。

2008年よりドイツ・ベルリンに在住。
ドイツの国家資格である職業訓練プログラム「アプリケーション開発専門IT技術者」を修了後、医療系自社開発企業にてデスクトップ・Webアプリケーションの開発に4年間従事。
2022年よりドイツの大手SIer「Adesso SE」にて、フルスタックエンジニアとしてリードポジションを務める。

2027年に日本へ帰国し、日本の中小企業へのAI導入支援を本格的に開始予定。

著書「AI時代の海外移住戦略

コメント

コメントする

目次