Agenty danych i GraphRAG na urządzeniu ze Spanner Omni

1. Przegląd

To ćwiczenie obejmuje podstawy Spanner Omni, wyjaśnia, dlaczego jest to najlepsza baza danych dla autonomicznych agentów AI, oraz przedstawia ogólną, kompleksową implementację wdrożonego na brzegu sieci, opartego na agencie trenera taktycznego i symulatora meczów pickleballa, który jest obsługiwany przez Spanner Omni (Relational + Graph GQL), Python Flask i Gemini 3.8 Flash.

Problem

Nowoczesne interaktywne platformy sportowe i środowiska gier w czasie rzeczywistym wymagają jednoczesnej obsługi 3 różnych zbiorów zadań:

Transakcje operacyjne o wysokiej równoczesności:

Rezerwacje kortów, członkostwo graczy, rozliczenia i harmonogramy turniejów wymagające ścisłej serializacji ACID w celu wyeliminowania konfliktów związanych z podwójną rezerwacją.

Dane telemetryczne połączonego wykresu:

Ciągłe sekwencje uderzeń, łańcuchy Markowa i wykresy pokrycia stref wymagające przechodzenia z małymi opóźnieniami bez rozbudowanych potoków ETL.

Czas odpowiedzi na brzegu sieci poniżej milisekundy:

Szybkie symulacje fizyki z częstotliwością 60 FPS, w których czas przesyłania danych w obie strony w sieci chmurowej o szerokim zasięgu (100–200 ms) powodowałby niedopuszczalne zakłócenia.

W przeszłości zespoły inżynierów rozwiązywały ten problem, łącząc ze sobą fragmentaryczne architektury, które wyglądały mniej więcej tak: PostgreSQL do danych transakcyjnych, Neo4j do grafów, Redis do tymczasowego przechowywania danych na serwerach brzegowych i potoki synchronizacji w tle. Powoduje to rozbieżności w architekturze, dużą złożoność operacyjną i opóźnienia w spójności wynikowej.

Jak to zrobić!

Wdrażając Google Cloud Spanner Omni na brzegu sieci i łącząc go z Google Gemini 3.8 Flash, stworzyliśmy Spanner Omni Pickleball Game – platformę gier sportowych z jednym silnikiem, która łączy relacyjną logikę biznesową, natywną telemetrię uderzeń ISO GQL i trenera AI, który udziela wskazówek taktycznych w czasie rzeczywistym.

Spanner Omni

Spanner Omni udostępnia rozproszony silnik bazy danych Google o skali globalnej, który zapewnia skalowalność poziomą, transakcje ACID i możliwości wielomodelowe (relacyjne, grafowe, wektorowe i analityczne) w samodzielnie zarządzanych, lokalnych, wielochmurowych i odciętych od sieci zewnętrznych środowiskach. Spanner Omni to dostępna do pobrania, samodzielnie zarządzana wersja Google Cloud Spanner. Zapewnia podstawowe funkcje rozproszonej bazy danych Spanner – automatyczne fragmentowanie, replikację opartą na Paxos i silną spójność zewnętrzną – niezależnie od tego, gdzie zdecydujesz się ją uruchomić:

acc871a470f63b29.png

Czego się nauczysz

W tym ćwiczeniu przejdziesz od tradycyjnych, odizolowanych architektur baz danych do nowoczesnego, ujednoliconego rozwiązania baz danych na brzegu sieci. Nauczysz się:

  1. Wdróż i organizuj Spanner Omni lokalnie przy użyciu Dockera, aby zarządzać wydajnymi, jednoinstancyjnymi relacyjnymi zbiorami zadań.
  2. Zaprojektuj ujednolicony schemat grafu relacyjnego i ISO GQL, który mapuje transakcyjne modele biznesowe wraz z wysokowymiarową telemetrią gry bez konieczności synchronizacji zewnętrznej bazy danych.
  3. Zaimplementuj ścisłą serializowalność ACID w transakcjach zapisu i odczytu w Pythonie, aby programowo zapobiegać sytuacjom wyścigu i eliminować konflikty związane z podwójną rezerwacją.
  4. Utwórz natywny potok GraphRAG, który wykorzystuje zapytania grafowe ISO GQL zintegrowane z SQL, aby przekazywać dane telemetryczne meczu w czasie rzeczywistym i z uwzględnieniem kontekstu bezpośrednio do Gemini w celu natychmiastowego udzielania wskazówek taktycznych.

