Article

SEOとリスティング広告、二刀流で検索上位を独占する方法

高田晃太郎
SEOとリスティング広告、二刀流で検索上位を独占する方法

SISTRIXのCTR調査では、Googleの1位は平均28.5%のクリック率(CTR:検索結果の表示に対するクリック割合)、2位は15.7%、3位は11%台¹と報告されています。さらにSparktoroの分析では、ゼロクリック検索(クリックせずに検索が完結)が全体の半数近くを占める傾向が続いています²。既存の研究では、広告と自然検索の同時露出がクリックの純増(インクリメンタル)を生む可能性がある一方³⁴、特にブランド語ではカニバリゼーション(広告が自然検索のクリックを食い合う現象)も起こりうると示唆されています⁵。ここで判断を誤ると、広告費は増えるのにビジネス成果(ROI:投資対効果)は伸びません。逆に、エンジニアリング視点で計測と自動化を組み込み、SEOとリスティング広告を一体運用すれば、検索結果ページ(SERP)の可視性とROIを同時に引き上げられます。この記事では、CTOやエンジニアリーダーが主導できる形に落とし込み、全体像の整理からデータ統合、検証デザイン、運用オーケストレーションまでを、実装例とともにわかりやすく示します。

検索の二刀流アーキテクチャ:SERPオーケストレーションの全体設計

まず前提として、SEOは中長期の構造価値を積み上げ、リスティング広告は短期の機会獲得や在庫調整に長けています。対立概念ではなく、SERPという一枚のキャンバスを塗り分ける役割分担として捉えると設計が明瞭になります。ブランド、カテゴリ、競合指名、情報探索、商談直結といった検索意図のレイヤーごとに、自然検索で取るべきポジションと広告で上塗りすべきポジションを定義します。ここで重要なのは、意思決定の単位をキャンペーンや記事ではなくクエリクラスタ(意味や意図が近い検索語の集合)に置くことです。トピックモデルやn-gram(単語列の頻度分析)でクラスタ化し、SERPの構造(広告の拡張表示、ショッピング、地図、動画、生成AIの回答枠など)まで含めて可視性スコアを算定します。結果として、同じ予算でも「見られ方」の質が変わり、商談・売上の前段であるリードの質が安定します。

測定の中核は、広告と自然検索を同じ「検索語」単位で突き合わせることにあります。媒体別のレポートやラストクリックのコンバージョン数だけを眺めても、相乗効果もカニバリも見えません。GA4の生ログ、Search Console、Google Adsのサーチタームを同一日の同一クエリに正規化して結合し、ブレンデッド(広告+自然の合算)なクリック、インプレッション、CTR、CPC(クリック単価)、CVR(コンバージョン率)、そして最終的なROAS(広告費用対効果)やCPA(獲得単価)を算出します。この一枚の事実テーブルが意思決定の土台になります。

BigQueryで作るブレンデッド検索テーブル

以下は、Search ConsoleとGoogle Adsのサーチタームを日付と正規化クエリで結合し、ブレンデッド指標を作るSQLの一例です。実運用では命名やスキーマを環境に合わせて調整します。列名やNULL処理はデータの仕様に応じて適宜変更してください。

