Przejdź do treści

Wykrywanie anomalii SEO z Google Search Console, BigQuery i AI

Zobacz, jak zbudować system wykrywania anomalii SEO na danych Search Console, regułach SQL, BigQuery ML i generatywnym AI.

Maciej Sala

Founder StriveLab

13 min czytaniaAktualizacja

Dlaczego sam raport Search Console nie wystarcza do automatycznych alertów

Search Console udostępnia widok ostatnich 24 godzin, funkcję porównywania okresów oraz raport Insights, które pomagają w ręcznej analizie, lecz nie zastępują automatycznych alertów SEO precyzyjnie dopasowanych do architektury konkretnego serwisu. Średnia dla całej domeny potrafi skutecznie ukryć poważny problem w jednej sekcji, a trzeba jeszcze pamiętać o regularnym otwieraniu raportu i ręcznym ustawianiu właściwych filtrów.

Największym zagrożeniem są spadki częściowe. Gdy strona główna trzyma się mocno, a blog rośnie, katalog /poradniki/ może tracić widoczność tuż po wdrożeniu nowego layoutu. W efekcie ogólne statystyki domeny wyglądają pozornie dobrze, choć w firmowej kasie sytuacja wygląda zupełnie inaczej. To z kolei wymusza przeniesienie wykrywania anomalii znacznie bliżej surowych 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 danych i tabeli ExportLog:

  • 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.
  • ExportLog: rejestr udanych zapisów do każdej z tabel danych.

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 przechowuje liczniki potrzebne do ponownej agregacji.

Warto znać kilka szczegółów, 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,

  • eksport jest przyrostowy i może zawierać powtarzające się klucze, dlatego prawie zawsze ponownie agregujesz metryki,

  • ExportLog zapisuje wyłącznie udane eksporty, a tabele URL i site mogą zakończyć zapis o różnych porach,

  • tekst zapytania może zostać ukryty z powodów prywatności,

  • dane URL i site mają różną semantykę agregacji, więc ich sumy i średnie pozycje nie zawsze są bezpośrednio porównywalne,

  • metryki są zwykle przypisywane do adresu kanonicznego wybranego przez Google, a nie zawsze do URL-a, na który ostatecznie trafił użytkownik.

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. Średnią pozycję w stylu raportu GSC liczysz więc jako SUM(sum_position) / SUM(impressions) + 1 dla tabeli URL albo SUM(sum_top_position) / SUM(impressions) + 1 dla tabeli site.

Funkcja Bulk Data Export zaczyna zbierać dane dopiero od momentu konfiguracji – nie uzupełnia automatycznie wcześniejszej historii. Zapisane w BigQuery informacje zostaną tam tak długo, dopóki ich nie usuniesz lub nie skonfigurujesz wygaśnięcia partycji. Pamiętaj więc, że jeśli Twój detektor korzysta z kilkumiesięcznej bazy odniesienia, okres przechowywania danych w bazie musi być odpowiednio dłuższy niż samo okno analizy.

Dane godzinowe z Search Analytics API a eksport do BigQuery

Bulk Data Export świetnie sprawdza się jako źródło pełnych danych dobowych, ale nie działa w czasie rzeczywistym. Z kolei Search Analytics API obsługuje wymiar godzinowy ze statusem HOURLY_ALL, który pozwala pobrać do 10 dni danych z podziałem na godziny, przy czym najnowsze wpisy pojawiają się z kilkugodzinnym opóźnieniem.

Unikaj łączenia tych danych z zamkniętymi dniami w BigQuery bez wyraźnego oznaczenia. Ostatnie godziny mają charakter wstępny i mogą ulec zmianie. Najbezpieczniejsze podejście opiera się na dwóch niezależnych alarmach:

  1. Wczesny alarm godzinowy porównuje zakończone godziny z analogicznymi godzinami z poprzednich dni.

  2. Alarm dobowy potwierdza problem dopiero po poprawnym zapisie tabeli w ExportLog.

