Medio sector te va a decir que el query fan-out se "simula" pidiéndole a un modelo que se invente las sub-consultas de una keyword. Está bien como aproximación, pero es adivinar. Lo que casi nadie cuenta es que puedes sacar el abanico real —las consultas que el sistema lanza de verdad y las webs que consulta para responder— directamente de la API, campo por campo. Eso es lo que te voy a enseñar aquí.

Este post es el hermano pequeño y afilado del pilar sobre cómo funcionan las IAs por dentro. Allí monté la tubería completa (embeddings, RAG, grounding, source attribution); aquí bajo a una sola pieza y la abro en canal: el fan-out y, sobre todo, cómo ver qué dominios pisa el motor. Va con código real y con la distinción que casi nadie hace bien.

El fan-out en una frase (y por qué te cambia la unidad de trabajo)

Ante una consulta, el motor no lanza una búsqueda: usa un LLM para partir tu pregunta en un abanico de sub-consultas sintéticas —relacionadas, implícitas, comparativas, recientes— que ejecuta en paralelo y luego sintetiza. Google lo confirmó al presentar AI Mode: su técnica de fan-out descompone la pregunta en subtemas y dispara muchas búsquedas a la vez para bajar más hondo en la web que una búsqueda tradicional. La consecuencia para ti es directa: tu unidad de optimización deja de ser la keyword y pasa a ser el clúster de sub-intenciones. Si solo cubres la cabecera, apareces en una rama del abanico y desapareces en las otras diez. Hasta aquí, el pilar. Ahora lo interesante.

Consultado ≠ citado: la distinción que lo cambia todo

Antes de tocar una línea de código, mete esto en la cabeza porque es donde casi todo el mundo se equivoca. Hay dos conjuntos de webs distintos y no son lo mismo:

  • Consultadas / recuperadas: todas las URLs que el motor buscó y leyó para construir la respuesta. Es el universo real del que tira.
  • Citadas / mostradas: el subconjunto que acaba enlazado en la respuesta final. Casi siempre es mucho más pequeño.

La documentación de OpenAI lo dice sin rodeos: el campo de fuentes devuelve la lista completa de URLs que el modelo consultó, y su número suele ser mayor que el de citas. Traducción SEO: puedes estar siendo consultado —el motor te lee, entras en la deliberación— y no salir citado. Ese hueco es oro, porque te dice que estás en el radar y que el problema es de citabilidad (extraibilidad, autoridad), no de descubrimiento. Casi todas las herramientas del mercado solo miran lo citado. Nosotros vamos a mirar las dos capas.

El método estrella: groundingMetadata de la API de Gemini

Cuando llamas a Gemini con la herramienta de Google Search activada, la respuesta trae un objeto groundingMetadata que es, literalmente, la caja negra abierta. Tres campos:

  • webSearchQueries: el array de consultas que el sistema lanzó a Google. Esto es el fan-out real, no una simulación.
  • groundingChunks: las fuentes recuperadas, cada una con web.uriweb.title y web.domain. Las webs consultadas, servidas en bandeja.
  • groundingSupports: el mapeo fino de qué trozo de la respuesta se apoya en qué chunk (con segment y groundingChunkIndices). Ojo: el confidenceScore venía relleno hasta Gemini 2.0; de 2.5 en adelante llega vacío, así que no cuentes con él.

Un fragmento del JSON que devuelve, para que veas la pinta:

"groundingMetadata": {
  "webSearchQueries": ["quién ganó la última super bowl"],
  "groundingChunks": [
    { "web": { "uri": "https://…", "title": "Super Bowl LX", "domain": "domain.com" } },
    { "web": { "uri": "https://…", "title": "Resultados SB LX", "domain": "domain.com" } }
  ],
  "groundingSupports": [
    { "segment": { "endIndex": 125, "text": "…" }, "groundingChunkIndices": [0, 1] }
  ]
}

Y así lo extraes con el SDK de Python. Cuatro líneas y tienes el abanico y los dominios consultados de cualquier consulta comercial:

from google import genai
from google.genai import types

client = genai.Client()
tool   = types.Tool(google_search=types.GoogleSearch())
cfg    = types.GenerateContentConfig(tools=[tool])

r  = client.models.generate_content(
    model="gemini-2.5-flash",
    contents="mejor CRM para pymes en España",
    config=cfg,
)
gm = r.candidates[0].grounding_metadata

print("FAN-OUT (sub-consultas reales):")
for q in gm.web_search_queries or []:
    print("  -", q)

