Misurare la visibilità del brand nelle risposte AI con Python

Abbiamo fatto girare 45 prompt su Google AI Mode tre volte ciascuno. In 43 casi su 45 i brand citati sono cambiati a parità di prompt. Metodo, numeri e script Python.

Per misurare la visibilità di un brand nelle risposte AI, fai girare un insieme fisso di prompt sulla superficie AI più volte, conta la quota di risposte che citano il brand con una regex a confine di parola, e riporta un intervallo invece di un numero solo. Una singola esecuzione non è una misura: il 19 ottobre 2026 abbiamo inviato 45 prompt a Google AI Mode tre volte ciascuno (135 risposte), e per 43 prompt su 45 l’insieme dei brand citati è cambiato fra esecuzioni identiche. Lo script qui sotto fa tutto il lavoro in circa 70 righe di Python, a 10 crediti per risposta.

Perché una sola esecuzione di una risposta AI non è una misura?

Perché lo stesso prompt restituisce ogni volta una risposta diversa. Sulle 135 risposte, solo 1 prompt su 45 ha prodotto tre testi identici. Fra le coppie (brand, prompt) in cui un brand è comparso almeno una volta, 138 su 302 (46%) mancavano in almeno una delle tre esecuzioni. Le risposte sono lunghe (mediana 396 parole) e nominano ciascuna circa cinque dei brand monitorati, quindi piccoli cambiamenti di formulazione fanno entrare e uscire i brand.

Se controlli il tuo brand una volta a settimana e lo vedi comparire e poi sparire, è soprattutto questo rumore, non un cambio della tua visibilità. Il post su AI Mode tratta il formato della risposta; questo riguarda cosa fare con molte risposte.

Come l’abbiamo misurato?

Tre categorie di prodotto che gli sviluppatori cercano, 15 prompt ciascuna, formulati come li scrive la gente: “best vector database”, “best vector database for rag”, “best async python web framework”, “web scraping api with a free tier”. Nessun prompt nomina un brand. Ogni prompt è andato a /v1/ai con country=us e language=en, tre volte. 135 chiamate, 1.350 crediti, 6,9 secondi in media (due chiamate di conferma a api.fastaf.co lo stesso giorno hanno richiesto 6,1 e 3,6 secondi, entrambe con X-FastAF-Credits-Used: 10). Una chiamata ha restituito HTTP 200 con una risposta vuota ed è stata addebitata 10 crediti all’epoca; dopo questa misura abbiamo cambiato la regola, e una risposta AI Mode vuota ora viene rimborsata. Trattata come dato, sarebbe valsa “nessun brand citato” per tutti, perciò lo script riprova comunque le risposte vuote.

Ho scelto in anticipo 14 brand per categoria. Le citazioni si contano solo nel testo della risposta (paragrafi, voci di elenco e celle di tabella), non nei titoli delle pagine citate. La “posizione” è l’ordine della prima comparsa fra i brand monitorati. Qui non c’è sentiment, né raccomandazione, né classifica dei prodotti: è quello che una superficie di Google ha detto in un giorno.

Cosa mostrano i dati?

Il tasso di citazione è la quota delle 45 risposte che nominano il brand. L’intervallo è un intervallo bootstrap al 95% che ricampiona i prompt, quindi include l’effetto di quali 15 prompt hai scelto. Esecuzione 1, 2 e 3 sono i tassi che avresti riportato da un singolo passaggio.

Categoria Brand Tasso Intervallo 95% Esec. 1 / 2 / 3
Vector database Qdrant 96% 87-100% 93 / 93 / 100
Pinecone 84% 71-96% 80 / 80 / 93
Weaviate 73% 56-91% 67 / 73 / 80
Chroma 53% 31-73% 60 / 53 / 47
Framework Python Flask 91% 82-98% 80 / 93 / 100
Starlette 49% 31-67% 53 / 40 / 53
Litestar 38% 20-58% 40 / 27 / 47
API di scraping Firecrawl 71% 51-91% 80 / 67 / 67
ScraperAPI 47% 29-64% 53 / 40 / 47
Apify 42% 24-60% 33 / 40 / 53

