🧪

SEO スプリットテストと実験

勘や前後比較グラフではなく、対照実験で「本当にオーガニックトラフィックを動かしたもの」を証明する。

📖 13 分で読めます 🕑 更新日 2026-06-22

ブログのタイトルタグを変更した。2 週間後、クリックが 8% 増えている。あなたは祝福の Slack メッセージを投稿する。すると同僚が、こんな居心地の悪い質問を投げかける。「それがタイトルタグのおかげだと、どうやって分かるの?

分からないのだ。その 2 週間のあいだに、Google は告知なしのランキング調整を行い、競合のドメインが失効し、あなたのカテゴリには通常どおりの季節的な伸びがあり、あるニュースレターが記事 3 本にリンクした。素朴な前後比較グラフは、それらすべてを 1 つの数字に静かにまとめてしまい、手柄をあなたの変更に与える。これは SEO チームが自分自身を欺く、最も一般的な手口だ。

その解決策は、新薬の臨床試験やコンバージョン最適化で使われるものと同じ。すなわち**対照群(コントロールグループ)**だ。テスト対象ページと同じアルゴリズム更新、同じ季節性、同じ外部ノイズを経験するページ群を用意でき、テスト対象ページではただ 1 つの変数だけを変えるのであれば、両グループ間の差はあなたの変更に帰属できる。これが SEO スプリットテストであり、本物のテストを数回走らせると、前後比較グラフを二度と信用しなくなる。

💡 ヒント:考え方のモデルはこうだ。あなたが測っているのは「トラフィックが増えたかどうか」ではない。「放っておいても増えていた分より多く増えたかどうか」を測っているのだ。その「放っておいても」の部分を、対照群があなたのために推定してくれる。

SEO の A/B テストとは

まず、プロダクトアナリティクス出身の開発者がつまずく混同を整理しておこう。古典的な A/B テストはユーザー単位だ。各訪問者がランダムにバリアントへ割り当てられ、ユーザー間でコンバージョンを比較する。SEO ランキングではこれはできない。なぜなら、あなたが気にする「ユーザー」は Googlebot であり、それはただ 1 体しか存在しないからだ。同じ URL のバリアント A とバリアント B を Googlebot に出し分けることはできない。それはクローキングであり、ペナルティを受ける。

代わりに、SEO スプリットテストはページ単位だ。ランダム化の単位は訪問者ではなく URL である。似たページの母集団を取り、対照群とバリアント群に分割し、バリアント群に変更を適用し、両グループ間でオーガニックパフォーマンスが時間とともにどう推移するかを比較する。

ユーザー単位 A/B(CRO)ページ単位スプリットテスト(SEO)
ランダム化する単位訪問者 / セッションURL / ページ
何が変わるか各ユーザーに表示される UIグループ全体で 1 つのオンページ要素
誰が「見る」か実在の人間クローラー + SERP 経由の検索者
指標コンバージョン率クリック、表示回数、CTR、掲載順位
誤った場合のリスク悪い UXクローキングペナルティ(ユーザーで出し分けた場合)

これを成立させる要件は比較可能性だ。対照ページとバリアントページは、あなたの変更以外のすべてにおいて統計的な双子のように振る舞わなければならない。双子が似ているほど、検出できる効果は小さくて済む。

テストの設計

良い設計のほとんどは、ページに 1 ピクセルも触れる前に決まる。ここを急ぐと、どんなに巧妙な分析でも救えない。

1. ページ母集団を選ぶ。 テンプレートを共有し、同じインテントを満たすページ群が必要だ。商品詳細ページ、レシピページ、都市別ランディングページ、1 つのカテゴリのブログ記事など。それらはすでに安定したオーガニック表示回数を得ているべきで、トラフィックがほぼゼロのページからはシグナルが得られない。数百ページが快適な下限、1 万ページなら贅沢だ。手作業で選んだ 12 ページはテストではなく、ただの逸話だ。

2. グループにランダム化する。 母集団をおおよそ 50/50 にランダムに分割する。「改善したいページ」をバリアント群に、「退屈なページ」を対照群に入れてはいけない。そのバイアスは誤解を招く結果を保証する。URL をハッシュ化して、割り当てを決定論的かつ再現可能にする。