W sytuacji, kiedy biznes nie wymaga natychmiastowej reakcji tego samego dnia, prostszym i mniej podatnym na fałszywe alarmy rozwiązaniem będzie oparcie się wyłącznie na eksporcie dobowym.

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

Alert obejmujący całą domenę może początkowo wydawać się dobrym pomysłem, ale w praktyce okazuje się mało użyteczny, ponieważ ogólny spadek nie wskazuje przyczyny. Gdy spadek o 42% dotyczy katalogu /blog/ na urządzeniach mobilnych w Polsce, można od razu rozpocząć analizę: sprawdzić ostatnie wdrożenia, działania robotów indeksujących, strukturę szablonu, linkowanie wewnętrzne oraz nowe błędy w kodzie HTML.

Właśnie dlatego najpierw sklasyfikuj adresy według ścieżki, zamiast analizować cały adres. Dzięki temu parametry zapytania zawierające ciąg /blog/ nie przypiszą strony do niewłaściwej grupy.

Code
CREATE OR REPLACE VIEW `my_project.seo.segment_daily` AS
WITH source AS (
  SELECT
    data_date,
    url,
    COALESCE(
      REGEXP_EXTRACT(url, r'^https?://[^/]+(/[^?#]*)'),
      '/'
    ) AS path,
    country,
    device,
    clicks,
    impressions,
    sum_position
  FROM `my_project.searchconsole.searchdata_url_impression`
  WHERE search_type = 'WEB'
    AND url IS NOT NULL
)
SELECT
  data_date AS date,
  CASE
    WHEN path = '/blog' OR STARTS_WITH(path, '/blog/') THEN 'blog'
    WHEN path = '/uslugi' OR STARTS_WITH(path, '/uslugi/') THEN 'services'
    WHEN path = '/produkty' OR STARTS_WITH(path, '/produkty/') THEN 'products'
    WHEN path = '/kategorie' OR STARTS_WITH(path, '/kategorie/') THEN 'categories'
    ELSE 'other'
  END AS page_group,
  country,
  device,
  SUM(clicks) AS clicks,
  SUM(impressions) AS impressions,
  -- przechowujemy surową sumę pozycji; średnią liczymy dopiero po finalnej agregacji
  SUM(sum_position) AS sum_position
FROM source
GROUP BY date, page_group, country, device;

Widok agreguje od razu do poziomu używanego przez detektor. Przechowywanie w nim każdego URL-a i gotowego CTR byłoby zbędne, ponieważ kolejne zapytanie ponownie grupuje dane według page_group, kraju i urządzenia. Zwróć też uwagę na pozycję: eksport z GSC nie dostarcza gotowej średniej, lecz wartość sum_position. Faktyczną średnią obliczasz dopiero podczas odczytu za pomocą wyrażenia SAFE_DIVIDE(SUM(sum_position), SUM(impressions)) + 1. Nie wolno ponownie uśredniać średnich, ponieważ dałoby to niepoprawny wynik.

Prosta detekcja anomalii SEO na kompletnych danych

Na start nie potrzebujesz uczenia maszynowego. Możesz porównać ostatni pomyślnie wyeksportowany dzień z medianą ośmiu wcześniejszych wystąpień tego samego dnia tygodnia. Poniedziałek porównujesz z poniedziałkami, a nie z niedzielą.

Samo CURRENT_DATE() - 3 jest kruche. Eksport może pojawić się później, a korekta starszych danych może zostać zapisana ponownie. Datę raportową wybierz z ExportLog. Następnie zbuduj pełną siatkę segmentów i poprawnych dat. Bez tego segment, który spadł do zera i nie ma żadnego wiersza w tabeli, zniknie również z wyniku alertu.

Code
DECLARE report_date DATE DEFAULT (
  SELECT MAX(data_date)
  FROM `my_project.searchconsole.ExportLog`
  WHERE namespace = 'searchdata_url_impression'
);
 
ASSERT report_date IS NOT NULL AS 'Brak udanego eksportu tabeli URL';
 
