Przejdź do treści

Wykrywanie anomalii SEO z Google Search Console, BigQuery i AI

Ruch SEO spada, a Search Console pokazuje problem dopiero po czasie. Jak zbudować własny system wykrywania anomalii w BigQuery wsparty AI?

Maciej Sala

Founder StriveLab

8 min czytaniaOpublikowano 4 czerwca 2026 (Aktualizacja 6 lipca 2026)

Problem wykrywania anomalii SEO: Google Search Console mówi za późno

Search Console nie jest złym narzędziem. Po prostu nie powstało jako system ostrzegania. Wchodzisz do raportu, wybierasz zakres dat, filtrujesz zapytania, potem strony, potem urządzenia. Jeśli masz szczęście, widzisz spadek. Jeśli nie, widzisz średnią, która ukrywa problem w jednej sekcji serwisu.

Najgorsze są spadki częściowe. Strona główna trzyma się dobrze, blog rośnie, ale katalog /poradniki/ traci widoczność po wdrożeniu nowego layoutu. W raporcie domeny wszystko wygląda „prawie normalnie”. W przychodach już nie.

Dlatego detekcję anomalii trzeba przenieść bliżej danych.

Dane z Google Search Console: czego szukać w eksporcie do BigQuery

Bulk Data Export z Google Search Console zapisuje dane do BigQuery codziennie. W praktyce najczęściej pracujesz na dwóch tabelach:

  • searchdata_site_impression: dane zagregowane na poziomie właściwości, z zapytaniami, krajem, typem wyszukiwania i urządzeniem.
  • searchdata_url_impression: dane na poziomie URL, dobre do analizy katalogów, szablonów i konkretnych stron.

Do pierwszej wersji systemu alertów biorę tabelę URL. Daje mniej „ładny” obraz, ale lepiej odpowiada na pytanie, które naprawdę pada w firmie: co dokładnie spadło?

Załóżmy, że tabela nazywa się:

Code
my_project.searchconsole.searchdata_url_impression

W danych masz między innymi datę, URL, kraj, urządzenie, typ wyszukiwania, kliknięcia, impresje i sumę pozycji. CTR i średnią pozycję liczysz samodzielnie, bo eksport nie zapisuje ich jako gotowych metryk na każdym poziomie agregacji.

Warto znać trzy szczegóły, zanim zaczniesz pisać alerty:

  • data_date jest datą danych w czasie Pacific Time, więc nie próbuj dopasowywać jej co do godziny do polskiej strefy czasowej,
  • dane mogą mieć powtarzające się klucze, dlatego prawie zawsze agregujesz kliknięcia, impresje i pozycję,
  • tabela ExportLog zapisuje udane eksporty; warto ją monitorować, bo przerwa w eksporcie może wyglądać jak anomalia .

W tabeli URL Google używa pola sum_position, a w tabeli site pola sum_top_position. W obu przypadkach pozycja jest liczona od zera, więc średnią pozycję w stylu raportu GSC liczysz jako SUM(sum_position) / SUM(impressions) + 1.

Segment URL przed alertem: jak ograniczyć fałszywe alarmy SEO

Alert na całą domenę jest kuszący. Jest też mało użyteczny.

Jeśli ruch spada o 12% w całym serwisie, nie wiesz jeszcze nic. Jeśli spada o 42% w katalogu /blog/ na mobile w Polsce, zaczyna się praca. Możesz sprawdzić ostatnie wdrożenia, crawl, indeksację, szablon, linkowanie wewnętrzne i nowe błędy w HTML.

Dlatego pierwszy krok to prosta klasyfikacja URL-i.

Code
CREATE OR REPLACE VIEW `my_project.seo.url_daily` AS
SELECT
  data_date AS date,
  url,
  CASE
    WHEN REGEXP_CONTAINS(url, r'/blog/') THEN 'blog'
    WHEN REGEXP_CONTAINS(url, r'/uslugi/') THEN 'services'
    WHEN REGEXP_CONTAINS(url, r'/produkty/') THEN 'products'
    WHEN REGEXP_CONTAINS(url, r'/kategorie/') THEN 'categories'
    ELSE 'other'
  END AS page_group,
  country,
  device,
  SUM(clicks) AS clicks,
  SUM(impressions) AS impressions,
  SAFE_DIVIDE(SUM(clicks), SUM(impressions)) AS ctr,
  -- przechowujemy surową sumę pozycji; średnią liczymy dopiero po finalnej agregacji
  SUM(sum_position) AS sum_position