Saltano all’occhio tre cose. Primo, i brand a metà di una categoria oscillano di 10-20 punti fra un singolo passaggio e l’altro (Apify dal 33% al 53%); lo scarto medio per i brand sopra il 20% era di 11-14 punti. Secondo, con 15 prompt gli intervalli sono più o meno 20 punti a metà scala, quindi una variazione di “+5 punti” settimana su settimana è invisibile. Terzo, le classifiche cambiano da un passaggio all’altro: nella categoria scraping, ScraperAPI era terzo nel passaggio 1, e Apify ha sostituito Oxylabs nei primi cinque solo nel passaggio 3. Tasso e posizione sono inoltre due cose diverse: Pinecone è stato nominato prima (posizione media 2,2) di Qdrant (2,9) pur con un tasso più basso.

Le risposte citano i siti dei brand stessi?

Per lo più no. Fra gli hostname distinti citati per risposta, la quota appartenente a uno dei brand monitorati era 44% per le API di scraping, 30% per i vector database e 31% per i framework Python. Il resto erano terze parti: reddit.com compare in 52 delle 135 risposte (39%), poi github.com, YouTube e blog di confronti. Chi scrive di te conta quanto ciò che dice il tuo sito.

Anche le fonti variano: due esecuzioni dello stesso prompt condividevano in media il 28-34% dei siti citati (sovrapposizione di Jaccard), quindi anche “quali pagine cita la risposta” richiede delle ripetizioni.

Come lo eseguo io in Python?

Imposta FASTAF_API_KEY, riempi PROMPTS con 15 o più prompt e BRANDS con i nomi che ti interessano, poi eseguilo. Ogni chiamata viene aggiunta a runs.jsonl, quindi un crash o una riesecuzione saltano quanto è già fatto.

import json, os, random, re, time
from concurrent.futures import ThreadPoolExecutor

import requests

API = "https://api.fastaf.co/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['FASTAF_API_KEY']}"}
REPEATS = 3
PROMPTS = ["best vector database", "best vector database for rag", "best self hosted vector database"]  # use 15
BRANDS = {  # case-sensitive on purpose: "Falcon" is not "falcon"
    "Qdrant": r"\bQdrant\b", "pgvector": r"\bpgvector\b", "Milvus": r"\bMilvus\b",
    "Pinecone": r"\bPinecone\b", "Weaviate": r"\bWeaviate\b", "Chroma": r"\bChroma(?:DB)?\b",
}

def text_of(node):
    """Every visible string of the answer: paragraphs, list items, table cells."""
    if isinstance(node, str):
        return [node]
    if isinstance(node, list):
        return [s for v in node for s in text_of(v)]
    if isinstance(node, dict):
        return [s for k, v in node.items() if k in ("snippet", "text", "items", "table") for s in text_of(v)]
    return []

def ask(prompt):
    for attempt in range(5):
        r = requests.get(f"{API}/ai", headers=HEADERS, timeout=90,
                         params={"query": prompt, "country": "us", "language": "en"})
        if r.status_code == 200:
            text = "\n".join(text_of(r.json()["text_blocks"]))
            if text:  # an empty 200 is not "no brand mentioned": retry it
                return text
        time.sleep(3 * (attempt + 1))
    raise RuntimeError(f"gave up on {prompt!r}")

def collect(path="runs.jsonl"):
    done = {(r["prompt"], r["run"]) for r in map(json.loads, open(path))} if os.path.exists(path) else set()
    jobs = [(p, n) for p in PROMPTS for n in range(1, REPEATS + 1) if (p, n) not in done]
    def work(job):
        prompt, n = job
        row = {"prompt": prompt, "run": n, "text": ask(prompt)}
        with open(path, "a") as f:  # one line per finished call
            f.write(json.dumps(row) + "\n")
    with ThreadPoolExecutor(max_workers=5) as pool:
        list(pool.map(work, jobs))

def interval(per_prompt, n_boot=4000):
    """95% bootstrap interval resampling PROMPTS, keeping each prompt's runs together."""
    rng, k = random.Random(7), len(per_prompt)
    rates = sorted(sum(rng.choices(per_prompt, k=k)) / k for _ in range(n_boot))
    return rates[int(0.025 * n_boot)], rates[int(0.975 * n_boot)]