WITH exported_dates AS (
  SELECT data_date AS date
  FROM `my_project.searchconsole.ExportLog`
  WHERE namespace = 'searchdata_url_impression'
    AND data_date BETWEEN DATE_SUB(report_date, INTERVAL 70 DAY) AND report_date
  GROUP BY date
),
comparison_dates AS (
  SELECT date
  FROM exported_dates
  WHERE EXTRACT(DAYOFWEEK FROM date) = EXTRACT(DAYOFWEEK FROM report_date)
  QUALIFY ROW_NUMBER() OVER (ORDER BY date DESC) <= 9
),
daily_raw AS (
  SELECT
    date,
    page_group,
    COALESCE(country, 'UNKNOWN') AS country,
    COALESCE(device, 'UNKNOWN') AS device,
    SUM(clicks) AS clicks,
    SUM(impressions) AS impressions,
    SUM(sum_position) AS sum_position
  FROM `my_project.seo.segment_daily`
  JOIN comparison_dates USING (date)
  GROUP BY 1, 2, 3, 4
),
segments AS (
  SELECT DISTINCT page_group, country, device
  FROM daily_raw
),
daily AS (
  SELECT
    d.date,
    s.page_group,
    s.country,
    s.device,
    COALESCE(r.clicks, 0) AS clicks,
    COALESCE(r.impressions, 0) AS impressions,
    SAFE_DIVIDE(r.clicks, r.impressions) AS ctr,
    SAFE_DIVIDE(r.sum_position, r.impressions) + 1 AS avg_position
  FROM comparison_dates d
  CROSS JOIN segments s
  LEFT JOIN daily_raw r
    ON r.date = d.date
    AND r.page_group = s.page_group
    AND r.country = s.country
    AND r.device = s.device
),
baseline_medians AS (
  SELECT
    page_group,
    country,
    device,
    APPROX_QUANTILES(clicks, 2)[OFFSET(1)] AS median_clicks,
    APPROX_QUANTILES(impressions, 2)[OFFSET(1)] AS median_impressions,
    APPROX_QUANTILES(ctr, 2)[OFFSET(1)] AS median_ctr,
    APPROX_QUANTILES(avg_position, 2)[OFFSET(1)] AS median_position
  FROM daily
  WHERE date < report_date
  GROUP BY page_group, country, device
  HAVING COUNT(*) >= 6
),
baseline AS (
  SELECT
    m.*,
    APPROX_QUANTILES(ABS(d.clicks - m.median_clicks), 2)[OFFSET(1)]
      AS mad_clicks
  FROM baseline_medians m
  JOIN daily d USING (page_group, country, device)
  WHERE d.date < report_date
  GROUP BY
    m.page_group,
    m.country,
    m.device,
    m.median_clicks,
    m.median_impressions,
    m.median_ctr,
    m.median_position
),
current_day AS (
  SELECT *
  FROM daily
  WHERE date = report_date
),
metrics AS (
  SELECT
    report_date,
    c.page_group,
    c.country,
    c.device,
    c.clicks,
    b.median_clicks AS baseline_clicks,
    SAFE_DIVIDE(c.clicks - b.median_clicks, b.median_clicks) AS clicks_delta,
    SAFE_DIVIDE(
      c.clicks - b.median_clicks,
      1.4826 * NULLIF(b.mad_clicks, 0)
    ) AS clicks_robust_z,
    c.impressions,
    b.median_impressions AS baseline_impressions,
    SAFE_DIVIDE(c.impressions - b.median_impressions, b.median_impressions)
      AS impressions_delta,
    c.ctr,
    b.median_ctr AS baseline_ctr,
    c.ctr - b.median_ctr AS ctr_delta,
    c.avg_position,
    b.median_position AS baseline_position,
    c.avg_position - b.median_position AS position_delta,
    GREATEST(b.median_clicks - c.clicks, 0) AS click_gap_vs_baseline,
    GREATEST(b.median_impressions - c.impressions, 0)
      AS impression_gap_vs_baseline
  FROM current_day c
  JOIN baseline b USING (page_group, country, device)
),
scored AS (
  SELECT
    * EXCEPT (impression_gap_vs_baseline),
    baseline_clicks >= 20
      AND click_gap_vs_baseline >= 10
      AND (clicks_delta <= -0.30 OR clicks_robust_z <= -3)
      AS has_clicks_drop,
    baseline_impressions >= 100
      AND impression_gap_vs_baseline >= 50
      AND impressions_delta <= -0.25
      AS has_visibility_drop,
    baseline_impressions >= 100
      AND ctr_delta <= -0.02
      AND ctr < baseline_ctr * 0.75
      AS has_ctr_drop,
    baseline_impressions >= 100
      AND position_delta >= 2
      AS has_ranking_drop
  FROM metrics
)
SELECT
  report_date,
  page_group,
  country,
  device,
  CASE
    WHEN IF(has_clicks_drop, 1, 0)
      + IF(has_visibility_drop, 1, 0)
      + IF(has_ctr_drop, 1, 0)
      + IF(has_ranking_drop, 1, 0) >= 2 THEN 'mixed'
    WHEN has_visibility_drop THEN 'visibility_drop'
    WHEN has_ctr_drop THEN 'ctr_drop'
    WHEN has_ranking_drop THEN 'ranking_drop'
    ELSE 'clicks_drop'
  END AS anomaly_type,
  * EXCEPT (
    report_date,
    page_group,
    country,
    device,
    has_clicks_drop,
    has_visibility_drop,
    has_ctr_drop,
    has_ranking_drop
  ),
  CURRENT_TIMESTAMP() AS created_at