FROM `my_project.searchconsole.searchdata_url_impression`
WHERE search_type = 'web'
GROUP BY date, url, page_group, country, device;

Zwróć uwagę na pozycję: eksport GSC nie daje gotowej średniej, tylko sum_position. Średnią liczysz dopiero przy odczycie: SAFE_DIVIDE(SUM(sum_position), SUM(impressions)) + 1. Dlatego widok przechowuje surową sumę, a nie uśrednioną wartość — inaczej przy kolejnej agregacji po katalogach wyszłaby średnia ze średnich, czyli liczba bez sensu.

To nie musi być piękne. Ma być czytelne i łatwe do zmiany, gdy dodasz nowy katalog albo typ strony.

Prosta detekcja anomalii SEO: dziś kontra poprzednie tygodnie

Na start nie potrzebujesz uczenia maszynowego. W wielu projektach wystarczy porównać ostatni dostępny dzień z medianą z poprzednich pięciu takich samych dni tygodnia. Poniedziałek porównujesz z poniedziałkami, a nie z niedzielą.

Code
DECLARE report_date DATE DEFAULT DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY);
 
WITH daily AS (
  SELECT
    date,
    page_group,
    country,
    device,
    SUM(clicks) AS clicks,
    SUM(impressions) AS impressions,
    SAFE_DIVIDE(SUM(clicks), SUM(impressions)) AS ctr,
    SAFE_DIVIDE(SUM(sum_position), SUM(impressions)) + 1 AS avg_position
  FROM `my_project.seo.url_daily`
  WHERE date BETWEEN DATE_SUB(report_date, INTERVAL 35 DAY) AND report_date
  GROUP BY date, page_group, country, device
),
baseline AS (
  SELECT
    page_group,
    country,
    device,
    APPROX_QUANTILES(clicks, 100)[OFFSET(50)] AS median_clicks,
    APPROX_QUANTILES(impressions, 100)[OFFSET(50)] AS median_impressions,
    APPROX_QUANTILES(ctr, 100)[OFFSET(50)] AS median_ctr,
    APPROX_QUANTILES(avg_position, 100)[OFFSET(50)] AS median_position
  FROM daily
  WHERE date < report_date
    AND EXTRACT(DAYOFWEEK FROM date) = EXTRACT(DAYOFWEEK FROM report_date)
  GROUP BY page_group, country, device
),
current_day AS (
  SELECT *
  FROM daily
  WHERE date = report_date
)
SELECT
  report_date,
  c.page_group,
  c.country,
  c.device,
  c.clicks,
  b.median_clicks,
  SAFE_DIVIDE(c.clicks - b.median_clicks, b.median_clicks) AS clicks_delta,
  c.impressions,
  b.median_impressions,
  SAFE_DIVIDE(c.impressions - b.median_impressions, b.median_impressions) AS impressions_delta,
  c.ctr,
  b.median_ctr,
  c.ctr - b.median_ctr AS ctr_delta,
  c.avg_position,
  b.median_position,
  c.avg_position - b.median_position AS position_delta,
  GREATEST(b.median_clicks - c.clicks, 0) AS estimated_lost_clicks
FROM current_day c
JOIN baseline b USING (page_group, country, device)
WHERE b.median_clicks >= 20
  AND SAFE_DIVIDE(c.clicks - b.median_clicks, b.median_clicks) <= -0.30
ORDER BY clicks_delta ASC;

To zapytanie robi jedną rzecz: pokazuje segmenty, które straciły co najmniej 30% kliknięć względem typowego wyniku dla tego samego dnia tygodnia. Próg 20 kliknięć chroni przed alertami z mikrodanych, gdzie jedna wizyta robi dramatyczny procent.

Lepszy alert SEO: rozbij przyczynę na typ spadku

Spadek kliknięć to objaw. Przyczyna siedzi głębiej.

Jeśli impresje spadły, problem może dotyczyć indeksacji, pozycji, sezonowości albo popytu. Jeśli impresje stoją w miejscu, ale CTR leci w dół, sprawdź title, description, rich results, zmianę intencji zapytań i wygląd SERP. Jeśli pozycja spada, zacznij od zmian w treści, linkowaniu, wydajności i konkurencji.

W SQL możesz dodać prostą etykietę:

