2025年 5月 8日に ipullrank で公開された Francine Monahanという方の「An Introduction to Relevance Engineering: The Future of Search」を読みました。

アイオイクスさんの翻訳記事も読みました。
https://seojapan.com/column/column-14753

上記内容を自分なりに整理し、AIとも競技を重ねた結果、とある試みをトライしました。

それは、

関連性スコアの可視化(高度施策):記事内容と検索クエリの「ベクトル距離」を測る

というアクションです。

「ベクトル」って表現最近SEOに絡ませる人よくいるなぁ〜。
格好良いな〜って感じで…。

具体的なアウトプットイメージ

出力

こんな感じ。
類似度スコア(similarity)が1に近いほど高関連性、0以下ならほぼ無関係という考え方みたいです。

タイトルとクエリの関連性だけだと判断材料が弱すぎるので、
・対象URLの見出し構成を(h1〜h3)確認
・その上でAIによる改善案も出す

といった仕組みにしました。

使ってみた結果

有料のAPI叩いでまで使うもんじゃないかな」なんて感じちゃいました。
特に、私は自分で作業を行うケースが多いため、大量の依頼は投げられませんし、プロンプト組んで普通にGPTに投げる方が調整できて早いしお金も必要以上にかからないなぁという。

あとは抜粋されるクエリも「他のクエリによっては…」みたいなことも気になってしまい、アウトプットの調整が必要だなぁと。

一方で作り込みが甘いだけで、AIによる分析パートの展開をもう少し工夫できれば面白そうにも感じています。

とかもカバーできると良さそうですね。

この仕組みが合う方

この辺りのユーザーさんには刺さるのではないかと感じております。

この仕組みが合わない方

この辺は難しいかなと。

実装の手順

詳細はPythonファイルにて解説します。

必要なもの

Pythonファイル


import openai
import pandas as pd
import requests
from bs4 import BeautifulSoup
from datetime import date, timedelta
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from numpy import dot
from numpy.linalg import norm

# --- 1. OAuth 認証 ---
SCOPES = ['https://www.googleapis.com/auth/webmasters.readonly']
flow = InstalledAppFlow.from_client_secrets_file('client_secrets.json', SCOPES)
creds = flow.run_local_server(port=0)
# このファイルと同じフォルダに、GCPでJSON形式で認証キーをダウンロードし、「client_secrets.json」という名前で格納

# --- 2. Search Console API 初期化 ---
service = build('searchconsole', 'v1', credentials=creds)
site_list = service.sites().list().execute()

print("🔗 利用可能なプロパティ:")
urls = []
for site in site_list.get('siteEntry', []):
    if site['permissionLevel'] != 'siteUnverifiedUser':
        print("-", site['siteUrl'])
        urls.append(site['siteUrl'])
# GCPであらかじめ、Search Console APIをONにしておいてください。

# --- 3. 使用するサイトURLを選択 ---
target_site = input("使用するサイトURLを入力してください: ").strip()

# --- 4. クエリ+ページの取得 ---
end_date = date.today()
start_date = end_date - timedelta(days=30)

request = {
    'startDate': start_date.isoformat(),
    'endDate': end_date.isoformat(),
    'dimensions': ['query', 'page'],
    'rowLimit': 50 #50クエリ分。1000まで任意の数字を選べる。多いほど時間がかかるので注意
}

response = service.searchanalytics().query(siteUrl=target_site, body=request).execute()
rows = response.get('rows', [])
data = []
for row in rows:
    query, page = row['keys']
    data.append({'query': query, 'page': page})

df = pd.DataFrame(data).drop_duplicates()

# --- 5. 見出し構成の取得 (h1〜h3) ---
def get_headings(url):
    try:
        res = requests.get(url, timeout=5)
        soup = BeautifulSoup(res.text, 'html.parser')
        headings = []
        for tag in soup.find_all(['h1', 'h2', 'h3']):
            txt = tag.get_text(strip=True)
            if txt:
                headings.append(f"{tag.name}: {txt}")
        return '\n'.join(headings)
    except:
        return ""

df['headings'] = df['page'].apply(get_headings)
df = df[df['headings'] != ""]
#好みに応じて、h4まで入れても良いかも

# --- 6. OpenAI APIキーの入力 ---
from openai import OpenAI

api_key = ""
while not api_key.startswith("sk-"):
    api_key = input("🛡️ OpenAI APIキー(sk-〜)を入力して Enter を押してください: ").strip()
    if not api_key.startswith("sk-"):
        print("⚠️ 正しい形式のAPIキー(sk-で始まる)を入力してください。")

client = OpenAI(api_key=api_key)


# --- 7. Embedding取得とCos類似度計算 ---
def get_embedding(text, model="text-embedding-ada-002"):
    response = client.embeddings.create(input=[text], model=model)
    return response.data[0].embedding

def cosine_similarity(v1, v2):
    return dot(v1, v2) / (norm(v1) * norm(v2))

results = []
for _, row in df.iterrows():
    try:
        headings_vec = get_embedding(row['headings'])
        query_vec = get_embedding(row['query'])
        sim = cosine_similarity(headings_vec, query_vec)
        results.append({
            "query": row['query'],
            "url": row['page'],
            "headings": row['headings'],
            "similarity": round(sim, 4)
        })
    except Exception as e:
        print("❌ スキップ:", row['query'], "→", str(e))
        continue

# --- 8. 類似度の低い記事をAIで分析 ---
result_df = pd.DataFrame(results).sort_values(by="similarity").head(50)
#▲(50)ここはリクエスト数に合わせて

def get_rewrite_suggestion(query, headings):
    try:
        system_prompt = "あなたはSEOの専門家です。見出し構成と検索クエリから、ユーザーの意図に合う改善案を具体的に提案してください。"
        user_prompt = f"【検索クエリ】\n{query}\n\n【見出し構成】\n{headings}"
        res = client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt}
            ]
        )
        return res.choices[0].message.content.strip()
    except Exception as e:
        return f"エラー: {e}"

result_df["rewrite_suggestion"] = result_df.apply(
    lambda row: get_rewrite_suggestion(row["query"], row["headings"]), axis=1
)
#結局ここのプロンプト次第でより良いものが作れるはず。
# --- 9. 保存 ---
result_df.to_csv("gsc_similarity_headings_with_suggestions.csv", index=False, encoding="utf-8-sig")
print("✅ gsc_similarity_headings_with_suggestions.csv に出力しました。")

回すと、サーチコンソール連携アカウントの選択が求められます。
選んで、その中で確認したいURLを打ち込みましょう。

その上で、OpenAIのAPIキーを打ち込めばcsvで処理してくれます。

まとめ

この記事で振り返っているうちに、作り込めばもう少し面白いものになりそうだな〜と思いました。
そしてひどく雑な記事だなとも…w

あくまでも備忘録程度なので、こんなクオリティで申し訳ないです。
このテーマについては近いうちリベンジします。

AIの修正内容に「ユニークに」とか「このテーマを調べるユーザーが起因しそうな内容をたくさん挙げて、端的にコンテンツに落とし込んで」とかも良さそうですね。

Supervisor Image

葛西 洋介

都内Webマーケティング企業に勤務。対応サイトは過去大小含め100サイト以上。
企業サイトの案件が多く、SEOというよりは事業理解や現実的なマーケティング側面を大切にしています。