import hashlib

def assign_group(url: str, salt: str = "title-test-2026q2") -> str:
    h = hashlib.sha256((salt + url).encode()).hexdigest()
    # use one hex digit; even -> control, odd -> variant
    return "variant" if int(h[-1], 16) % 2 else "control"

salt を使えば、次の実験のためにクリーンに再ランダム化でき、同じページが常に同じバケットに入るのを防げる。

3. 変数はきっかり 1 つだけ変える。 タイトルを書き換え、同時にメタディスクリプションも、さらにFAQ スキーマも追加すると、勝ったとしてもどのレバーが効いたのかは分からない。テストごとに変数は 1 つ。ここでの規律こそが、測定を「雰囲気」から分かつものだ。

4. 期間と有意性を事前に決める。 開始前に決めておく。どれだけ走らせるか、そして何をもって勝ちとするか。検索の効果は遅い。Google が再クロール・再インデックスし、ランキングが落ち着くまで数日かかる。最低でも 4〜6 週間、理想的には週次サイクルを数回またぐように走らせる。決定的に重要なのは、終了日を事前にコミットすることだ。毎日のぞき見して、線が良く見えた瞬間に止めるのは「p ハッキング」だ。偶然だけで、およそ 2 回に 1 回は偽の勝者を見つけてしまう。

5. 外部要因をコントロールする。 既知のコアアップデートの週にテストを開始しない。可能なら季節のピークの揺れは避ける。公開ペース、内部リンクの変更、被リンクキャンペーンを、テスト期間中は両グループで一定に保つ。対照群は共有されたノイズを吸収するが、片方のグループだけを襲うショックは吸収できない。

⚠️ 注意:書面のログを残すこと。開始日、終了日、仮説、正確な変更内容、グループ割り当て、そして気づいた外部イベント。6 週間は、自分が何をしたかを必ず忘れるほど長い。文書化されていないテストは再現不能だ。

何をテストするか

SERP でのページの表示のされ方、あるいはクローラーのページ理解に影響するものは、すべて候補になる。おおむねレバレッジの高い順に並べた。

要素テストする仮説注視すべき主要指標
タイトルタグキーワードの前方配置 / 数字の追加 / 角括弧の修飾語が CTR を押し上げるCTR、次いでクリック
メタディスクリプションより明確な価値提案は同じ順位でより多くのクリックを得るCTR
H1オンページの見出しをクエリインテントに合わせると関連性が高まる表示回数、掲載順位
構造化データFAQPage / Product / HowTo の追加でリッチリザルトを獲得表示回数、CTR
コンテンツ形式TL;DR ブロック、表、ステップリストがエンゲージメントと関連性を高める掲載順位、クリック
内部リンク権威あるページからの文脈リンク追加が順位を押し上げる掲載順位、表示回数
パンくずBreadcrumbList マークアップが SERP の URL 行の表示を変えるCTR

有用な区別がある。タイトルとメタディスクリプションのテストは CTR を動かす。それらは検索者が見てクリックするものを変え、ランキング順位にはほとんど影響しない。H1、内部リンク、コンテンツのテストは掲載順位と表示回数を動かす。それらはページがどうランク付けされ表示されるかを変える。自分がどのレバーを引いているかを知れば、どの指標が反応すべきかが分かる。そして、間違った指標が動いた結果は、別の何かが起きているという危険信号だ。

🧑‍💻 開発者視点:構造化データのテストは特にクリーンに走らせられる。変更が純粋にマークアップ内で完結し、グループ全体へのテンプレート化が容易で、その効果(リッチリザルトの有無)は表示回数と CTR の曲線にしばしば鋭く現れるからだ。

測定

Google Search Console があなたの計測器だ。検索パフォーマンスレポート、とりわけその API と一括の Search Console BigQuery エクスポートは、ページ別・日別のクリック、表示回数、CTR、平均掲載順位を提供する。その日次・URL 単位の粒度こそ、スプリットテストが必要とするものだ。

中核となる手法は対照群に対する正規化だ。バリアント群の生の数字を単独で見ることは決してない。生の数字は、これまで議論してきたあらゆるものに汚染されているからだ。代わりに、バリアントと対照の比率を時間とともに追う。