FROM scored
WHERE has_clicks_drop
  OR has_visibility_drop
  OR has_ctr_drop
  OR has_ranking_drop
ORDER BY clicks_delta ASC;

Zapytanie stosuje osobne progi dla kliknięć, wyświetleń, CTR i pozycji. Dla kliknięć wymaga sensownego poziomu bazowego, minimalnej bezwzględnej różnicy oraz spadku procentowego albo dużego odchylenia względem medianowego odchylenia bezwzględnego, czyli MAD. Współczynnik 1.4826 skaluje MAD do wartości zbliżonej do odchylenia standardowego przy rozkładzie normalnym. Nie zamienia to prostej reguły w dowód statystyczny, ale ogranicza alarmy wywołane zwykłym szumem. Progi 20, 10, 100, 50, 25%, 2 pp i 2 pozycje są wartościami startowymi, a nie uniwersalną definicją anomalii — trzeba je dostroić do wolumenu i wartości biznesowej segmentów.

click_gap_vs_baseline jest różnicą względem poziomu bazowego, a nie wiarygodnym szacunkiem utraconego ruchu. Popyt, sezonowość, święta, kampanie marki i zmiany w wynikach wyszukiwania również wpływają na liczbę kliknięć.

Lepszy alert SEO: rozbij przyczynę na typ spadku

W powyższym zapytaniu instrukcja CASE nadaje etykietę na podstawie przekroczonych progów kliknięć, wyświetleń, CTR i średniej pozycji. Jeżeli jednocześnie wystąpią co najmniej dwa sygnały, alert otrzymuje etykietę mixed. W przeciwnym razie reguły rozróżniają spadek kliknięć, widoczności, CTR albo pogorszenie pozycji.

To nie jest diagnoza. To skrót, który mówi zespołowi, od którego obszaru zacząć. Warto też liczyć click_gap_vs_baseline, ponieważ alerty sortowane wyłącznie po procencie spadku promują małe segmenty. Różnicę względem poziomu bazowego traktuj jako miarę priorytetu, a nie dowód utracenia konkretnej liczby kliknięć przez błąd SEO.

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

Wrzucenie danych do modelu z pytaniem „czy mamy problem?” nie tworzy wiarygodnego detektora. Model może dopowiedzieć wzorzec, którego nie ma w danych. Reguła SQL też może być źle zaprojektowana, ale pozostaje jawna, testowalna i audytowalna.

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

Wartość ctr_delta w tym kontrakcie jest różnicą zapisaną jako ułamek. -0.04 oznacza spadek o 4 punkty procentowe, a nie o 4%. Jednostki powinny być jawne zarówno w tabeli alertów, jak i w prompcie.