print("\nDOMINIOS CONSULTADOS:")
for ch in gm.grounding_chunks or []:
    if ch.web:
        print(f"  {ch.web.domain:30s}  {ch.web.title}")

Dos honestidades para que no te la pegues. Primera: esto expone la misma maquinaria de recuperación, pero es el grounding de tu llamada a la API, no un volcado literal del AI Mode de consumo — es una ventana a cómo piensa el sistema, no una foto exacta de lo que ve el usuario en google.com. Segunda: algún modelo en preview ha dejado de devolver groundingChunks temporalmente, así que valida que el campo viene antes de fiarte. Y una nota legal que a un SEO se le olvida: si muestras estos resultados en un producto, Google te obliga a renderizar sus Search Suggestions tal cual vienen en searchEntryPoint. Para análisis interno da igual; para producto, cúmplelo.

Lo mismo en OpenAI, Perplexity y Anthropic

Gemini es el más generoso, pero los demás también te dan las fuentes si sabes dónde mirar.

OpenAI (Responses API + web_search). La respuesta trae dos cosas distintas, y aquí es donde vive la distinción consultado/citado: las annotations de tipo url_citation en el mensaje son lo citado (url, title, posición en el texto); y el campo sources —que activas con include=["web_search_call.action.sources"]— es lo consultado, la lista completa, casi siempre más larga.

from openai import OpenAI
client = OpenAI()

r = client.responses.create(
    model="gpt-5.1",
    tools=[{"type": "web_search"}],
    include=["web_search_call.action.sources"],
    input="mejor CRM para pymes en España",
)

consultadas, citadas = [], []
for item in r.output:
    if item.type == "web_search_call":
        consultadas += [s.url for s in item.action.sources]      # todo lo que leyó
    if item.type == "message":
        for a in item.content[0].annotations:
            if a.type == "url_citation":
                citadas.append(a.url)                            # solo lo que citó

print(f"consultadas: {len(consultadas)}  ·  citadas: {len(citadas)}")

Perplexity (Sonar API). El más directo: cada respuesta trae los campos citations y search_results como top-level, con las URLs. Sonar Pro devuelve de media el doble de citas que Sonar. Un POST a api.perplexity.ai/chat/completions con model: "sonar" y ya tienes las fuentes en data["search_results"]. Truco: fija search_recency_filter a week si tu nicho es sensible a frescura, porque sin él Perplexity tiende a fuentes más antiguas pero de más autoridad.

Anthropic (Claude + web search). Devuelve bloques de resultado de búsqueda con url y título, y en el texto mete citations con el fragmento citado, la url y el título. Misma idea: fuentes estructuradas listas para parsear.

Con esto ya puedes hacer algo que las herramientas de pago cobran: comparar qué universo de dominios consulta cada motor para la misma query comercial. Spoiler de lo que vas a ver: el solape entre ChatGPT y Perplexity es bajísimo (se ha medido en torno al 11% de dominios). Cada motor es un canal distinto, con sus fuentes fetiche —ChatGPT tira muchísimo de Wikipedia, Perplexity históricamente de Reddit y de lo reciente—. Optimizar "para la IA" en abstracto no existe; optimizas para el patrón de cada motor.

La otra cara: los logs de bots te dicen quién viene a leerte

Las APIs te enseñan lo que el motor consulta cuando  preguntas. Los logs de tu servidor te enseñan lo que los crawlers de IA vienen a leer de tu web, en real, todos los días. Son la señal de "consultado" más honesta que tienes, porque es tráfico real contra tu dominio. Hay que saber leerlos, porque no todos los bots hacen lo mismo. Tres familias:

  • Crawlers de entrenamiento (GPTBotClaudeBotCCBotGoogle-Extended): alimentan el modelo. Bloquearlos NO te quita de las citas; solo decide si tu contenido entra en el entrenamiento. Es una decisión de propiedad intelectual, no de visibilidad.
  • Crawlers de recuperación en vivo (OAI-SearchBotPerplexityBotClaude-SearchBot): indexan para responder en tiempo real. Bloquear estos SÍ te borra de las respuestas. Son los que importan para GEO.
  • Fetchers disparados por el usuario (ChatGPT-UserPerplexity-UserClaude-User): buscan tu página cuando alguien, en el chat, pide algo que la toca. Son la señal de demanda más caliente que existe.