Code
CASE
  WHEN impressions_delta <= -0.25 THEN 'visibility_drop'
  WHEN ctr < median_ctr * 0.75 THEN 'ctr_drop'
  WHEN position_delta >= 2 THEN 'ranking_drop'
  ELSE 'mixed'
END AS anomaly_type

To nie jest diagnoza. To skrót, który mówi zespołowi, od której szuflady zacząć. Warto też liczyć estimated_lost_clicks, bo alerty sortowane po procencie spadku często promują małe segmenty, a alerty sortowane po utraconych kliknięciach szybciej pokazują realny wpływ na biznes.

AI w monitoringu SEO: autor notatki, nie sędzia

Najgorszy pomysł: wrzucić dane do modelu i zapytać „czy mamy problem?”. Model może zgadywać. SQL nie zgaduje.

Lepszy układ wygląda tak:

  1. SQL wykrywa segmenty, które przekroczyły progi.
  2. Query dopisuje typ anomalii i metryki.
  3. AI dostaje tylko wyniki alertu i pisze krótką notatkę dla człowieka.

Przykładowy prompt:

Code
Napisz krótką notatkę SEO po polsku.
Nie diagnozuj ponad dane.
Użyj 4 sekcji:
- co spadło
- skala spadku
- najbardziej prawdopodobny obszar do sprawdzenia
- pierwsze 3 kroki
 
Dane:
page_group: blog
country: POL
device: MOBILE
clicks_delta: -0.41
impressions_delta: -0.38
ctr_delta: -0.04
position_delta: +0.6
anomaly_type: visibility_drop

Wynik powinien być nudny i konkretny. Na przykład: „Blog, Polska, mobile stracił 41% kliknięć i 38% impresji względem typowego poniedziałku. CTR prawie się nie zmienił, więc zacznij od indeksacji, crawl budgetu, zmian w sitemapie i ostatnich wdrożeń na szablonie wpisu”.

Tyle. Bez dramatu, bez wróżenia.

Jeśli chcesz wygenerować takie notatki bez wynoszenia danych z BigQuery, możesz użyć AI.GENERATE_TEXT na zdalnym modelu skonfigurowanym w BigQuery ML. W praktyce nie wysyłaj do modelu całej tabeli GSC. Wysyłaj tylko kilka już zagregowanych wierszy alertu i trzymaj parametry generowania pod kontrolą.

Code
SELECT *
FROM AI.GENERATE_TEXT(
  MODEL `my_project.seo.gemini_flash`,
  (
    SELECT
      FORMAT(
        '''
        Napisz krótką notatkę SEO po polsku.
        Nie diagnozuj ponad dane.
        Segment: %s / %s / %s.
        Spadek kliknięć: %.0f%%.
        Spadek impresji: %.0f%%.
        Zmiana CTR: %.2f pp.
        Zmiana pozycji: %.2f.
        Typ anomalii: %s.
        Utracone kliknięcia względem mediany: %d.
        ''',
        page_group,
        country,
        device,
        clicks_delta * 100,
        impressions_delta * 100,
        ctr_delta * 100,
        position_delta,
        anomaly_type,
        estimated_lost_clicks
      ) AS prompt
    FROM `my_project.seo_alerts.daily_anomalies`
    WHERE report_date = DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY)
    ORDER BY estimated_lost_clicks DESC
    LIMIT 5
  ),
  STRUCT(512 AS max_output_tokens, 0.2 AS temperature)
);

To nadal wymaga konfiguracji połączenia i zdalnego modelu w Google Cloud, ale architektonicznie jest czyste: SQL wybiera dane, AI tylko redaguje komunikat.

Harmonogram wykrywania anomalii SEO: raz dziennie i do tabeli alertów

BigQuery Scheduled Queries pozwala uruchamiać zapytanie cyklicznie. Dla GSC ustawiam zwykle codzienne uruchomienie rano, ale z datą raportu cofniętą o 2-3 dni, bo dane Search Console nie przychodzą natychmiast.

Wyniki zapisuję do tabeli:

Code
my_project.seo_alerts.daily_anomalies

Minimalny schemat:

Code
report_date DATE,
page_group STRING,
country STRING,
device STRING,
anomaly_type STRING,
clicks INT64,
baseline_clicks INT64,
clicks_delta FLOAT64,
impressions INT64,
baseline_impressions INT64,
impressions_delta FLOAT64,
avg_position FLOAT64,
baseline_position FLOAT64,
position_delta FLOAT64,
estimated_lost_clicks INT64,
created_at TIMESTAMP