Wynik analizy powinien być w pełni konkretny i wyraźnie rozdzielać twardą obserwację od samej hipotezy, co oznacza, że model może na przykład wykazać spadek wyświetleń, a następnie wskazać indeksację, zmiany w szablonie czy wahania popytu jako potencjalne obszary do sprawdzenia, nie powinien jednak pochopnie stwierdzać, że odnalazł ostateczną przyczynę problemu.

Jeśli chcesz wygenerować takie notatki z poziomu BigQuery, możesz użyć AI.GENERATE_TEXT z modelem zdalnym skonfigurowanym w BigQuery ML. Funkcja nadal wysyła prompt do wskazanego modelu w Vertex AI albo obsługiwanej usłudze modelowej, więc nie oznacza to, że dane pozostają wyłącznie w silniku BigQuery. Lokalizacja datasetu, połączenia i endpointu oraz wymagania dotyczące przetwarzania danych muszą być świadomą decyzją.

Model zdalny można utworzyć na przykład tak:

Code
CREATE OR REPLACE MODEL `my_project.seo.gemini_flash`
REMOTE WITH CONNECTION DEFAULT
OPTIONS (ENDPOINT = 'gemini-2.5-flash');

Przed uruchomieniem potrzebujesz aktywnych API, połączenia Cloud Resource oraz odpowiedniej roli dla konta usługi tego połączenia. Dataset i połączenie powinny znajdować się w zgodnej lokalizacji. Globalnego endpointu nie używaj, jeśli musisz kontrolować region przetwarzania.

Najpierw wybierz maksymalnie kilka najważniejszych alertów dla jawnie wskazanego @report_date, zbuduj z nich prompty według powyższego kontraktu i zapisz do małej tabeli stagingowej daily_prompts. Dopiero tę tabelę przekaż do modelu:

Code
SELECT result, prompt, status
FROM AI.GENERATE_TEXT(
  MODEL `my_project.seo.gemini_flash`,
  TABLE `my_project.seo_alerts.daily_prompts`,
  STRUCT(512 AS max_output_tokens, 0.2 AS temperature)
);

Jawny filtr daty podczas budowania tabeli stagingowej jest potrzebny także dlatego, że tabela alertów wymaga filtra partycji. Samo LIMIT w zapytaniu przekazanym bezpośrednio do AI.GENERATE_TEXT ogranicza liczbę wynikowych promptów, ale nie gwarantuje, że BigQuery pominie wcześniejsze przetwarzanie całego wejścia. Po wywołaniu kontroluj również status odpowiedzi modelu i obsłuż błędy limitów oraz nieudane wywołania, zamiast zakładać, że każdy wiersz otrzymał poprawny tekst.

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

BigQuery Scheduled Queries pozwala uruchamiać zapytanie cyklicznie. Dla eksportu GSC wystarczy jedno uruchomienie dziennie. Zapytanie powinno jednak wybrać najnowszą datę potwierdzoną w ExportLog, a nie zakładać stałego opóźnienia dwóch lub trzech dni.

Wyniki zapisuję do tabeli:

Code
my_project.seo_alerts.daily_anomalies

Minimalny schemat może wyglądać tak:

Code
CREATE TABLE IF NOT EXISTS `my_project.seo_alerts.daily_anomalies` (
  report_date DATE,
  page_group STRING,
  country STRING,
  device STRING,
  anomaly_type STRING,
  clicks INT64,
  baseline_clicks INT64,
  clicks_delta FLOAT64,
  clicks_robust_z FLOAT64,
  impressions INT64,
  baseline_impressions INT64,
  impressions_delta FLOAT64,
  ctr FLOAT64,
  baseline_ctr FLOAT64,
  ctr_delta FLOAT64,
  avg_position FLOAT64,
  baseline_position FLOAT64,
  position_delta FLOAT64,
  click_gap_vs_baseline INT64,
  created_at TIMESTAMP
)
PARTITION BY report_date
CLUSTER BY page_group, country, device
OPTIONS (require_partition_filter = TRUE);