-- Search Console: gsc.search_performance(date, query, clicks, impressions, ctr, position)
-- Google Ads: ads.search_terms(date, query, clicks, impressions, cost, conversions, campaign, is_brand)
WITH gsc AS (
  SELECT DATE, LOWER(TRIM(query)) AS nq, clicks AS gsc_clicks, impressions AS gsc_impr,
         SAFE_DIVIDE(clicks, impressions) AS gsc_ctr, position AS gsc_pos
  FROM `project.dataset_gsc.search_performance`
),
ads AS (
  SELECT DATE, LOWER(TRIM(query)) AS nq, clicks AS ads_clicks, impressions AS ads_impr,
         cost AS ads_cost, conversions AS ads_conv, campaign, is_brand
  FROM `project.dataset_ads.search_terms`
)
SELECT 
  COALESCE(a.date, g.date) AS date,
  COALESCE(a.nq, g.nq) AS nq,
  IFNULL(g.gsc_clicks,0) AS gsc_clicks,
  IFNULL(g.gsc_impr,0)   AS gsc_impr,
  g.gsc_ctr,
  g.gsc_pos,
  IFNULL(a.ads_clicks,0) AS ads_clicks,
  IFNULL(a.ads_impr,0)   AS ads_impr,
  IFNULL(a.ads_cost,0)   AS ads_cost,
  IFNULL(a.ads_conv,0)   AS ads_conv,
  SAFE_DIVIDE(IFNULL(a.ads_clicks,0)+IFNULL(g.gsc_clicks,0),
              NULLIF(IFNULL(a.ads_impr,0)+IFNULL(g.gsc_impr,0),0)) AS blended_ctr,
  SAFE_DIVIDE(IFNULL(a.ads_cost,0), NULLIF(IFNULL(a.ads_conv,0),0)) AS ads_cpa,
  SAFE_DIVIDE(IFNULL(a.ads_cost,0), NULLIF(IFNULL(a.ads_clicks,0),0)) AS ads_cpc,
  ANY_VALUE(a.campaign) AS sample_campaign,
  BOOL_OR(a.is_brand) AS is_brand
FROM ads a
FULL OUTER JOIN gsc g
  ON a.date = g.date AND a.nq = g.nq
;

このテーブルを中核に、クエリクラスタ単位でのブレンデッドKPIと、広告を止めた場合・続けた場合のインクリメンタリティ推定へとつなげていきます。

ブランド語のカニバリとインクリメンタリティ検証

広告の純増効果はクエリタイプで大きく変わります。一般に非ブランド語や競合指名では広告の純増が出やすく、ブランド語では自然検索の1位が強固なときにカニバリが起こりやすいという傾向が報告されています⁴⁵。これを思い込みで判断せず、意図別のクラスタに分けて地理・時間・デバイスのスプリットで実験します。差の差分(Before/After×On/Offの交互作用で効果を推定)や合成コントロールを用いれば、広告On/Offの効果を自然変動から切り分けられます。さらに、既存研究では検索広告のクリックの多くが純増である一方³、オーガニック順位が高い場合は純増率が低下する傾向が示されています⁴。安定した結論を得るには、期間を十分取り、季節性やイベントの影響を固定効果で抑えるのが無難です。

# 差の差分で広告On/Offのインクリメンタリティを推定
import pandas as pd
import statsmodels.formula.api as smf

# df: date, region, on_off(1/0), period(1=post), clicks, conversions
# 例: treatment地域=広告On, control地域=広告Off
model = smf.ols('conversions ~ on_off*period + C(region) + C(date)', data=df).fit(cov_type='HC3')
print(model.summary())
# 交互作用 on_off:period の係数が広告の純増効果(ATET)の推定値

オフライン売上やリード品質を加味するなら、GA4のイベントIDをキーにCRMの商談ステージを結合し、最終粗利ベースのROASで評価します。編集プロセスや営業組織との責任分界点を明確にし、マルチタッチアトリビューションの限界は認めた上で、地理・時間の実験設計で因果推定を最優先に置くのが安全です。

実装と運用:SEOと広告をリアルタイム連携させる

技術の肝は、データの同期と入札・予算の自動調整です。自然検索の平均掲載順位が高いクエリでは広告の強度を弱め、順位が落ちる、あるいはSERPにショッピング・動画・生成AIの表示が強いと判断したら広告で上塗りする、という動的な配分が望ましいです。これを手作業で回すのは現実的ではないため、Search ConsoleのランキングとGoogle Adsの入札や予算を連携させるスクリプトを仕込みます。

Ads Scriptで予算を有機順位に連動させる

Search Consoleの順位集計をスプレッドシートに日次で書き出し、Google Adsのキャンペーン予算に反映する例です。自動入札(tCPAやtROAS)を採用している場合は、過度な目標値変更を避け、まずは予算比率の微調整から始めるのが安全です。

// Google Ads Scripts(毎日実行)
const SHEET_URL = 'https://docs.google.com/spreadsheets/d/...';
const THRESH_HIGH = 3.0;  // 自然検索の平均掲載順位が高い
const THRESH_LOW = 5.0;   // 自然検索の平均掲載順位が低い