Dopiero na tej tabeli budujesz powiadomienie: Slack, e-mail, dashboard w Looker Studio albo webhook do n8n. Powiadomienie nie powinno odpalać się na każdy wiersz. Lepiej zebrać alerty w jeden dzienny raport i posortować je po utraconych kliknięciach.

Google Cloud pozwala też oprzeć alert o metrykę liczby wierszy zwróconych przez scheduled query. To przydatny wariant: zapytanie zwraca wiersze tylko wtedy, gdy są anomalie, a Cloud Monitoring wysyła powiadomienie, gdy liczba wierszy jest większa od zera. Do pełnej treści alertu i tak zwykle potrzebujesz osobnej tabeli wynikowej albo webhooka.

Co sprawdzić po alercie

Alert mówi „tu jest dym”. Nie mówi jeszcze „tu jest ogień”. Po spadku w segmencie idę tym porządkiem:

  • ostatnie wdrożenia na szablon lub layout,
  • zmiany w robots.txt, sitemapie, linkach kanonicznych i meta robots,
  • raport indeksowania w GSC dla przykładowych URL-i,
  • logi crawlowania, jeśli masz Cloudflare lub serwerowe access logi,
  • zmiany title i description,
  • nowa treść konkurencji albo zmiana intencji w SERP,
  • problemy z renderowaniem, szczególnie przy React/Next.js.

Najpierw technika, potem treść. Jeśli problem wyszedł dzień po wdrożeniu, nie zaczynaj od burzy mózgów o „jakości treści”. Sprawdź kod.

Dobry alert SEO nie ma imponować. Ma skrócić drogę od spadku do pierwszej sensownej decyzji.

Kiedy dołożyć model anomalii

Reguły SQL wystarczą długo. Model ma sens, gdy masz dużo segmentów, mocną sezonowość, kilka krajów, różne typy stron i alerty zaczynają się mieszać. Wtedy można sięgnąć po bardziej statystyczne podejście: odchylenie standardowe, percentyle, model szeregu czasowego albo funkcję AI.DETECT_ANOMALIES w BigQuery.

Najprostszy wariant dla szeregu dziennego wygląda koncepcyjnie tak:

Code
SELECT *
FROM AI.DETECT_ANOMALIES(
  (
    SELECT
      date,
      page_group,
      country,
      device,
      SUM(clicks) AS clicks
    FROM `my_project.seo.url_daily`
    WHERE date >= DATE_SUB(CURRENT_DATE(), INTERVAL 180 DAY)
    GROUP BY date, page_group, country, device
  ),
  data_col => 'clicks',
  timestamp_col => 'date',
  target_last_n_points => 7,
  id_cols => ['page_group', 'country', 'device'],
  anomaly_prob_threshold => 0.95
);

To podejście wykorzystuje wbudowany model szeregów czasowych, ale nadal potrzebuje dobrych danych wejściowych. Jeśli segment ma 12 kliknięć miesięcznie, model nie wyczaruje stabilnego sygnału. Jeśli w danych brakuje kilku dni eksportu, model może potraktować problem techniczny z danymi jako problem SEO.

Dlatego traktuj model jako drugą warstwę:

  • warstwa 1: reguły SQL — szybkie, tanie, zrozumiałe dla zespołu,
  • warstwa 2: model anomalii — pomocny przy sezonowości i wielu seriach czasowych,
  • warstwa 3: generatywne AI — streszczenie alertu i lista pierwszych kroków.

Kontrola kosztów i jakości danych

Taki system jest tani tylko wtedy, gdy pilnujesz zakresu zapytań. BigQuery rozlicza skanowane dane, a wywołania modeli generatywnych mogą dokładać osobny koszt po stronie AI. W praktyce:

  • filtruj po data_date i nie skanuj całej historii przy każdym uruchomieniu,
  • zapisuj dzienne agregaty do własnej tabeli, zamiast za każdym razem czytać surowy eksport GSC,
  • używaj progów wolumenu, żeby nie generować alertów i promptów dla mikrosegmentów,
  • sprawdzaj ExportLog, zanim uznasz spadek za problem SEO,
  • ogranicz max_output_tokens, LIMIT i liczbę segmentów wysyłanych do modelu.

To nie jest detal techniczny. Bez kontroli kosztów alert SEO szybko staje się kolejnym raportem, którego nikt nie chce utrzymywać.