Zapytanie cykliczne może zostać uruchomione ponownie po błędzie lub wykonane ręcznie. Z kolei, zwykłe polecenie WRITE_APPEND utworzy w takiej sytuacji duplikaty danych. Aby taka sytuacja nie miała miejsca, zapisuj wyniki za pomocą operacji MERGE z kluczem złożonym z daty i wymiarów albo nadpisuj wyłącznie partycję dotyczącą konkretnego dnia. Do samego uruchamiania zadań użyj dedykowanego konta usługi z minimalnymi uprawnieniami zamiast poświadczeń pojedynczego pracownika.

Najprostszy idempotentny wariant w zapytaniu skryptowym zapisuje najpierw wynik detektora do tabeli tymczasowej detected, a następnie atomowo zastępuje alerty dla przetwarzanej daty:

Code
-- CREATE TEMP TABLE detected AS (zapytanie detektora z poprzedniej sekcji);
BEGIN TRANSACTION;
DELETE FROM `my_project.seo_alerts.daily_anomalies`
WHERE report_date = @report_date;
 
INSERT INTO `my_project.seo_alerts.daily_anomalies`
SELECT * FROM detected WHERE report_date = @report_date;
COMMIT TRANSACTION;

W kodzie produkcyjnym zastąp komentarz pełnym zapytaniem detektora i utrzymuj identyczną kolejność kolumn w tabeli tymczasowej oraz docelowej. Alternatywą jest MERGE; usunięcie i ponowne wstawienie jednej partycji jest jednak krótsze, a zarazem usuwa alert, który po ponownym przeliczeniu przestał spełniać warunki.

Wartość epoch_version w tabeli ExportLog rośnie w sytuacji, gdy Google dokonuje poprawek wcześniej zapisanych danych. W sytuacji, gdy zajmujesz się materializowaniem agregatów, pamiętaj o zapamiętywaniu najwyższej przetworzonej wersji dla każdej pary namespace oraz data_date. Ewentualna zmiana tej wersji powinna automatycznie uruchomić ponowne przeliczenie odpowiedniej partycji danych oraz powiązanych alertów, których poziom bazowy korzysta z poprawionej daty.

Dopiero na tej tabeli budujesz powiadomienie do Slacka, poczty, Looker Studio albo webhooka. Powiadomienie nie powinno uruchamiać się dla każdego wiersza. Lepiej zebrać anomalie w jeden dzienny raport i posortować je po click_gap_vs_baseline, prawdopodobieństwie anomalii albo własnej wartości biznesowej.

Google Cloud pozwala oprzeć alert na metryce liczby wierszy zwróconych przez Scheduled Query, ponieważ zapytanie zwraca wiersze tylko w przypadku wykrycia anomalii, a Cloud Monitoring reaguje na wartość większą od zera. Metryka oparta na ostatniej liczbie wierszy może jednak utrzymywać swoją poprzednią wartość przez wiele tygodni po wyłączeniu lub awarii harmonogramu, dlatego należy osobno monitorować completed_runs, historię uruchomień albo logi usługi BigQuery Data Transfer Service. Pamiętaj, że całkowity brak nowego wykonania nie może wyglądać jak poprawny i bezpieczny stan systemów monitorowania.

Co sprawdzić po alercie

Alert sygnalizuje jedynie problem, a nie jego ostateczną przyczynę, dlatego po zauważeniu spadku w segmencie postępuję według następującej kolejności:

  • kompletność ExportLog i status wykonania pipeline'u,
  • oficjalną listę anomalii danych Search Console, aby wykluczyć błąd raportowania po stronie Google,

  • dane z analityki lub logów serwera, aby odróżnić problem raportowania od realnej zmiany wejść,

  • ostatnie zmiany w szablonie lub layoucie,
  • 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.