For each day d:
  ratio[d] = clicks_variant[d] / clicks_control[d]

変更が公開される前、ratio はおおむね横ばいであるはず。それがあなたのベースラインであり、両グループがそもそも比較可能だったことを証明する。変更後、ratio の持続的なシフトが効果だ。両グループは同じコアアップデート、同じ季節性、同じニュースサイクルに乗っているため、それらの共有された力は比率からおおむね打ち消し合う。残るのがあなたの変数だ。

GSC BigQuery エクスポートからの最小限のスケッチを示す。

SELECT
  data_date,
  SUM(IF(g.is_variant, clicks, 0)) AS variant_clicks,
  SUM(IF(NOT g.is_variant, clicks, 0)) AS control_clicks,
  SAFE_DIVIDE(
    SUM(IF(g.is_variant, clicks, 0)),
    SUM(IF(NOT g.is_variant, clicks, 0))
  ) AS variant_to_control
FROM `searchconsole.searchdata_url_impression` AS s
JOIN `my_dataset.group_assignment` AS g USING (url)
GROUP BY data_date
ORDER BY data_date;

variant_to_control を時間軸にプロットし、ローンチ日に印を付ける。明確に一段上がってそれが持続すれば勝ち。ローンチ線で切れ目なくふらふらと彷徨う曲線は、ヌル(効果なし)の結果だ。

信頼性について: グラフを目で見るのは出発点であって、判決ではない。ノイズだらけの 2 ページでも、目を細めれば段差に見える。日次の比率をサンプルとして扱い、変更前の期間の分布と変更後の期間の分布を比較し、そのシフトが何も変えていなかった頃に見られた日々のジッターに対して大きいかどうかを問う。変更前の比率が日々 ±15% で跳ねていたなら、変更後の 5% の上昇はノイズだ。

これを厳密にやる版が CausalImpact だ。Google のオープンソースライブラリで広まった、ベイズ構造時系列のアプローチである。発想はエレガントだ。対照群を予測変数として与えると、変更前の期間における対照とバリアントの関係を学習する。そして、もし何も変わらなかった場合にバリアントがどう振る舞っていたか反事実/カウンターファクチュアル)をローンチ後について予測し、その予測と現実とのギャップを信用区間つきで報告する。区間がゼロを含まなければ、統計的に擁護できる効果が得られたことになる。

library(CausalImpact)

# response = variant clicks; covariate = control clicks (the predictor)
data <- zoo(cbind(variant_clicks, control_clicks), dates)
pre  <- c(start_date, launch_date - 1)
post <- c(launch_date, end_date)

impact <- CausalImpact(data, pre, post)
summary(impact)        # relative effect + 95% credible interval
plot(impact)           # observed vs. counterfactual

これが「上がったように見える」と「クリック +6.3%、95% 区間 [+2.1%, +10.4%]、p = 0.004」との違いだ。後者は懐疑的なステークホルダーを生き延びるが、前者は生き延びない。

落とし穴

失敗するテストのほとんどは、いくつかの構造的な理由のどれかで失敗する。これらに注意せよ。

  • サンプルが小さすぎる。 ひとつかみのページ、あるいは表示回数がちょろちょろとしか出ないページでは、日々のノイズを乗り越えられない。変更前の比率がやたらと跳ねているなら、巨大な効果以外を検出するだけのボリュームが足りていない。もっと多くのページをプールするか、トラフィックの多い母集団を選ぼう。
  • テスト期間が短すぎる。 検索は再クロール・再インデックスの遅延を経て反応し、その後にランキングが落ち着く。2 週間で止めると、定常状態ではなく過渡的な状態を測っているかもしれない。最低でも 4〜6 週間は与えよう。
  • テスト途中でコアアップデートが来る。 更新がたまたま片方のグループのクエリ構成をより強く直撃した場合、グループを不均等にかき混ぜることがある。確定した更新が期間中に来たら、それを注記し、変更前の平行性が崩れていないか精査し、破棄して再実行する覚悟をしておく。
  • グループが比較可能でない。 静かな殺し屋だ。あなたの「ランダム」な分割が実はページの古さ、トラフィック階層、サブトピックと相関していたなら、比率はあなたの変更とは無関係な理由でドリフトする。変更後の期間を信じる前に、必ず変更前の比率が横ばいであることを検証すること。横ばいでないベースラインはテスト全体を無効にする。
  • グループ間のリーク。 内部リンク、サイト全体のテンプレート変更、サイトマップの入れ替えは、バリアントの変更を対照ページへにじませることがある。変更はバリアント集合に厳密にスコープを限ること。
  • のぞき見と早期停止。 すでに述べたが、繰り返す価値がある。終了日を事前に決め、それを守ること。