Wymagania

  • standardowy komputer lokalny lub maszyna wirtualna do programowania z systemem Linux (Ubuntu 22.04 lub nowszy), macOS lub Windows;
  • Zainstalowany i uruchomiony Docker Desktop lub Docker Engine.
  • Zainstalowany Python 3.10 lub nowszy.
  • edytor tekstu lub IDE (np. VS Code lub PyCharm);
  • Aktywny klucz interfejsu Gemini API Google. (Uwaga: do lokalnego uruchamiania Spanner Omni nie jest wymagane konto rozliczeniowe Google Cloud, ponieważ usługa działa na bezpłatnej, samodzielnie zarządzanej licencji deweloperskiej).

2. Wdrażanie i konfiguracja Spannera Omni

W tym kroku uruchomimy lokalną instancję bazy danych Edge. Spanner Omni działa w wysoce zoptymalizowanym kontenerze, zapewniając możliwości relacyjne na skalę planetarną na lokalnym laptopie dewelopera.

  1. Otwórz terminal i utwórz stały wolumin Dockera do przechowywania schematów bazy danych i danych telemetrycznych gry:
docker volume create spanner_omni_data
  1. Pobierz i uruchom oficjalny obraz kontenera Spanner Omni skonfigurowany w trybie programisty na jednym serwerze:
docker run -d --name spanner-omni \
  -p 9010:9010 -p 9020:9020 \
  -v spanner_omni_data:/spanner \
  us-docker.pkg.dev/spanner-omni/images/spanner-omni:2026.r2.1-beta \
  start-single-server
  1. Sprawdź, czy kontener został uruchomiony:
docker ps

🛠️ Szybkie rozwiązywanie problemów: jeśli uruchomienie nie powiedzie się, ponieważ porty 9010 lub 9020 są już używane przez procesy w tle lub starsze emulatory, wykonaj w terminalu ten skrypt czyszczący:

pkill -f "spanner" || true
fuser -k 9010/tcp 9020/tcp 2>/dev/null || true
docker rm -f spanner-local spanner-omni spanner-omni-local 2>/dev/null || true

3. Projektowanie schematu DDL grafu relacyjnego i ISO GQL

Po aktywowaniu Spanner Omni możemy utworzyć schemat bazy danych. Ten schemat stanowi jedno źródło wiarygodnych danych, łącząc relacyjne podmioty gospodarcze (takie jak korty, gracze i rezerwacje) z szybkimi, połączonymi schematami telemetrii grafów (uderzenia, strefy i przejścia).

Aby utworzyć tabele operacyjne i telemetryczne:

  1. Łączenie się z bazą danych za pomocą interfejsu wiersza poleceń Spanner lub klienta aplikacji
  2. Uruchamianie instrukcji DDL

W tej chwili nie musisz nic robić, ponieważ zajmujemy się tym w kodzie aplikacji:

-- ============================================================================
-- 1. OPERATIONAL & FACILITY MANAGEMENT (RELATIONAL ACID)
-- ============================================================================

