毎月実施している小規模調査では、ある業界から10件の購買意図クエリを抽出し、4つのAIアシスタントに同一の質問を投げ、それぞれが実際に参照したソースを記録している。

分析自体は容易だ。セットの重複を測り、ドメイン数を数えれば完了する。

困難だったのは、4つの異なるAPIから引用を抽出して比較可能な形に整えること——その作業で週末を費やした。必要になったときにどこにも情報がなかったため、ここに書き残しておく。

問題

4社とも使用したURLを返してくれるが、どこに配置するか、どのように命名するか、「URL」とはそもそも何を指すかについて、合意は一切存在しない。

各APIが返す内容の概要は次のとおりだ(2026年半ば時点の形状——仕様は変動するので、必ず最新ドキュメントを確認すること)。

OpenAI:Responses API + web searchツール。引用はテキスト出力の注釈として同梱される:

const urls = [];
for (const item of resp.output ?? []) {
  for (const block of item.content ?? []) {
    for (const a of block.annotations ?? []) {
      if (a.type === "url_citation") urls.push(a.url);
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Anthropic:Messages API + web searchツール。検索結果はテキストとは独立した専用のコンテンツブロックとして返される:

const urls = [];
for (const block of msg.content ?? []) {
  if (block.type === "web_search_tool_result") {
    for (const r of block.content ?? []) {
      if (r.url) urls.push(r.url);
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Gemini:Google Search grounding。候補のgrounding metadata配下に配置される:

const urls = (resp.candidates?.[0]?.groundingMetadata?.groundingChunks ?? [])
  .map(c => c.web?.uri)
  .filter(Boolean);

Enter fullscreen mode Exit fullscreen mode

Perplexity:レスポンス直下のフラット配列として提供される:

const urls = resp.citations ?? resp.search_results?.map(r => r.url) ?? [];

Enter fullscreen mode Exit fullscreen mode

ネストの深さが4通り、同一概念を表す単語が3種類(「annotation」「search result」「grounding chunk」)、さらにURL自体を返さないものも存在する。詳細は後述する。

つまり、各プロバイダーごとにアダプターを用意し、{ engine, question, urls[] } に正規化してから境界を超えないようにする——後から見れば当然のことだが、最初からそうしなかったことを後悔した。

落とし穴1:Geminiのgrounding URIは実際の出典ではない

これが静かに数値を破壊しかねない問題だった。

GeminiのgroundingChunks[].web.uriは発行元のURLではなく、Google独自のgroundingエンドポイント経由のリダイレクトURLである。鵜呑みにすると、Geminiの引用元がすべて同一ドメインに見えてしまい、「Geminiがどのサイトを参照しているか」という分析結果がたった1サイトに収束してしまう。

解決するにはリダイレクトを辿る必要がある:

async function resolveFinal(url) {
  try {
    const r = await fetch(url, { method: "HEAD", redirect: "follow" });
    return r.url;
  } catch {
    return url; // 原文を保持し、フラグを立てて続行
  }
}

Enter fullscreen mode Exit fullscreen mode

大量処理で得た2つの知見。HEADで十分であり、GETよりはるかに安価——必要なのは最終URLだけである。また、同一リダイレクトが何度も出現するため、積極的にキャッシュしないと無用な負荷がかかる。

この問題に気づいたきっかけは、初回実行時にGeminiの数値が異常だったことだ。1エンジンのドメイン多様性がほぼゼロに収束した場合には、ほぼ確実に抽出バグである——そのようなサニティチェックを早い段階で実装しておく価値がある。

落とし穴2:同一ページが5通りの表記で現れる

正しいURLを取得しても、それらが互いに一致しない。以下はすべて同一ページである:

https://www.example.com/clinic/
http://example.com/clinic
https://example.com/clinic?utm_source=chatgpt.com
https://example.com/clinic#reviews
https://example.com/Clinic

Enter fullscreen mode Exit fullscreen mode

集合を比較する際、1ページが5通りに表記されると「異なる」ソースが5つと数えられ、重複率が実際より低く算出されてしまう。もし「重複が驚くほど少ない」という結論を導こうとしているなら、これはまさに望ましくない方向への自滅である。

最終的に採用した正規化関数:

const TRACKING = /^(utm_|fbclid|gclid|msclkid|ref|source$)/i;

function canonical(raw) {
  const u = new URL(raw);
  u.protocol = "https:";
  u.hostname = u.hostname.replace(/^www\./, "").toLowerCase();
  u.hash = "";
  for (const k of [...u.searchParams.keys()]) {
    if (TRACKING.test(k)) u.searchParams.delete(k);
  }
  u.pathname = u.pathname.replace(/\/+$/, "") || "/";
  return u.toString();
}

Enter fullscreen mode Exit fullscreen mode

意図的にパスを小文字化していない——多くのサーバーでパスは大文字小文字を区別するため、誤って2つの異なるページを統合するよりは重複を許容する判断である。

笑えた事例:複数のアシスタントが返却URLに?utm_source=chatgpt.com(または同等の独自パラメータ)を付与していた。ツールが自らのリファラートラフィックをタグ付けしているのである。剥がさないとエンジン固有の重複が生じる。

分析——こちらは本当に退屈な作業

質問ごとのセット重複率をエンジン間でペアワイズ計算:

const jaccard = (a, b) => {
  const A = new Set(a), B = new Set(b);
  const inter = [...A].filter(x => B.has(x)).length;
  const union = new Set([...A, ...B]).size;
  return union ? inter / union : 0;
};

Enter fullscreen mode Exit fullscreen mode

Jaccard指数には明らかな欠陥がある——1回しか参照されていないソースと、すべてのエンジンが全質問で参照したソースを同一に扱ってしまう。重み付けについては検討を重ねたが、どちらが適切か結論は出ていない。「2つのシステムが同一の根拠を参照したか」を測るより良い指標があれば、ぜひ教えていただきたい。

得られた数値

今月の対象は美容・審美クリニック。10質問、9都市、4エンジン。

  • 214の異なるWebサイトがわずか10質問で参照された
  • そのうち75%は1エンジンのみで、他エンジンは一切参照しなかった
  • 同一質問における任意の2エンジン間の重複率:7%
  • 10質問中、4エンジンすべてが参照したWebサイトが出たのはわずか1質問
  • 参照されたものの約13%がディレクトリサイトで、残りは事業者自身のサイトだった

最後の割合は業種によって変動した——前月の法律事務所調査では、ディレクトリドメインが上位を占めていた。

すぐに浮かぶ反論は「LLMは非決定的なので、2回の呼び出しで異なるのは当然——これはノイズではないか」というものだろう。

そこでサブセットを各エンジン3回ずつ再実行し、エンジン自身との比較を行った:

  • 同一エンジン・再質問:51%の重複
  • 異なるエンジン・同一実行:7%の重複

同一エンジン内での一貫性は、競合エンジンとの一致度より約7.5倍高かった。実行ごとのばらつきは存在するが、エンジン間の乖離よりはるかに小さい。相違は構造的なものであり、サンプリング誤差ではない。

逆方向からの検証として、単一アシスタントが特定サイトを認識できるかどうかについても別途まとめている——同じ課題を1エンジンずつ取り扱ったものだ。

主張できないこと

1業種・10質問はサンプルであり、全数調査ではない。ばらつき制御は2質問×3回再実行のみで、全セットには及ばない。「参照」とは当該レスポンスの引用にURLが登場したことを意味し、重み付けやページ内のどの部分が実際に寄与したかは不明である。また、すべては2026年7月時点のスナップショットであり、これらのシステムは常に変化している。

同様の取り組みを行う人への示唆

プロバイダー横断で検索結果を比較するコードを書く場合、分析ではなく抽出と正規化に時間の大部分を割く覚悟が必要だ。筆者の体感では、退屈な作業が全体の80%を占めた。

また、サニティチェックは早期に実装すべきだ。筆者が実際に遭遇した2つのバグ(リダイレクト問題とトラッキングパラメータ問題)は、出力上は一切可視化されなかった。コードは正常に動作し、数値だけが誤っていた——しかも仮説を都合よく補強する方向に。こうしたバグこそ、過剰に警戒する価値がある。

最後に一点: 「AI可視性」を単一のチャネルとして扱う考え方は、データに触れた瞬間に崩壊する。同一質問に対して4つのエンジンがほぼ完全に異なる参照リストを作成した——つまり、問題は1つではなく4つ存在する。

今月の質問別内訳を含む完全版レポートはこちら。生のJSONLデータも共有可能なので、件数の確認や訂正があればぜひ。

(開示:筆者はこの分野のツールを開発している。調査は非公開ではなく、登録も不要である。)