Kolejność zależy od sygnału. Kiedy problem pojawił się bezpośrednio po wdrożeniu i dotyczy konkretnego szablonu, sprawdzenie kodu ma wysoki priorytet. Jeśli natomiast spadły wyświetlenia w całej branży, trzeba uwzględnić sezonowość, popyt i zmiany w wynikach wyszukiwania. Warto przechowywać obok szeregu daty wdrożeń, migracji, kampanii, świąt oraz incydentów. Korelacja czasowa co prawda nie dowodzi przyczyny, ale pomaga ustalać kolejność pracy.

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

Kiedy dołożyć model anomalii

Zwykłe reguły SQL wystarczą na długi czas. Zaawansowany model analityczny ma uzasadnienie dopiero wtedy, gdy zarządzasz wieloma segmentami, uwzględniasz silną sezonowość, prowadzisz działania w różnych krajach lub obsługujesz zróżnicowane typy stron. Funkcja AI.DETECT_ANOMALIES korzysta z wbudowanego modelu TimesFM, który porównuje aktualne wyniki z prognozowanym przedziałem. W wyniku otrzymujesz m.in. flagę is_anomaly, granice przedziału, prawdopodobieństwo oraz status wywołania.

Zanim uruchomisz funkcję, musisz przygotować uporządkowaną, gęstą tabelę z dokładnie jednym wierszem dla każdej poprawnie wyeksportowanej daty oraz kombinacji identyfikatorów. Unikaj przekazywania surowej, nieregularnej tabeli, w której brak wpisu może oznaczać zarówno zerowy ruch, jak i zwykły błąd eksportu danych.

Code
SELECT *
FROM AI.DETECT_ANOMALIES(
  (
    SELECT
      date,
      page_group,
      country,
      device,
      clicks
    FROM `my_project.seo.segment_daily_dense`
    WHERE date >= DATE_SUB(
      (
        SELECT MAX(date)
        FROM `my_project.seo.segment_daily_dense`
      ),
      INTERVAL 180 DAY
    )
  ),
  data_col => 'clicks',
  timestamp_col => 'date',
  target_last_n_points => 7,
  id_cols => ['page_group', 'country', 'device'],
  anomaly_prob_threshold => 0.95
)
WHERE is_anomaly
  AND COALESCE(ai_detect_anomalies_status, '') = '';

BigQuery domyślnie korzysta obecnie z TimesFM 2.5 i automatycznie dobiera najmniejsze obsługiwane context_window, które mieści wejściowy szereg, dlatego w podstawowym przykładzie nie trzeba podawać tych parametrów. Wybór okna i próg detekcji nie mają jednak uniwersalnego charakteru, dlatego należy je walidować na historycznych incydentach oraz w stabilnych okresach, kiedy nic szczególnego się nie działo. Pamiętaj, że wyższy próg ogranicza liczbę anomalii, ponieważ poszerza przedział ufności. W przypadku segmentów z bardzo małym ruchem nawet poprawnie skonfigurowany i uruchomiony model może jednak nie dostarczyć użytecznego sygnału.

Dlatego traktuj model jako drugą warstwę:

  1. Warstwa pierwsza obejmuje kontrolę danych i reguły SQL. Jest szybka, przewidywalna i zrozumiała dla zespołu.
  2. Warstwa druga wykorzystuje model anomalii. Pomaga przy sezonowości i wielu szeregach czasowych.
  3. Warstwa trzecia wykorzystuje generatywne AI. Redaguje streszczenie i sugeruje pytania do sprawdzenia.

Kontrola kosztów i jakości danych