function main() {
  const sheet = SpreadsheetApp.openByUrl(SHEET_URL).getSheetByName('ranking');
  const values = sheet.getDataRange().getValues(); // [campaign, avg_pos, current_budget]
  const header = values.shift();

  values.forEach(row => {
    const [campaignName, avgPos, currentBudget] = row;
    const it = AdsApp.campaigns().withCondition(`Name = "${campaignName}"`).get();
    if (!it.hasNext()) return;
    const campaign = it.next();

    let newBudget = currentBudget;
    if (avgPos <= THRESH_HIGH) newBudget = currentBudget * 0.8; // 有機が強いなら広告を抑制
    else if (avgPos >= THRESH_LOW) newBudget = currentBudget * 1.15; // 有機が弱いなら強化

    const budget = campaign.getBudget();
    budget.setAmount(Math.max(newBudget, 1000)); // 最低額のガードレール
  });
}

検索語や広告グループ単位での調整に踏み込む前に、まずはキャンペーン粒度での予算モジュレーションを安定稼働させ、学習のリセットを避ける配慮をします。PMax(Performance Max)を併用している場合は、ブランド語の除外と検索キャンペーンの優先順位を明確にし、意図せぬブランド費用の膨張を防ぎます。

APIでのフル同期:GSC×Ads×BigQueryのETL

スクリプトによる連携を中期的にプラットフォーム化するには、クラウドのETL(抽出・変換・格納)を組みます。以下はPythonでSearch ConsoleとGoogle Adsを取り込み、BigQueryに書き込む例です。スケジューラはCloud SchedulerとCloud Run、あるいはAirflowを用います。APIの呼び出し制限や認証管理、欠損値の処理は運用環境に合わせて整備してください。

import os
import pandas as pd
from googleapiclient.discovery import build
from google.ads.googleads.client import GoogleAdsClient
from google.cloud import bigquery

SC_SITE = 'https://www.example.com/'
PROJECT = 'my-project'
DATASET = 'search_blended'
TABLE = 'query_daily'

# Search Console
sc_service = build('searchconsole', 'v1', cache_discovery=False)

# Google Ads
ads_client = GoogleAdsClient.load_from_storage(path='google-ads.yaml')
customer_id = '1234567890'

# BigQuery
bq = bigquery.Client(project=PROJECT)

def fetch_gsc(start_date, end_date):
  request = {
    'startDate': start_date,
    'endDate': end_date,
    'dimensions': ['DATE','QUERY'],
    'rowLimit': 25000
  }
  response = sc_service.searchanalytics().query(siteUrl=SC_SITE, body=request).execute()
  rows = response.get('rows', [])
  data = []
  for r in rows:
    date = r['keys'][0]
    query = r['keys'][1].lower().strip()
    clicks = r.get('clicks',0)
    impressions = r.get('impressions',0)
    position = r.get('position', None)
    data.append([date, query, clicks, impressions, position])
  return pd.DataFrame(data, columns=['date','query','gsc_clicks','gsc_impr','gsc_pos'])

def fetch_ads(start_date, end_date):
  ga_service = ads_client.get_service('GoogleAdsService')
  query = f'''
    SELECT segments.date, search_term_view.search_term, 
           metrics.clicks, metrics.impressions, metrics.cost_micros, metrics.conversions,
           campaign.name
    FROM search_term_view
    WHERE segments.date BETWEEN '{start_date}' AND '{end_date}'
  '''
  stream = ga_service.search_stream(customer_id=customer_id, query=query)
  data = []
  for batch in stream:
    for row in batch.results:
      date = str(row.segments.date)
      q = row.search_term_view.search_term.lower().strip()
      clicks = row.metrics.clicks
      impr = row.metrics.impressions
      cost = row.metrics.cost_micros/1e6
      conv = row.metrics.conversions
      camp = row.campaign.name
      data.append([date, q, clicks, impr, cost, conv, camp])
  return pd.DataFrame(data, columns=['date','query','ads_clicks','ads_impr','ads_cost','ads_conv','campaign'])

def to_bq(df):
  table_id = f"{PROJECT}.{DATASET}.{TABLE}"
  job = bq.load_table_from_dataframe(df, table_id, job_config=bigquery.LoadJobConfig(write_disposition='WRITE_APPEND'))
  job.result()