Audyt techniczny i optymalizacja pod kątem SEO i GEO.
Audyt techniczny SEO

Często zadawane pytania

Po co wykrywać anomalie SEO w BigQuery, skoro mam raporty w Search Console?

Search Console dobrze pokazuje, co się stało. BigQuery pozwala zbudować własny radar: dzienne alerty, segmenty URL, porównania tydzień do tygodnia i reguły dopasowane do Twojej strony. Nie czekasz, aż ktoś ręcznie otworzy raport.

Czy Bulk Data Export z Search Console ma limit wierszy jak interfejs GSC?

Nie działa jak zwykły eksport z interfejsu. Google opisuje Bulk Data Export jako dzienny zrzut danych do BigQuery, bez typowego limitu wierszy z UI. Nadal obowiązują jednak ograniczenia prywatności: rzadkie zapytania mogą być anonimizowane, a część pól może być pusta.

Od ilu danych ma sens wykrywanie anomalii SEO?

Sensowny próg to zwykle kilka tysięcy kliknięć miesięcznie albo duży serwis z wieloma grupami URL. Przy mniejszym ruchu też da się to zrobić, ale alerty będą bardziej szumiały i trzeba pracować na dłuższych oknach, np. 14 albo 28 dni.

Czy AI powinno samo decydować, że mamy problem SEO?

Nie. Model może opisać anomalię i zasugerować, gdzie szukać przyczyny. Sama detekcja powinna zaczynać się od twardych reguł w SQL: spadek kliknięć, spadek impresji, zmiana CTR, utrata pozycji, koncentracja problemu w konkretnym typie stron.

Czy BigQuery AI.DETECT_ANOMALIES zastępuje własne reguły SQL?

Nie w pierwszej wersji systemu. AI.DETECT_ANOMALIES może pomóc przy większych szeregach czasowych i mocnej sezonowości, ale nadal potrzebujesz segmentów, progów wolumenu, tabeli alertów i procesu weryfikacji. Bez tego model wykryje odchylenie, którego zespół nie umie przełożyć na decyzję.

Jak często uruchamiać alerty SEO?

Raz dziennie wystarczy w większości projektów. Dane GSC mają opóźnienie, więc alert co godzinę daje fałszywe poczucie kontroli. Dzienny rytm jest prostszy, tańszy i lepiej pasuje do procesu naprawy.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat SEO

Czytaj dalej

Zobacz więcej wpisów
Data Science w SQL: Jak wdrażać modele AI w Google BigQuery bez Pythona

Zapomnij o żmudnym eksporcie danych do zewnętrznych narzędzi i skomplikowanych skryptach w Pythonie. Dziś praca analityka zmienia się na naszych oczach: model predykcyjny uruchamiasz jedną komendą w konsoli BigQuery , a wyniki odbierasz w tej samej tabeli, w której zaczynałeś pracę. Sprawdź, jak wykorzystanie AI w SQL rewolucjonizuje analitykę danych w firmach – bez konieczności bycia ekspertem od machine learningu.

Maciej Sala

Maciej Sala

Founder StriveLab

AI jako wsparcie analityka: hurtownie danych, SQL i Google BigQuery

AI może skrócić drogę od pytania biznesowego do zapytania SQL , ale nie zbuduje wiarygodnej analityki na niejednoznacznych danych. Zanim analityk odda modelowi część pracy, firma potrzebuje jednej definicji metryk, opisanego schematu i kontroli nad tym, co trafia do asystenta.

Maciej Sala

Maciej Sala

Founder StriveLab

Google Search Console + Next.js — indeksacja, błędy, performance i co z nimi robić

Trudno wyobrazić sobie współczesną analitykę bez Google Search Console GSC , czyli podstawowego narzędzia pokazującego dane bezpośrednio z Google Search. PageSpeed Insights mierzy wydajność, Ahrefs śledzi backlinki, ale GSC pokazuje całą paletę danych ale też problemów/błędów dotyczących strony internetowej. Przykładowo, które strony są zaindeksowane, jakie błędy crawlowania występują, na jakie frazy rankujesz i jakie wyniki Core Web Vitals Dane Core Web Vitals w GSC pochodzą z Chrome UX Report, czyli realnych wizyt użytkowników, a nie z pojedynczego testu laboratoryjnego Lighthouse. mają istniejący użytkownicy.

Maciej Sala

Maciej Sala

Founder StriveLab