Un dato que te reordena las prioridades: en un log de 14 días documentado públicamente, ChatGPT-User fue el bot más activo con diferencia, y los tres crawlers de OpenAI juntos supusieron ~58% de todo el tráfico de bots de IA. Traducción: hoy la visibilidad en ChatGPT pesa de forma desproporcionada. Míralo en tu propio log, no en el de otro.

El barrido rápido sobre tu access log:

grep -aoE '(GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-User|Claude-SearchBot|PerplexityBot|Perplexity-User|Google-Extended|Googlebot|Bytespider|Amazonbot|Meta-ExternalAgent)' access.log \
  | sort | uniq -c | sort -rn

Dos avisos que te ahorran hacer el ridículo en un informe. Uno: el user-agent se falsea con un header, así que para afirmar "me visitó GPTBot" hay que verificarlo por DNS inverso o contra los rangos de IP publicados por cada proveedor, no por la cadena de texto. Dos: hay bots que se saltan el robots.txt —Bytespider es el caso clásico, y Cloudflare documentó en 2025 rastreo no declarado de Perplexity más allá de sus bots listados—. Y la línea que separa esto del resto: un log prueba que te leyeron, no que te citaron. Consultado, otra vez, no es citado.

Qforia mostrando el abanico de sub-consultas de query fan-out generado para una consulta comercial
// Qforia (iPullRank) simulando el fan-out de una query con la API de Gemini. Útil para explorar; para el dato real, la extracción de arriba.

Herramientas que te lo dan masticado

Montártelo tú da control y sale casi gratis, pero para escala y series temporales hay atajos. Qforia, de iPullRank, simula el fan-out con la API de Gemini y te lo pinta en árbol: perfecto para explorar la forma del abanico de una query antes de escribir contenido. Para monitorización continua de qué fuentes cita cada motor sobre tu set de prompts, el source tracking de Peec AILLM Pulse o el Conversation Explorer de Profound hacen el trabajo pesado —lo cubrí en detalle en el pilar—. Mi regla: los scripts para el análisis puntual y competitivo que quieres bajo tu control; las herramientas para el tracking diario que no quieres mantener a mano.

De datos a acción: el mapa de dominios que dominan tu espacio de respuesta

Aquí es donde todo esto deja de ser un truco de fiesta y se convierte en inteligencia competitiva. Coge tu set de queries comerciales, pásalas por el grounding de Gemini, y agrega la frecuencia de dominios consultados. Lo que te sale es el mapa de quién manda en tu espacio de respuesta: los dominios que aparecen una y otra vez son los que el motor considera autoridad en tu categoría. Ahí ves de un vistazo dónde estás ausente, qué sub-consultas del fan-out no cubre nadie tuyo (huecos de contenido) y —lo más accionable— en qué webs de terceros necesitas estar mencionado, porque son las que el motor lee para hablar de tu sector.

from google import genai
from google.genai import types
from collections import Counter
from urllib.parse import urlparse

client = genai.Client()
tool   = types.Tool(google_search=types.GoogleSearch())
cfg    = types.GenerateContentConfig(tools=[tool])

MONEY_QUERIES = [
    "mejor CRM para pymes en España",
    "software de facturación autónomos",
    "alternativa a Holded",
    # … tu set comercial completo, por fase del funnel
]

dominios = Counter()
fanout   = {}

for q in MONEY_QUERIES:
    r  = client.models.generate_content(model="gemini-2.5-flash", contents=q, config=cfg)
    gm = r.candidates[0].grounding_metadata
    fanout[q] = list(gm.web_search_queries or [])
    for ch in gm.grounding_chunks or []:
        if ch.web and ch.web.uri:
            dom = ch.web.domain or urlparse(ch.web.uri).netloc
            dominios[dom] += 1

print("DOMINIOS QUE DOMINAN TU ESPACIO DE RESPUESTA:")
for dom, n in dominios.most_common(20):
    print(f"  {n:3d}  {dom}")

Ese ranking es tu backlog de PR digital y de contenido, ordenado por cómo funciona el motor de verdad y no por intuición. Los dominios de arriba con los que no tienes relación son objetivos de mención. Las sub-consultas de fanout que ninguna de tus páginas responde son artículos por escribir. Y si cruzas esto con tus logs de bots, cierras el círculo: sabes qué te consultan a ti y qué consultan de tu competencia.

El SEO clásico te enseñó a mirar la SERP. Esto es mirar la deliberación entera del motor: las preguntas que se hace y las fuentes que lee antes de responder. La mayoría sigue optimizando para la foto final; tú puedes trabajar sobre el proceso. Ahí está la ventaja, al menos hasta que sea lo normal — que lo será.