if __name__ == '__main__':
  start, end = '2025-07-01', '2025-07-31'
  g = fetch_gsc(start, end)
  a = fetch_ads(start, end)
  df = pd.merge(g, a, on=['date','query'], how='outer').fillna(0)
  df['blended_ctr'] = (df.gsc_clicks + df.ads_clicks) / (df.gsc_impr + df.ads_impr).replace(0, pd.NA)
  to_bq(df)

これで日次の同期ができれば、クエリクラスタ単位の可視性管理と、予算・入札の自動オーケストレーションが実現できます。API連携の設計思想やサンプル群も参考にしてください。

配分ロジック:シャドーCPAで“有機の価値”を予算へ折り込む

二刀流運用の肝は、広告の意思決定に自然検索の貢献を織り込むことです。広告単体のCPAやROASではなく、もし広告を止めた場合の自然検索の補完や落ち込みを考慮した、影のコストや影の効果を算定します。ここでは簡易だが運用現場で効く枠組みとしてシャドーCPAの考え方を紹介します。あるクエリクラスタcにおける広告のCPAを、自然検索が吸収するコンバージョン増減ΔCVの推定値で調整し、広告コストを実質効果で割り直します。自然検索の順位とSERP構成でΔCVを推定し、インクリメンタリティ実験から得た係数で校正します⁴。現場では、クラスタごとの「純増率」を一度推定しておくと、日次の配分が安定します。

# シャドーCPAで予算配分を最適化
import pandas as pd

# df: cluster, ads_cost, ads_cv, gsc_pos, incr_lift  (incr_lift: 広告の純増率推定 0~1)
# 例: incr_lift=0.6 は 広告CVのうち60%が純増、40%は自然へ置換される想定

def allocate_budget(df, total_budget):
  df = df.copy()
  df['shadow_cv'] = df['ads_cv'] * df['incr_lift']
  df['shadow_cpa'] = df['ads_cost'] / df['shadow_cv'].clip(lower=1e-6)
  # 逆数比例で配分(シャドーCPAが低い=効率が良いクラスタに厚く配分)
  weight = (1 / df['shadow_cpa']).clip(upper=10)
  df['budget'] = total_budget * weight / weight.sum()
  return df[['cluster','budget','shadow_cpa']]

clusters = pd.DataFrame({
  'cluster': ['brand','category','competitor','howto'],
  'ads_cost': [10000, 50000, 20000, 15000],
  'ads_cv':   [300, 200, 50, 20],
  'gsc_pos':  [1.2, 4.8, 6.3, 3.1],
  'incr_lift':[0.3, 0.7, 0.8, 0.6]
})
print(allocate_budget(clusters, total_budget=200000))

実務上は、この配分を日次で更新しつつ、週次・月次でのガードレールを設けて学習の安定性を担保します。自然検索の順位は短期に乱高下するため、P95でクリップするなどの平滑化、重要クエリのホワイトリスト化、そしてブランド語は別枠で運用するなどの基本も忘れないほうが良いです。コンテンツ戦略と連携し、上位表示の見込みが高いトピックには広告で先に市場検証をかけ、CVRの高いクエリが見つかったら、技術SEOとコンテンツ投資で自然側を追いかけるという循環が回り始めます。

技術SEOの実装面での相乗効果

技術SEOは広告の品質にも跳ね返ります。ページ速度やCLS(Cumulative Layout Shift:レイアウトの安定性)の改善は広告ランディングの品質スコア向上に繋がり、同じ入札でも表示機会とCPCが改善されることがあります。サーバーログに基づくクロールバジェット最適化は、検索ボリュームの大きい商材群の更新頻度を高め、広告で集めた行動データとの相関分析でCVRを改善しやすいUI変更を特定できます。構造化データの整備は、リッチリザルトの獲得確率を上げつつ、広告拡張と訴求軸のABテスト設計にもフィードバックします。

レポーティングとガバナンス:経営に刺さるKPIを一本化する