def report(path="runs.jsonl"):
    rows = [json.loads(line) for line in open(path)]
    print(f"{'brand':10}{'rate':>6}{'95% CI':>10}{'run1':>6}{'run2':>6}{'run3':>6}")
    table = []
    for brand, rx in BRANDS.items():
        hit = {(r["prompt"], r["run"]): bool(re.search(rx, r["text"])) for r in rows}
        per_prompt = [sum(hit[(p, n)] for n in range(1, REPEATS + 1)) / REPEATS for p in PROMPTS]
        by_run = [sum(hit[(p, n)] for p in PROMPTS) / len(PROMPTS) for n in range(1, REPEATS + 1)]
        table.append((sum(per_prompt) / len(PROMPTS), brand, interval(per_prompt), by_run))
    for rate, brand, (lo, hi), by_run in sorted(table, reverse=True):
        print(f"{brand:10}{rate:>6.0%}{f'{lo:.0%}-{hi:.0%}':>10}" + "".join(f"{x:>6.0%}" for x in by_run))

collect()
report()

Con la lista di tre prompt qui sopra, il 19 ottobre 2026 ha stampato pgvector, Weaviate, Qdrant e Milvus al 100% in ogni esecuzione, Chroma al 78% (67 / 67 / 100) e Pinecone al 67%.

Quali errori di conteggio cambiano i numeri?

Maiuscole e confini di parola. Senza \b, “Jina” trova corrispondenze dentro parole più lunghe; senza distinzione di maiuscole, “Bottle”, “Falcon” e “Reflex” combaciano con la prosa normale, e “scraper API” in minuscolo conta come il prodotto ScraperAPI. Nel mio set, il confronto senza distinzione di maiuscole ha portato ScraperAPI da 21 a 22 risposte e Zyte da 4 a 5, poco qui, ma con un brand dal nome comune è facile incappare in un “falcon” o “bottle” minuscolo. Aggiungi gli alias che vuoi (“ChromaDB”, “Bright Data” senza lo spazio) e tienili in un elenco rivisto. Conta inoltre solo la risposta, non la query riecheggiata, o un prompt che contiene il nome del tuo brand misura il tuo prompt.

Per le AI Overviews la stessa estrazione funziona sull’oggetto ai_overview dentro una risposta di /v1/serp, che costa 5 crediti invece di 10; quel post tratta presenza e fonti per parola chiave.

La GEO cambia cosa misurare?

Il paper sulla GEO (Aggarwal et al., KDD 2024) ha riportato fino al 40% di visibilità in più nelle risposte dei generative engine sul suo benchmark GEO-bench, ma si tratta di un insieme testato di modifiche ai contenuti, non di una garanzia per il tuo sito. La guida di Google alle funzioni di AI generativa, aggiornata l’ultima volta il 10 luglio 2026 al momento di questo controllo, dice “The best practices for SEO continue to be relevant” e che non servono markup speciali né un file llms.txt per comparire in Google Search. Quindi il lavoro resta SEO ordinaria; quello che aggiungi è un modo per capire se ha spostato qualcosa, e la risposta di sopra è: solo con ripetizioni e un intervallo.

Quanto costa?

45 prompt x 3 esecuzioni sono 1.350 crediti, cioè circa $0,68 sul pacchetto Starter da $10 (20.000 crediti). Una categoria da 15 prompt con 3 ripetizioni sono 450 crediti. Le richieste fallite non vengono addebitate. Vedi i prezzi e la documentazione.

La versione breve

Conta le citazioni con una regex a confine di parola e sensibile alle maiuscole, esegui ogni prompt almeno tre volte, e riporta il tasso di citazione con un intervallo che ricampiona i prompt. Nelle nostre 135 risposte, il 46% delle comparse brand-prompt è sparito in almeno una ripetizione, e meno della metà delle fonti citate erano i siti dei brand stessi. Inizia gratis con 500 crediti e fai girare lo script sulla tua categoria.