🧑‍💻 実装

テンプレートをフォークしたりロックされた CMS に触れたりせずに、どうやって実際にページの半分へバリアントを配信するのか。エッジでだ。このサイトはすでに Cloudflare 上で動いているので、Worker が各 URL を決定論的にグループへ割り当て、バリアントページの <head> をその場で書き換えられる。これはエッジ SEO ガイドで扱った、同じ HTMLRewriter の手法だ。

Worker は URL をハッシュ化し(分析コードと同じ決定論的な割り当て)、バリアントページについてはタイトルとメタディスクリプションを新しいパターンで書き換える。

async function assignGroup(url, salt = "title-test-2026q2") {
  const data = new TextEncoder().encode(salt + url);
  const digest = await crypto.subtle.digest("SHA-256", data);
  const lastByte = new Uint8Array(digest).at(-1);
  return lastByte % 2 ? "variant" : "control";
}

class TitleRewriter {
  element(el) { el.setInnerContent(this.newTitle); }
  constructor(newTitle) { this.newTitle = newTitle; }
}

export default {
  async fetch(request) {
    const res = await fetch(request);
    const url = new URL(request.url);

    // only experiment on the chosen template; pass everything else through
    if (!url.pathname.startsWith("/products/")) return res;
    if ((await assignGroup(url.pathname)) !== "variant") return res;

    const newTitle = await buildVariantTitle(url);  // your variant pattern
    return new HTMLRewriter()
      .on("title", new TitleRewriter(newTitle))
      .on('meta[name="description"]', {
        element(el) { el.setAttribute("content", buildVariantDescription(url)); },
      })
      .transform(res);
  },
};

これを安全にしているのは 2 点だ。第一に、割り当ては訪問者ではなく URL による。あるページへのすべてのリクエスト(人間でも Googlebot でも)は同じバリアントを受け取るので、クローキングにはならない。第二に、割り当ては決定論的で、SQL/R の分析が使うハッシュと一致する。よって配信時にページが属するグループは、まさにそれを測定するグループと同じだ。同じ salt と割り当て関数を BigQuery の group_assignment テーブルへエクスポートすれば、両者は完全に同期したまま保たれる。

⚠️ 注意:テスト開始時に割り当てをどこか永続的な場所にログとして残し、永遠に再計算するのはやめること。万一 salt や URL セットを変えると、再計算がページを静かにかき混ぜ、分析を破壊する。初日にグループのスナップショットを取ろう。

テストが終わったら、ロールバックは簡単だ。Worker が勝ったバリアントをすべてのページに配信するよう一行の変更をデプロイするか、Worker ルールを丸ごと削除する。オリジンへのデプロイも、テンプレートのフォークも不要で、完全に可逆だ。

重要ポイント

  • ✅ 前後比較グラフは決して信用しない。ランダム化した対照群を使い、共有ノイズ(更新、季節性)を打ち消す。
  • ✅ 訪問者ではなく URL で分割する。ユーザーごとにコンテンツを変えるのはクローキングだ。
  • ✅ 数百ページ超の比較可能な母集団で変数を 1 つだけ変え、何かを信じる前に変更前の比率が横ばいであることを検証する。
  • 終了日と有意性の基準を事前に設定する。4〜6 週間走らせ、のぞき見して止めない。
  • 対照群に対して正規化した GSC データで測定し、信頼区間つきの擁護可能な反事実のために CausalImpact を使う。
  • ✅ バリアントは Cloudflare Worker でエッジ配信し、テストをテンプレート不要・決定論的・即時に可逆にする。