ChatGPTにサイトを丸ごと読ませる!? WordPress×RAGで進化するQ&A
概要
-
この記事を読む対象者
生成系AI(ChatGPTなど)の連携に興味があるWordpressを使う人。 -
この記事の内容
WordPressの独自データを活用し、RAGを使った簡易チャット機能を構築する手順。 -
この記事を読んで分かること
CSV+BIN形式で記事要約を埋め込み検索し、WordPress REST API経由でChatGPTに回答させる実装方法。
序説
みなさん、WordPressでのサイト運営は楽しんでいますか?
中にはフルスクラッチで構築する方もいらっしゃいますが、簡単に導入・管理ができるCMS[1]を使う方も多いのではないでしょうか。
本記事では、そんなWordPressを使いながら RAG[2] を用いた検索機能の構築を紹介します。
成果物
以下の画像のように、WordPress上に用意したチャット画面でユーザが質問を入力すると、
1. 生成AI(ChatGPT)による回答が吹き出しに表示される
2. 質問内容に近い関連記事のリンクがリストアップされる
たとえば「蛇に関するお寺を教えて」と入力すると、「東京の蛇窪神社」や「奈良の大神神社」など、蛇にまつわる神社仏閣の情報が返り、その下に該当記事へ飛べるURLが複数表示されるイメージです。
利用者は会話しながら関連リンクへアクセスできるため、通常の検索と比べて “対話×参照” が一体化した、より分かりやすいインタフェースが実現できます。
RAGについて
RAGの流れ
RAG とは Retrieval Augmented Generation の略称です。
生成AIが回答を考える際に、事前に取得したドキュメント情報を「手がかり」として利用する手法を指します。
• 通常のChatGPT
大規模言語モデル(LLM)内部の知識だけで回答を生成
• RAG
1. ユーザの質問を埋め込みベクトル化
2. 自前のデータ(同じくベクトル化済み)とのコサイン類似度で上位数件を抽出
3. その情報をプロンプトに含めてGPT-3.5などに渡し、回答を生成
「どんな記事があるか」をChatGPTが元々知らない場合でも、RAGを利用することで手がかりを渡して回答を補強できます。
なぜ RAG を WordPress でやるのか?
たとえば、神社仏閣や旅行、美味しいご飯のレポートなど、読者に役立つ膨大な記事をWordPressに蓄積していたとします。
通常のChatGPTに「このブログのどこに〇〇の情報が載っているか教えて!」と尋ねても、外部サイトの詳細まではわかりません。
そこで、ChatGPTにWordPress内の記事を直接参照させるのがRAGの基本的な発想です。
WordPressは世界で最も使われているCMSで、プラグインやテーマによる拡張がしやすい特徴があります。
また、独自のREST APIを活用すれば、埋め込み検索で見つけた記事の内容をChatGPTに投げて回答を生成するフローが比較的容易に実装できます。
どんなメリットがあるのか?
• 独自コンテンツ を使ったQ&Aが可能
• WordPress + ChatGPT な体験を少ないコードで実装できる
• “問い合わせフォーム” が高度化し、ChatGPTに有益な回答を返してもらえる
全体のアーキテクチャ
- データ準備
• WordPressの記事をスクレイピング、または直接DBから取得するなどして CSVファイル を作成
• CSVの各行(要約文やタイトルなど)を埋め込みモデルでベクトル化し、BINファイル に保存 - WordPressのプラグイン / テーマ
• REST APIエンドポイントを追加(例: /wp-json/openai/v1/chat)
• コサイン類似度計算で上位3件ほどの記事をピックアップ
• GPT-3.5に「これが参考資料です。回答をお願いします!」とリクエスト - フロントエンド
• ユーザ入力を受け取り、上記のエンドポイントへ fetch() で送信
• レスポンスをHTMLに描画し、関連リンクも一覧表示
このようにして、WordPress内の記事を検索に利用しながらChatGPTで回答を生成する仕組みが成立します。
実装
今回の例では以下のような構成で実装しています。
• wp-config.php: OpenAI APIキーを定義する。
• embeddings.bin, summarized_content.csv: WordPress上で使うデータファイル群。
• embeddings.bin は文章をベクトル化(埋め込み)した結果をまとめたバイナリ。
• summarized_content.csv はURLや記事タイトル・要約などをまとめたCSV。
• openai-api.php: RAGの主要ロジック。REST APIエンドポイントを登録し、ベクトル検索・OpenAI API呼び出しなどを行う。
• JavaScript (js), HTML (html): ユーザ入力を受け付けてサーバに送信し、応答を表示する部分。
全体の流れ
- データ準備
• WordPressの記事をスクレイピング or 直接DBから引っ張るなどして CSVファイル を作る
• CSV の各行(要約文やタイトルなど)を 埋め込みモデル でベクトル化 → BINファイル に保存 - WordPressのプラグイン / テーマ
• REST API エンドポイントを追加 (/wp-json/openai/v1/chat)
• コサイン類似度計算で上位3件くらいの記事をピックアップ
• GPT-3.5 に「これが参考資料ね。回答お願い!」と投げる - フロントエンド
• ユーザ入力を取得 → 上記のエンドポイントへ fetch()
• レスポンスを HTML に描画。記事へのリンクも表示。
1.データ作成 (CSVとBIN)
サイトマップ取得→要約→CSV作成→埋め込みベクトル作成→BIN保存→Q&Aテスト までを一連で行うコードをpythonで作成しました。
実行には以下を満たす環境や前提が必要です。
• pip install openai requests beautifulsoup4 pandas numpy scikit-learn
• OpenAI APIキー(OPENAI_API_KEY)を取得済み
• 実際のサイトの構造によっては、正規表現での不要部分除去などを適宜カスタマイズが必要
サンプルコードは付録にあります。
2. WordPress 側のコード
2.1 wp-config.php に APIキーを定義
WordPressのルートにある wp-config.php 内に、OpenAIのAPIキーを定義します。
/** OpenAI APIキーを定義 */
define( 'OPENAI_API_KEY', 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx' );
2.2 テーマの functions.php から openai-api.php を読み込む
例として、テーマフォルダの functions/functions.php に下記を追加し、openai-api.php を読み込みます。
<?php
// (テーマのfunctions内)
// openai-rag実装を読み込む
require_once( 'functions/openai-api.php' );
2.3 openai-api.php (メインの実装)
以下のコードを openai-api.php として用意します。
- REST APIエンドポイント: /wp-json/openai/v1/chat にPOSTリクエストを送る。
- search_query() で、埋め込みベクトルを使った類似度検索(トップ3件)。
- 検索結果 (context) をプロンプトに混ぜて gpt-3.5-turbo に問い合わせ、回答を生成。
- WordPressのREST APIとして、JSON形式で応答を返す。
サンプルコードは付録にあります。
3. フロント側(JS と HTML)の実装
3.1 JavaScript例
ユーザ入力を拾ってREST APIへ送信し、結果を表示するコードです。
以下のようなスクリプトを、テーマやプラグイン内、もしくは別のJSファイルに入れて使います。
document.addEventListener("DOMContentLoaded", function () {
const button = document.getElementById("sendMessage");
const input = document.getElementById("userInput");
const output = document.getElementById("responseOutput");
button.addEventListener("click", async () => {
const userInput = input.value.trim();
if (!userInput) {
output.textContent = "メッセージを入力してください。";
return;
}
// 「生成中...」表示
output.textContent = "生成中...";
try {
const response = await fetch("/wp-json/openai/v1/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message: userInput,
}),
});
if (!response.ok) {
output.textContent = `エラーが発生しました。ステータスコード: ${response.status}`;
return;
}
const data = await response.json();
// 応答とリンクの表示
let outputText = `<p>${data.reply}</p>`; // 応答メッセージ
outputText += "<p>あなたにおすすめの記事:</p><ul>";
// リンク一覧
const links = data.references.split("\n");
links.forEach(link => {
if (link.trim()) { // 空文字対策
outputText += `<li><a href="${link}" target="_blank">${link}</a></li>`;
}
});
outputText += "</ul>";
output.innerHTML = outputText;
} catch (error) {
output.textContent = "通信エラーが発生しました。";
console.error("エラー詳細:", error);
}
});
});
• fetch("/wp-json/openai/v1/chat", {...}) でWordPressのREST APIへPOSTリクエスト。
• 応答JSON内の reply に回答が入っているので、HTMLとして表示。
<!-- 吹き出し風の表示エリア -->
<div id="responseOutput" class="bubble">
こんにちは!ご質問をどうぞ。
</div>
<!-- 入力エリア -->
<textarea id="userInput" placeholder="メッセージを入力"></textarea>
<button id="sendMessage">送信</button>
結言
これらのコードを組み合わせることで、WordPressを使ったRAGチャット機能が完成します。
記事を大量に持っているブログやウェブサイトに導入すれば、“サイト内検索 + ChatGPT” のハイブリッド体験が手軽に構築できるはずです。
以上、RAGを利用してWordPress上で独自のチャットQ&Aを作る手順でした。
ぜひ自分のサイトに応用し、読み手が気軽に相談できるレコメンド機能を実現してみてください。
付録
サンプルコードのリポジトリ
rag.py
import requests
from bs4 import BeautifulSoup
import time
import pandas as pd
import os
import re
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
# openai公式ライブラリ
import openai
# ---------------------------------------
# 1) 初期設定
# ---------------------------------------
OPENAI_API_KEY= 'sk-xxxx...' # ご自身のAPIキーを設定
openai.api_key = OPENAI_API_KEY
# Embeddingに使用するモデル
# 公開されているモデル例: "text-embedding-ada-002"
EMBEDDING_MODEL = 'text-embedding-3-small' # 必要に応じて "text-embedding-ada-002" に変更
# Chatに使用するモデル
GPT_MODEL = "gpt-3.5-turbo"
# サイトマップのURL
SITEMAP_URL = 'https://jinjabukkaku.online/post-sitemap.xml'
# 出力ファイル
CSV_FILE = 'summarized_content.csv'
BIN_FILE = 'embeddings.bin'
# ---------------------------------------
# 2) サイトマップのURL一覧取得
# ---------------------------------------
def fetch_sitemap_urls(sitemap_url):
response = requests.get(sitemap_url)
soup = BeautifulSoup(response.content, 'xml')
urls = [loc.text for loc in soup.find_all('loc')]
# WordPressの場合、末尾に"/"が付いていないと不正なURLも混じるかもしれないので適宜絞り込み
return [url for url in urls if url.endswith('/')]
# ---------------------------------------
# 3) 不要部分を削除する関数 (例)
# ---------------------------------------
def remove_footer(text):
# フッターや不要部分を正規表現で削除
footer_pattern = r"(検索:.*?All Rights Reserved\.)"
return re.sub(footer_pattern, '', text, flags=re.DOTALL).strip()
def remove_top_bar(text):
top_bar_pattern = r"(神社仏閣オンライン.*?お問合せ/会社概要)"
return re.sub(top_bar_pattern, '', text, flags=re.DOTALL).strip()
def remove_title(text):
title_pattern = r"(: 神社仏閣オンライン)"
return re.sub(title_pattern, '', text, flags=re.DOTALL).strip()
# ---------------------------------------
# 4) 要約を行う関数
# ---------------------------------------
def summarize_text(text):
"""
ChatCompletionを使ってテキストを要約。
長いテキストに対しては短めの分割処理やトークン数注意が必要。
"""
prompt = f"""
次の文章を要約してください。重要なポイントだけを残して簡潔にまとめてください。
文章:
{text}
"""
response = openai.ChatCompletion.create(
model=GPT_MODEL,
messages=[
{"role": "system", "content": "あなたは文章要約の専門家です。"},
{"role": "user", "content": prompt}
],
temperature=0.5,
)
return response.choices[0].message.content.strip()
# ---------------------------------------
# 5) ページをスクレイピングして要約CSVを作る
# ---------------------------------------
def create_summarized_csv(sitemap_url, csv_file):
urls = fetch_sitemap_urls(sitemap_url)
page_contents = []
for url in urls:
try:
page_response = requests.get(url)
page_soup = BeautifulSoup(page_response.content, 'html.parser')
# タイトル
title_tag = page_soup.find('title')
title = title_tag.text if title_tag else 'No Title'
title = remove_title(title)
# カテゴリ/タグ(rel="category tag" のリンク)
category_tags = page_soup.find_all('a', rel='category tag')
tag_list = [link.text for link in category_tags]
tag_str = ', '.join(tag_list) if tag_list else 'No Tag'
# 本文 (今回は get_text() でまるごと抜く簡易例)
body = page_soup.get_text(separator=" ", strip=True)
body = remove_top_bar(body)
body = remove_footer(body)
# 必要に応じて文字数制限。長文は先頭部だけ要約などの工夫が必要。
# 例: Bodyが極端に長いとトークン数オーバーするため、先頭2000文字だけ要約
excerpt = body[:2000] if len(body) > 2000 else body
if len(excerpt) > 200:
summary = summarize_text(excerpt)
else:
summary = excerpt
# データ保存
page_contents.append({
'url': url,
'title': title,
'tag': tag_str,
'body': summary
})
print(f"[{url}] -> 要約完了")
time.sleep(1) # API連打防止
except Exception as e:
print(f"[{url}] -> エラー: {e}")
# DataFrame化・CSV保存
df = pd.DataFrame(page_contents)
df.to_csv(csv_file, index=False, encoding='utf-8')
print(f"==== CSV保存完了: {csv_file} ====")
# ---------------------------------------
# 6) CSVを読み込み、埋め込みベクトルを生成して BINファイルに保存
# ---------------------------------------
def generate_embeddings_and_save(csv_file, bin_file):
df = pd.read_csv(csv_file)
# OpenAI Embedding API で埋め込みベクトルを取得する関数
def get_embedding(text):
try:
emb_response = openai.Embedding.create(
model=EMBEDDING_MODEL,
input=text
)
return emb_response['data'][0]['embedding']
except Exception as e:
print(f"Embeddingエラー: {e}")
return [0.0] * 1536 # 次元数はモデルにより要変更
# 埋め込み生成
all_embeddings = []
for i, row in df.iterrows():
body_text = row['body']
embedding = get_embedding(body_text)
all_embeddings.append(embedding)
# 適宜ウェイトを置く
time.sleep(0.5)
# numpy配列化
embeddings_array = np.array(all_embeddings, dtype=np.float32)
embeddings_array.tofile(bin_file)
print(f"==== BIN保存完了: {bin_file} ====")
# 次回以降の検索を簡易にするため、DataFrameに埋め込みも保持しておきたい場合は
# JSON等にシリアライズするなどの手段で保存してもよい
# ---------------------------------------
# 7) BIN読み込み→類似度検索→ChatGPT回答 (インタラクティブ例)
# ---------------------------------------
def interactive_qa(csv_file, bin_file):
df = pd.read_csv(csv_file)
# モデルの埋め込み次元 (text-embedding-3-small は不明、ada-002は1536)
EMBED_DIM = 1536
# BIN読み込み
bin_data = np.fromfile(bin_file, dtype=np.float32)
num_records = len(df)
if bin_data.size != num_records * EMBED_DIM:
raise ValueError("BINファイルのサイズがCSV行数×次元数と一致しません。")
# 2次元にリシェイプ
embeddings_matrix = bin_data.reshape((num_records, EMBED_DIM))
# DataFrameにembedding列として取り込む
df["embedding"] = embeddings_matrix.tolist()
# 検索用関数
def search_query(query, top_n=3):
# クエリをベクトル化
try:
query_emb = openai.Embedding.create(
model=EMBEDDING_MODEL,
input=query
)['data'][0]['embedding']
except Exception as e:
print(f"クエリ埋め込みエラー: {e}")
query_emb = [0.0]*EMBED_DIM
# コサイン類似度
query_emb_np = np.array(query_emb, dtype=np.float32).reshape(1, -1)
all_embs_np = np.array(df["embedding"].tolist(), dtype=np.float32)
sims = cosine_similarity(query_emb_np, all_embs_np)[0]
df["similarity"] = sims
top_results = df.sort_values(by="similarity", ascending=False).head(top_n)
return top_results[["url", "title", "tag", "body", "similarity"]]
# ChatGPTへ問い合わせ
def generate_answer(query):
results = search_query(query, top_n=3)
# コンテキスト
context = "\n\n".join(results["body"].tolist())
urls = "\n".join(results["url"].tolist())
prompt = f"""
以下の情報を元に、質問に答えてください:
神社、仏閣の知識:
{context}
質問:
{query}
回答:
"""
try:
response = openai.ChatCompletion.create(
model=GPT_MODEL,
messages=[
{
"role": "system",
"content": (
"あなたは神社、仏閣について知識を持っています。"
"知識に基づいて具体的な神社、仏閣の名前を出し300文字以内で答えて下さい。"
)
},
{"role": "user", "content": prompt}
],
temperature=0.5,
)
answer = response.choices[0].message.content.strip()
except Exception as e:
print(f"ChatCompletionエラー: {e}")
answer = "回答を生成できませんでした。"
return answer, urls
# ループでQ&A
while True:
user_input = input("質問を入力してください (終了するには 'exit'): ")
if user_input.lower() == 'exit':
print("終了します。")
break
ans, refs = generate_answer(user_input)
print("\n=== 回答 ===")
print(ans)
print("\n=== おすすめの記事 ===")
print(refs)
print("\n")
# ---------------------------------------
# メイン実行
# ---------------------------------------
if __name__ == "__main__":
# (A) CSVファイルをまだ作っていない場合 → サイトマップからスクレイピング & 要約
if not os.path.exists(CSV_FILE):
create_summarized_csv(SITEMAP_URL, CSV_FILE)
else:
print(f"既に {CSV_FILE} が存在します。")
# (B) BINファイルがない場合は埋め込み生成→保存
if not os.path.exists(BIN_FILE):
generate_embeddings_and_save(CSV_FILE, BIN_FILE)
else:
print(f"既に {BIN_FILE} が存在します。")
# (C) インタラクティブにQ&Aテスト
interactive_qa(CSV_FILE, BIN_FILE)
openai-api.php
<?php
// REST APIエンドポイント登録
add_action('rest_api_init', function () {
register_rest_route('openai/v1', '/chat', array(
'methods' => 'POST',
'callback' => 'handle_openai_chat',
'permission_callback' => '__return_true', // 認証不要
));
});
// OpenAI APIキーの読み込み (wp-config.php に定義)
define('OPENAI_API_KEY', defined('OPENAI_API_KEY') ? OPENAI_API_KEY : null);
// 埋め込みの次元数 (例: 1536)
define('EMBED_DIMENSION', 1536);
// データファイルパス
define('CSV_FILE', ABSPATH . 'wp-content/plugins/openai-rag/data/summarized_content.csv');
define('BIN_FILE', ABSPATH . 'wp-content/plugins/openai-rag/data/embeddings.bin');
// データ読み込み関数
function load_data() {
$csv_file = CSV_FILE;
$bin_file = BIN_FILE;
// CSVデータ読み込み
$data = [];
if (($handle = fopen($csv_file, 'r')) !== false) {
$headers = fgetcsv($handle);
while (($row = fgetcsv($handle)) !== false) {
$data[] = array_combine($headers, $row);
}
fclose($handle);
}
// バイナリ (.bin) 読み込み
$bin_data = file_get_contents($bin_file);
// float32配列としてunpack → EMBED_DIMENSION(1536)次元ずつに分割
$embeddings_array = unpack('f*', $bin_data);
$embeddings = array_chunk($embeddings_array, EMBED_DIMENSION);
// CSVデータに埋め込みベクトルを追加
foreach ($data as $index => &$row) {
$row['embedding'] = $embeddings[$index];
}
return $data;
}
// コサイン類似度計算
function cosine_similarity($vec1, $vec2) {
$dot_product = 0.0;
foreach ($vec1 as $i => $val1) {
$dot_product += $val1 * $vec2[$i];
}
$magnitude1 = 0.0;
$magnitude2 = 0.0;
foreach ($vec1 as $v1) {
$magnitude1 += $v1 * $v1;
}
foreach ($vec2 as $v2) {
$magnitude2 += $v2 * $v2;
}
$magnitude1 = sqrt($magnitude1);
$magnitude2 = sqrt($magnitude2);
if ($magnitude1 == 0.0 || $magnitude2 == 0.0) {
// ゼロベクトル対策
return 0.0;
}
return $dot_product / ($magnitude1 * $magnitude2);
}
// 類似度検索
function search_query($query, $data, $top_n = 3) {
// OpenAI埋め込みモデルでクエリをベクトル化
$api_key = OPENAI_API_KEY;
$response = wp_remote_post('https://api.openai.com/v1/embeddings', [
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $api_key,
],
'body' => json_encode([
'model' => 'text-embedding-ada-002', // 例: 適宜モデル名を指定
'input' => $query
]),
]);
if (is_wp_error($response)) {
return [];
}
$body = json_decode(wp_remote_retrieve_body($response), true);
$query_embedding = $body['data'][0]['embedding'] ?? null;
if (!$query_embedding || !is_array($query_embedding)) {
// エラー時は空を返す
return [
[
'url' => '',
'title' => '',
'body' => '',
'similarity' => 0.0
]
];
}
// データとの類似度を計算
$similarities = [];
foreach ($data as $row) {
$similarity = cosine_similarity($query_embedding, $row['embedding']);
$similarities[] = [
'url' => $row['url'],
'title' => $row['title'],
'body' => $row['body'],
'similarity'=> $similarity
];
}
// 類似度順ソート → 上位N件取得
usort($similarities, fn($a, $b) => $b['similarity'] <=> $a['similarity']);
return array_slice($similarities, 0, $top_n);
}
// OpenAI APIリクエスト処理(RAG統合)
function handle_openai_chat(WP_REST_Request $request) {
$message = $request->get_param('message');
// メッセージが未入力の場合
if (!$message) {
return new WP_REST_Response(['reply' => 'メッセージがありません。'], 400);
}
// APIキーが存在しない場合
if (!OPENAI_API_KEY) {
return new WP_REST_Response(['reply' => 'APIキーが設定されていません。'], 500);
}
// CSVとBINのデータを読み込み
$data = load_data();
if (!$data) {
return new WP_REST_Response(['reply' => 'データがロードできませんでした'], 500);
}
// 類似度検索
$results = search_query($message, $data, 3);
// コンテキスト文章を作成
$context = implode("\n\n", array_column($results, 'body'));
$references = implode("\n", array_column($results, 'url'));
$similarities = implode("\n", array_column($results, 'similarity'));
// OpenAI Chat APIへコンテキストを付与して問い合わせ
$endpoint = 'https://api.openai.com/v1/chat/completions';
$api_key = OPENAI_API_KEY;
// 実際にシステムに与えるプロンプト例
$prompt = "以下の情報を元に、質問に答えてください: \n\n知識:\n" . $context . "\n\n質問:\n" . $message . "\n\n回答:";
$post_data = [
'model' => 'gpt-3.5-turbo',
'messages' => [
[
'role' => 'system',
'content' => 'あなたは神社、仏閣について知識を持っています。知識に基づいて具体的な神社、仏閣の名前を出し300文字以内で答えて下さい。'
],
[
'role' => 'user',
'content' => $prompt
]
],
'max_tokens' => 3000,
];
$response = wp_remote_post($endpoint, [
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $api_key,
],
'body' => json_encode($post_data, JSON_UNESCAPED_UNICODE),
'timeout' => 30,
]);
if (is_wp_error($response)) {
return new WP_REST_Response(['reply' => 'サーバーエラー: リクエスト失敗'], 500);
}
$body = json_decode(wp_remote_retrieve_body($response), true);
$reply = $body['choices'][0]['message']['content'] ?? '応答がありません。';
// APIレスポンスを返す
return new WP_REST_Response([
'reply' => $reply,
'context' => $context,
'references' => $references,
'similarity' => $similarities,
'dimension' => EMBED_DIMENSION,
], 200);
}
Discussion