Koszt zależy od wybranego modelu rozliczeń w BigQuery, ilości faktycznie przetworzonych danych oraz liczby wywołań BigQuery ML i modeli zdalnych, przy czym w standardowym trybie rozliczeń on-demand klauzula LIMIT w żaden sposób nie zmniejsza liczby skanowanych bajtów. W praktyce:

  • filtruj bezpośrednio po partycji data_date i sprawdzaj plan zapytania,

  • zapisuj dzienne agregaty do tabeli partycjonowanej i klastrowanej zamiast za każdym razem czytać surowy eksport,

  • używaj progów wolumenu, żeby nie generować alertów i promptów dla mikrosegmentów,

  • sprawdzaj ExportLog, zanim uznasz spadek za problem SEO,

  • materializuj mały zbiór promptów przed wywołaniem modelu,

  • ogranicz max_output_tokens i liczbę segmentów wysyłanych do modelu,

  • używaj dry run oraz maximum bytes billed do kontroli zapytań,

  • ustaw retencję partycji zgodną z potrzebnym oknem historycznym i polityką danych.

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 ma raport skuteczności, widok ostatnich 24 godzin i Insights, ale nie zastępuje własnego procesu monitoringu. BigQuery pozwala automatyzować dzienne alerty, segmentować adresy URL i łączyć wyniki z wdrożeniami lub danymi biznesowymi. Do alarmów wymagających danych godzinowych można użyć Search Analytics API, pamiętając, że najnowsze dane są wstępne.

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ą ograniczenia prywatności. Tekst rzadkich zapytań może być ukryty, a w niektórych typach danych część wymiarów może być pusta. Nie należy też zakładać, że sumy po dowolnych wymiarach będą identyczne z wykresem zagregowanym na poziomie usługi.

Od ilu danych ma sens wykrywanie anomalii SEO?

Nie istnieje uniwersalny próg kliknięć. Liczy się wolumen w pojedynczym monitorowanym segmencie i liczba obserwacji historycznych. Przy małym ruchu lepiej agregować dane tygodniowo, łączyć podobne adresy URL i wymagać minimalnej bezwzględnej zmiany. Dzienny procent dla kilku kliknięć będzie generował głównie szum.

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

Nie. Model może opisać anomalię i zasugerować, gdzie szukać przyczyny. Pierwsza wersja detekcji powinna opierać się na jawnych regułach statystycznych i biznesowych w SQL. Generatywne AI nie powinno samodzielnie ogłaszać przyczyny ani uruchamiać zmian na stronie.

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

Nie w pierwszej wersji systemu, ponieważ AI.DETECT_ANOMALIES może pomóc przy wielu szeregach czasowych i sezonowości, ale nadal potrzebujesz kompletnych dat, segmentów, progów wpływu, monitoringu samego eksportu i procesu weryfikacji. Funkcja wykorzystuje wbudowany model TimesFM, a jej wynik jest sygnałem statystycznym, nie diagnozą SEO.

Jak często uruchamiać alerty SEO?

Raz dziennie wystarczy w większości projektów. Dane GSC mają opóźnienie, a Bulk Data Export zapisuje dane dobowe. Jeżeli potrzebujesz szybszego sygnału, Search Analytics API udostępnia dane godzinowe z ostatnich dni. Takie dane są wstępne, dlatego alarm godzinowy powinien mieć osobne progi i zostać potwierdzony po zamknięciu dnia.

O autorze

Maciej Sala

Maciej Sala — konsultant technologiczny produktów cyfrowych i web 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 rozwija własne projekty.

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

Skontaktuj się ze mną
LH.pl – Hosting Mango

Biblioteka wiedzy na temat SEO

Czytaj dalej

Zobacz więcej wpisów
Data Science w SQL i wdrażanie modeli AI w BigQuery

Model działający w BigQuery potrafi skrócić drogę od tabeli do pierwszej prognozy z dni do godzin. SQL nie zwalnia jednak z walidacji, kontroli kosztów ani zabezpieczenia danych wysyłanych do zdalnego modelu. Ten przewodnik pokazuje kompletny warsztat: od przygotowania szeregu czasowego, przez poprawne wywołania TimesFM i Gemini, aż po kryteria wdrożenia na produkcję.

Maciej Sala

Maciej Sala

Founder StriveLab

AI jako wsparcie analityka w 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

Optymalizacja Google Search Console w projektach Next.js

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 dane o widoczności i problemach dotyczących strony internetowej. Dowiesz się z niego, które strony Google zna i indeksuje, jakie błędy crawlowania występują, na jakie frazy rankujesz oraz jakie wyniki Core Web Vitals osiąga strona podczas rzeczywistych wizyt użytkowników.

Maciej Sala

Maciej Sala

Founder StriveLab

LH.pl – Cloud Server 1C4G