経営の意思決定はシンプルなKPIを好みます。媒体別の綺麗なレポートを並べるのではなく、ブレンデッドCPC、ブレンデッドCTR、クエリクラスタ別のSERP可視性スコア、最終粗利ROAS、そしてインクリメンタルCPAに集約して報告します。現場では、広告とSEOの担当が別組織になっていることが多く、評価制度が分断を助長します。そこで、クエリクラスタ別の共同KPIを設定し、両者の目標を同じグラフで追わせるのが実務的です。ダッシュボードはBigQueryをソースにLooker Studioや自社BIで構成し、地理やデバイス別のフィルタで実験の結果を即座に切り出せるようにします。

リスク管理としては、競合が自社ブランド語に入札を強めた際の自動検知、生成AIサマリーによるオーガニック流入減の早期兆候検知、そして在庫やサプライの制約が出た際の出稿抑制など、業務シグナルとの連動も有効です。エンジニアリングが主導することで、これらの検知ロジックは比較的少ない労力で実装でき、マーケ側は判断とクリエイティブに集中できます。

差分検知とアラートの簡易実装

最後に、クエリクラスタの可視性低下を検知して、関係者に即時アラートを出すPythonの例を示します。統計的な異常検知はProphetやCUSUMに置き換えられますが、まずは分位点に基づくしきい値で十分に機能します。

import pandas as pd
from google.cloud import bigquery
import smtplib

bq = bigquery.Client()
SQL = '''
SELECT date, cluster,
       SUM(gsc_impr + ads_impr) AS impr,
       SUM(gsc_clicks + ads_clicks) AS clicks
FROM `project.search_blended.query_daily_clustered`
WHERE date >= DATE_SUB(CURRENT_DATE(), INTERVAL 35 DAY)
GROUP BY date, cluster
'''

df = bq.query(SQL).to_dataframe()
df['ctr'] = df['clicks'] / df['impr'].replace(0, pd.NA)

alerts = []
for cluster, g in df.groupby('cluster'):
  p10 = g['ctr'].quantile(0.10)
  today = g.sort_values('date').iloc[-1]
  if today['ctr'] < p10:
    alerts.append(f"CTR drop in {cluster}: {today['ctr']:.3f} < p10 {p10:.3f}")

if alerts:
  msg = "\n".join(alerts)
  # SMTP設定を環境変数から読み取り、Slack Webhookに切り替えても良い
  print(msg)

このような検知と自動調整のサイクルを回すうちに、SEOチームは技術負債の解消と情報設計に集中し、広告チームはクリエイティブと入札戦略の探索に注力できます。二刀流は資源の奪い合いではなく、資源配分の自動化と透明化によって、むしろ社内政治の摩擦を下げる道具になります。

まとめ:問いをデータに落とし、配分を自動化する

検索上位を独占するとは、派手な一撃を決めることではありません。検索意図をクラスタ化し、自然と広告の役割を定義し、結果を同じテーブルで観測し続けるという地味で再現性の高い営みの積み重ねです。まずはSearch ConsoleとGoogle Adsを日次で結合したブレンデッドテーブルを用意し、ブランド語のカニバリを差の差分で検証し、シャドーCPAで予算配分を組み替えてみてください。小さな改善の繰り返しが、やがて大きな可視性の差となって現れます。

**技術と運用が融合するとき、SERPはプロダクトの一部になります。**あなたの組織では、検索の意思決定はデータで語られていますか。それとも個別レポートに分断されていますか。次の一手として、データパイプラインにクエリクラスタの軸を追加し、広告とSEOの共同KPIを一本化するダッシュボードを作ってみましょう。

参考文献

  1. SISTRIX. Why almost everything you knew about Google CTR is no longer valid. https://www.sistrix.com/blog/why-almost-everything-you-knew-about-google-ctr-is-no-longer-valid/
  2. SparkToro. Less than half of Google searches now result in a click. https://sparktoro.com/blog/less-than-half-of-google-searches-now-result-in-a-click/
  3. Google Research. Incremental Clicks Impact of Search Advertising. https://research.google/pubs/incremental-clicks-impact-of-search-advertising/
  4. Google Research Blog. Impact of organic ranking on ad click incrementality. https://research.google/blog/impact-of-organic-ranking-on-ad-click-incrementality/
  5. Practical Ecommerce. SEO Study Shows Incremental Clicks with Branded SEO Plus PPC. https://www.practicalecommerce.com/SEO-Study-Shows-Incremental-Clicks-with-Branded-SEO-Plus-PPC