CREATE TABLE Courts (
    court_id INT64 NOT NULL,
    name STRING(100) NOT NULL,
    location STRING(100) NOT NULL,
    surface_type STRING(50) NOT NULL,
    is_indoor BOOL NOT NULL,
    hourly_rate FLOAT64 NOT NULL,
    has_lighting BOOL NOT NULL,
    created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (court_id);

CREATE TABLE Players (
    player_id INT64 NOT NULL,
    name STRING(100) NOT NULL,
    email STRING(100) NOT NULL,
    skill_level STRING(20) NOT NULL,
    phone STRING(50),
    created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (player_id);

CREATE TABLE Bookings (
    booking_id INT64 NOT NULL,
    court_id INT64 NOT NULL,
    player_id INT64 NOT NULL,
    booking_date DATE NOT NULL,
    start_hour INT64 NOT NULL,
    end_hour INT64 NOT NULL,
    player_name STRING(100) NOT NULL,
    court_name STRING(100) NOT NULL,
    status STRING(20) NOT NULL,
    notes STRING(MAX),
    created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (booking_id);

CREATE TABLE MatchRequests (
    match_id INT64 NOT NULL,
    player_id INT64 NOT NULL,
    player_name STRING(100) NOT NULL,
    court_id INT64,
    court_name STRING(100),
    play_date DATE NOT NULL,
    start_hour INT64 NOT NULL,
    skill_required STRING(20) NOT NULL,
    match_type STRING(20) NOT NULL,
    spots_open INT64 NOT NULL,
    status STRING(20) NOT NULL,
    notes STRING(MAX),
    created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (match_id);

-- ============================================================================
-- 2. TACTICAL GAME TELEMETRY & PROPERTY GRAPH (ISO GQL)
-- ============================================================================

CREATE TABLE CourtZones (
    zone_id STRING(50) NOT NULL,
    name STRING(100) NOT NULL,
    is_kitchen BOOL NOT NULL,
    side STRING(50) NOT NULL
) PRIMARY KEY (zone_id);

CREATE TABLE Shots (
    shot_id STRING(50) NOT NULL,
    player_id STRING(50) NOT NULL,
    shot_type STRING(100) NOT NULL,
    speed_mph FLOAT64,
    spin_type STRING(50),
    outcome STRING(50),
    from_zone_id STRING(50),
    to_zone_id STRING(50)
) PRIMARY KEY (shot_id);

CREATE TABLE ShotTransitions (
    transition_id STRING(50) NOT NULL,
    from_shot_id STRING(50) NOT NULL,
    to_shot_id STRING(50) NOT NULL,
    rally_depth INT64,
    pressure_level STRING(20)
) PRIMARY KEY (transition_id);

-- ============================================================================
-- 3. MATCH SESSIONS & AI COACHING HISTORY
-- ============================================================================

CREATE TABLE GameSessions (
    session_id STRING(50) NOT NULL,
    player_id INT64 NOT NULL,
    player_score INT64 NOT NULL,
    bot_score INT64 NOT NULL,
    bot_difficulty STRING(20) NOT NULL,
    winner STRING(20) NOT NULL,
    rally_length INT64 NOT NULL,
    fault_type STRING(100),
    coaching_summary STRING(MAX),
    created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (session_id);
cat << 'EOF' > Dockerfile
FROM ollama/ollama:latest
ENV OLLAMA_HOST=0.0.0.0:8080
ENV OLLAMA_MODELS=/root/.ollama
ENV MODEL=gemma4:e2b
RUN ollama serve & sleep 5 && ollama pull $MODEL
ENTRYPOINT ["ollama", "serve"]
EOF

💡 Dlaczego sygnatury czasowe zatwierdzenia są ważne: określenie OPTIONS (allow_commit_timestamp=true) umożliwia monotoniczne, dokładne do mikrosekundy porządkowanie we wszystkich lokalnych replikach węzłów. Jest to kluczowe dla synchronizacji szybkich zdarzeń w grze (takich jak nagrania wideo z wywołaniem linii) z odpowiednimi logami telemetrycznymi uderzeń w bazie danych.

4. Kontrola równoczesności ACID i zapobieganie podwójnym rezerwacjom

Na aktywnych portalach do rezerwacji obiektów sportowych wielu użytkowników często próbuje zarezerwować te same terminy. Aby zapobiec sytuacjom wyścigu, napiszemy transakcję odczytu i zapisu w Pythonie w Spanner Omni, aby zapewnić bezwzględną, odporną na podwójne rezerwacje serializowalność.

Nie musisz nic robić, po prostu przyjrzyj się kodowi:

from google.cloud import spanner

def create_booking_transaction(transaction, booking_data):
    # 1. Query for existing overlapping confirmed bookings on the same court
    query = """
        SELECT booking_id, player_name, start_hour, end_hour
        FROM Bookings
        WHERE court_id = @court_id
          AND booking_date = @booking_date
          AND status = 'CONFIRMED'
          AND NOT (end_hour <= @start_hour OR start_hour >= @end_hour)
    """
    params = {
        "court_id": booking_data["court_id"],
        "booking_date": booking_data["booking_date"],
        "start_hour": booking_data["start_hour"],
        "end_hour": booking_data["end_hour"],
    }
    param_types = {
        "court_id": spanner.param_types.INT64,
        "booking_date": spanner.param_types.DATE,
        "start_hour": spanner.param_types.INT64,
        "end_hour": spanner.param_types.INT64,
    }
    
    results = list(transaction.execute_sql(query, params=params, param_types=param_types))
    
    if results:
        conflict = results[0]
        raise ValueError(
            f"Court conflict: Already booked by {conflict[1]} ({conflict[2]}:00-{conflict[3]}:00)"
        )
    
    # 2. Insert new confirmed booking with Spanner commit timestamp
    insert_stmt = """
        INSERT INTO Bookings (
            booking_id, court_id, player_id, booking_date, start_hour, end_hour,
            player_name, court_name, status, notes, created_at
        ) VALUES (
            @booking_id, @court_id, @player_id, @booking_date, @start_hour, @end_hour,
            @player_name, @court_name, 'CONFIRMED', @notes, PENDING_COMMIT_TIMESTAMP()
        )
    """
    # execute_update statement writing to Spanner Omni...
    transaction.execute_update(insert_stmt, params=booking_data, param_types=booking_types)

5. Fizyka 60 kl./s w czasie rzeczywistym i regulowany silnik bota DUPR

Interfejs naszej aplikacji wykorzystuje symulację 2.5D HTML5 Canvas renderowaną z szybkością 60 klatek na sekundę (FPS). Renderowanie odbywa się całkowicie niezależnie od warstw bazy danych, co zapewnia płynne działanie, a dane telemetryczne są przesyłane dopiero po zakończeniu rajdów.

Symulator jest wyposażony w regulowany silnik AI OmniBot, który wykorzystuje matrycę trudności opartą na rzeczywistych poziomach DUPR (Dynamic Universal Pickleball Rating).

1640056a824b47e6.png

Nie musisz nic robić, po prostu zapoznaj się ze szczegółami.

6. Przetwarzanie grafu danych telemetrycznych ujęć ISO GQL

Po zakończeniu każdego rajdu frontend wysyła połączony wykres trajektorii do naszego punktu końcowego backendu /api/rally-end. Każde ujęcie jest rejestrowane jako węzeł relacyjny, a sekwencje są połączone dynamicznie za pomocą krawędzi przejścia.

Nie musisz nic robić, po prostu przyjrzyj się kodowi:

@app.route("/api/execute-shot", methods=["POST"])
def execute_shot():
    data = request.json or {}
    shot_id = f"shot_{int(time.time()*1000)}"
    
    # 1. Insert the Shot Node
    insert_shot_sql = """
        INSERT INTO Shots (shot_id, player_id, shot_type, speed_mph, spin_type, outcome, from_zone_id, to_zone_id)
        VALUES (@shot_id, @player_id, @shot_type, @speed, @spin, @outcome, @from_zone, @to_zone)
    """
    
    # 2. If part of a continuous rally, link with a ShotTransition Edge
    if data.get("prev_shot_id"):
        transition_id = f"trans_{int(time.time()*1000)}"
        insert_trans_sql = """
            INSERT INTO ShotTransitions (transition_id, from_shot_id, to_shot_id, rally_depth, pressure_level)
            VALUES (@trans_id, @from_shot, @to_shot, @depth, @pressure)
        """
        # Executed within a single, unified Spanner Omni transaction...

Dzięki wykorzystaniu natywnego silnika zapytań grafowych ISO GQL w Spannerze trener lub program analityczny może natychmiast wysyłać zapytania o złożone, wieloetapowe wzorce strukturalne bez konieczności wykonywania rozbudowanych operacji łączenia baz danych:

-- Find highest-frequency transition sequences leading to player unforced errors
GRAPH PickleballTelemetry
MATCH (s1:Shots)-[t:ShotTransitions]->(s2:Shots)
WHERE s1.shot_type = 'Speedup' AND s2.outcome = 'FAULT_NET'
RETURN s1.speed_mph, s2.from_zone_id, COUNT(*) AS occurrences
ORDER BY occurrences DESC
LIMIT 5;

7. Gemini AI Tactical Coaching & Grounded RAG Chat

Teraz aktywujemy mózg naszej aplikacji: Google Gemini 3.8 Flash. Nasz backend w Pythonie używa modelu do udostępniania 3 funkcji AI w czasie rzeczywistym, które są oparte bezpośrednio na usłudze Spanner Omni.

System używa modelu Google Gemini 3.8 Flash do niektórych procesów AI w czasie rzeczywistym:

Analiza wymian w czasie rzeczywistym i sędzia taktyczny:

Po każdym punkcie Gemini analizuje sekwencję uderzeń, poziom trudności bota i pozycję na korcie.

Spanner Multi-Table Grounded AI Concierge i analityk dopasowań:

Użytkownicy mogą sprawdzać harmonogramy kortów i ceny obiektów oraz pytać w języku naturalnym: „Jak mi poszło w porównaniu z OmniBotem?”. Backend dynamicznie wysyła zapytania do Spanner Omni w 4 domenach (Courts, Bookings, GameSessions i Shots), aby uziemić Gemini bez halucynacji.

Real-Time Performance Analytics API (/api/performance):

Agreguje bieżące współczynniki wygranych i przegranych, średnią głębokość wymian i ostatnie sesje gry bezpośrednio z tabeli GameSessions w Spanner Omni na potrzeby panelu gracza.

Nie musisz nic robić, po prostu zapoznaj się ze szczegółami.

8. Konfigurowanie, uruchamianie i testowanie

Uruchommy całą aplikację na komputerze lokalnym, aby sprawdzić system od początku do końca.

  1. Otwórz terminal hosta i sklonuj repozytorium projektu:
git clone https://github.com/AbiramiSukumaran/pickleball

cd pickleball
  1. Aby uzyskać klucz interfejsu Gemini API, wykonaj czynności opisane w tej dokumentacji:

https://medium.com/google-cloud/get-api-key-from-google-ai-studio-b2d2059a171d

  1. Otwórz plik .env w edytorze i skonfiguruj szczegóły środowiska:
SPANNER_EMULATOR_HOST="localhost:9010"
SPANNER_PROJECT="PROJECT_ID"
SPANNER_INSTANCE="omni-instance"
SPANNER_DATABASE="pickleball-db"
GEMINI_API_KEY="REPLACE_WITH_YOUR_ACTUAL_GEMINI_KEY"
  1. Zainstaluj zależności Pythona i uruchom aplikację:
pip install -r requirements.txt

python app.py
  1. Otwórz przeglądarkę i wejdź na stronę http://localhost:5000, aby zacząć grać. Możesz zarezerwować kort na karcie rezerwacji, zagrać przeciwko regulowanemu DUPR OmniBotowi i skonsultować się z analitykiem meczowym Gemini w czasie rzeczywistym na korcie.

Otwórz przeglądarkę lub kliknij link do aplikacji działającej na lokalnym hoście.

a2d18f8288607c66.png

9. Gratulacje!

Po ukończeniu tego ćwiczenia udało Ci się zaobserwować zmianę paradygmatu w inżynierii baz danych. Przez lata tworzenie responsywnych agentów działających w czasie rzeczywistym na brzegu sieci (na halach fabrycznych, w strefach wojskowych czy w sieciach sportowych na stadionach) wymagało bolesnych kompromisów projektowych: łączenia niestabilnych potoków ETL między odizolowanymi systemami transakcyjnymi, oddzielnymi węzłami grafów i indeksami wyszukiwania wektorowego.

Spanner Omni eliminuje ten kompromis. Łączy spójność rozproszoną na skalę globalną, zapytania grafowe ISO GQL i wydajność relacyjną w jednym lokalnym kontenerze do pobrania. Niezależnie od domeny Spanner Omni gwarantuje, że agenty brzegowe mają niezawodne dane podstawowe.