Data Agents und GraphRAG auf dem Gerät mit Spanner Omni

1. Übersicht

In diesem Codelab werden die Grundlagen von Spanner Omni behandelt, warum es die ultimative Datengrundlage für autonome KI-Agenten ist, und eine grobe End-to-End-Implementierung eines am Edge bereitgestellten, agentischen Pickleball-Taktiktrainers und Matchsimulators, der von Spanner Omni (Relational + Graph GQL), Python Flask und Gemini 3.8 Flash unterstützt wird.

Problem

Moderne interaktive Sportplattformen und Echtzeit-Gaming-Umgebungen erfordern die gleichzeitige Verarbeitung von drei unterschiedlichen Arbeitslasten:

Betriebstransaktionen mit hoher Gleichzeitigkeit:

Platzreservierungen, Spielermitgliedschaften, Abrechnung und Turnierpläne, die eine strikte ACID-Serialisierbarkeit erfordern, um Konflikte durch Doppelbuchungen zu vermeiden.

Connected Graph Telemetry:

Kontinuierliche Ballwechselsequenzen, Markov-Ketten für Übergänge und Diagramme zur Zonenabdeckung, die eine Traversierung mit niedriger Latenz ohne sperrige ETL-Pipelines erfordern.

Edge-Reaktionsfähigkeit im Submillisekundenbereich:

Schnelle Physiksimulationen mit 60 fps, bei denen Roundtrips in einem Cloud-Netzwerk mit großer Reichweite (100–200 ms) zu inakzeptablem Jitter führen würden.

In der Vergangenheit haben Engineering-Teams dieses Problem gelöst, indem sie fragmentierte Architekturen zusammengefügt haben, die in etwa so aussahen: PostgreSQL für Transaktionsdaten, Neo4j für Shot-Diagramme, Redis für Edge-Caching und Hintergrundsynchronisierungspipelines. Dies führt zu Architekturabweichungen, hoher operativer Komplexität und Verzögerungen bei der Eventual Consistency.

How we do it!

Durch die Bereitstellung von Google Cloud Spanner Omni am Edge und die Verknüpfung mit Google Gemini 3.8 Flash haben wir das Spanner Omni Pickleball Game entwickelt – eine Sportspielplattform mit einer einzigen Engine, die relationale Geschäftslogik, native ISO-GQL-Schusstelemetrie und taktisches KI-Coaching in Echtzeit vereint.

Spanner Omni

Spanner Omni bringt die weltweit verteilte Datenbank-Engine von Google mit horizontaler Skalierbarkeit, ACID-Transaktionen und Multi-Modell-Funktionen (relational, Graph, Vektor und Analyse) in selbstverwaltete, lokale, Multi-Cloud- und Air-Gap-Umgebungen. Spanner Omni ist eine herunterladbare, selbstverwaltete Version von Google Cloud Spanner. Sie bietet die wichtigsten verteilten Datenbankfunktionen von Spanner – automatisches Sharding, Paxos-basierte Replikation und starke externe Konsistenz –, unabhängig davon, wo Sie sie ausführen:

acc871a470f63b29.png

Lerninhalte

In diesem Codelab wechseln Sie von herkömmlichen, isolierten Datenbankarchitekturen zu einer modernen, einheitlichen Edge-Datenbanklösung. Sie lernen Folgendes:

  1. Stellen Sie Spanner Omni lokal mit Docker bereit und orchestrieren Sie es, um leistungsstarke relationale Arbeitslasten mit einer einzelnen Instanz zu verwalten.
  2. Entwerfen Sie ein einheitliches relationales und ISO-GQL-Diagrammschema, das transaktionale Geschäftsmodelle zusammen mit hochdimensionaler Spieltelemetrie abbildet, ohne dass eine externe Datenbanksynchronisierung erforderlich ist.
  3. Implementieren Sie strikte ACID-Serialisierbarkeit in Python-Lese-/Schreibtransaktionen, um Race-Bedingungen programmatisch zu verhindern und Konflikte durch Doppelbuchungen zu vermeiden.
  4. Erstellen Sie eine native GraphRAG-Pipeline, die SQL-integrierte ISO-GQL-Graphabfragen nutzt, um Echtzeit- und kontextbezogene Match-Telemetriedaten direkt in Google Gemini einzuspeisen und so sofortiges taktisches Coaching zu ermöglichen.

Voraussetzungen

  • Ein standardmäßiger lokaler Computer oder eine Entwicklungs-VM mit Linux (Ubuntu 22.04 oder höher), macOS oder Windows.
  • Docker Desktop oder Docker Engine muss installiert sein und ausgeführt werden.
  • Python 3.10 oder höher ist installiert.
  • Einen Texteditor oder eine IDE (z. B. VS Code oder PyCharm).
  • Einen aktiven Google Gemini API-Schlüssel. Hinweis: Für die lokale Ausführung von Spanner Omni ist kein Google Cloud-Rechnungskonto erforderlich, da es im Rahmen einer selbstverwalteten Entwickler-Evaluierungslizenz der kostenlosen Stufe ausgeführt wird.

2. Spanner Omni-Bereitstellung und -Konfiguration

In diesem Schritt starten wir unsere lokale Edge-Datenbankinstanz. Spanner Omni wird in einem hochgradig optimierten Container ausgeführt und bietet Ihnen auf Ihrem lokalen Entwickler-Laptop relationale Funktionen auf globaler Ebene.

  1. Öffnen Sie Ihr Terminal und erstellen Sie ein nichtflüchtiges Docker-Volume, um Ihre Datenbankschemas und Spieltelemetrie zu speichern:
docker volume create spanner_omni_data
  1. Rufen Sie das offizielle Spanner Omni-Container-Image ab, das im Entwicklermodus für einen einzelnen Server konfiguriert ist, und starten Sie es:
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. Prüfen Sie, ob der Container erfolgreich gestartet wurde:
docker ps

🛠️ Schnelle Fehlerbehebung:Wenn der Start fehlschlägt, weil die Ports 9010 oder 9020 bereits von Hintergrundprozessen oder älteren Emulatorausführungen verwendet werden, führen Sie dieses Bereinigungsskript in Ihrem Terminal aus:

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. Relationales und ISO-GQL-Graph-DDL-Schemadesign

Wenn Spanner Omni aktiv ist, können wir unser Datenbankschema erstellen. Dieses Schema dient als zentrale Informationsquelle und vereint relationale Geschäftsentitäten (wie Courts, Players und Bookings) mit schnellen, verbundenen Graph-Telemetrieschemas (Shots, Zones und Transitions).

So erstellen Sie die Betriebs- und Telemetrietabellen:

  1. Verbindung zur Datenbank über die Spanner CLI oder Ihren Anwendungsclient herstellen
  2. Führen Sie die folgenden DDL-Anweisungen aus:

Sie müssen nichts weiter tun, da wir das Problem im Anwendungscode beheben:

-- ============================================================================
-- 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

💡 Warum Commit-Zeitstempel wichtig sind: Durch die Angabe von OPTIONS (allow_commit_timestamp=true) wird eine monotone, mikrosekundengenaue Reihenfolge über alle lokalen Knotenreplikate hinweg ermöglicht. Dies ist entscheidend für die Synchronisierung von schnellen Spielereignissen (z. B. Videoaufnahmen von Linienentscheidungen) mit den entsprechenden Telemetrieprotokollen der Schüsse in der Datenbank.

4. ACID-Nebenläufigkeitserkennung und Vermeidung von Doppelbuchungen

In aktiven Sportbuchungsportalen versuchen oft mehrere Nutzer gleichzeitig, dieselben Zeitfenster für einen Platz zu reservieren. Um Race-Bedingungen zu vermeiden, schreiben wir eine Python-Lese-/Schreibtransaktion in Spanner Omni, um eine absolute, vor Doppelbuchungen geschützte Serialisierbarkeit zu gewährleisten.

Sie müssen jetzt nichts weiter tun, sondern sich nur den Code ansehen:

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. Echtzeit-Physik mit 60 fps und anpassbare DUPR-Bot-Engine

Das Frontend unserer Anwendung verwendet eine 2,5D-HTML5-Canvas-Simulation, die mit 60 Bildern pro Sekunde (FPS) gerendert wird. Dieses Rendering läuft völlig entkoppelt von den Datenbankebenen, um eine reibungslose Leistung zu gewährleisten. Telemetriedaten werden erst nach Abschluss der Rallyes übertragen.

Der Simulator verfügt über eine anpassbare OmniBot-KI-Engine, die eine Schwierigkeitsmatrix basierend auf realen DUPR-Werten (Dynamic Universal Pickleball Rating) verwendet.

1640056a824b47e6.png

Sie müssen nichts weiter tun, sondern nur die Details beachten.

6. ISO-GQL-Shot-Telemetry-Graph-Verarbeitung

Am Ende jeder Rallye sendet das Frontend das verbundene Trajektoriendiagramm an den Backend-Endpunkt /api/rally-end. Jede Aufnahme wird als relationaler Knoten aufgezeichnet und Sequenzen werden dynamisch über Übergangskanten verknüpft.

Sie müssen nichts weiter tun, sondern sich nur den Code ansehen:

@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...

Mit der nativen ISO-GQL-Graphabfrage-Engine von Spanner können Trainer oder Analyseprogramme komplexe, tiefgreifende strukturelle Muster sofort abfragen, ohne umfangreiche Datenbank-Joins ausführen zu müssen:

-- 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. Google AI Tactical Coaching und fundierter RAG Chat

Jetzt aktivieren wir das Gehirn unserer Anwendung: Google Gemini 3.8 Flash. Unser Python-Backend nutzt das Modell, um drei Echtzeit-KI-Funktionen bereitzustellen, die direkt auf Spanner Omni basieren.

Das System verwendet Google Gemini 3.8 Flash für einige spezifische Echtzeit-KI-Workflows:

Echtzeit-Analyse von Ballwechseln und taktischer Schiedsrichter:

Nach jedem Punkt analysiert Gemini die Schlagkette, den Schwierigkeitsgrad des Bots und die Positionierung auf dem Spielfeld.

Spanner Multi-Table Grounded AI Concierge & Match Analyst:

Nutzer können in natürlicher Sprache nach Platzbelegungen und Preisen fragen und sich erkundigen, wie sie gegen OmniBot abgeschnitten haben. Das Backend fragt Spanner Omni dynamisch in vier Bereichen ab (Plätze, Buchungen, Spielsitzungen und Schüsse), um Gemini ohne Halluzinationen zu fundieren.

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

Aggregiert Live-Gewinn-Verlust-Verhältnisse, die durchschnittliche Rallye-Tiefe und die letzten Spielsitzungen direkt aus der GameSessions-Tabelle von Spanner Omni für das Spieler-Dashboard.

Sie müssen nichts weiter tun, sondern nur die Details beachten.

8. Konfigurieren, ausführen und testen

Starten Sie die vollständige Anwendung auf Ihrem lokalen Computer, um das System durchgängig zu überprüfen.

  1. Öffnen Sie das Hostterminal und klonen Sie das Projekt-Repository:
git clone https://github.com/AbiramiSukumaran/pickleball

cd pickleball
  1. So rufen Sie Ihren Gemini API-Schlüssel ab:

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

  1. Öffnen Sie die Datei .env in Ihrem Editor und konfigurieren Sie die Umgebungsdetails:
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. Installieren Sie die Python-Abhängigkeiten und führen Sie die Anwendung aus:
pip install -r requirements.txt

python app.py
  1. Öffne deinen Webbrowser und rufe http://localhost:5000 auf, um mit dem Spielen zu beginnen. Auf dem Tab „Reservierung“ kannst du Platzzeiten buchen, gegen den anpassbaren DUPR OmniBot spielen und am Spielfeldrand den Gemini Match Analyst in Echtzeit konsultieren.

Öffnen Sie den Webbrowser oder klicken Sie auf den Link zur Anwendung, die auf Ihrem Localhost ausgeführt wird.

a2d18f8288607c66.png

9. Glückwunsch!

Mit diesem Codelab haben Sie einen Paradigmenwechsel in der Datenbankentwicklung miterlebt. Jahrelang mussten beim Erstellen von reaktionsschnellen Echtzeit-Agents am Edge (in Fabriken, militärischen Zonen oder Sportnetzwerken am Spielfeldrand) schmerzhafte Designkompromisse eingegangen werden: Es mussten anfällige ETL-Pipelines zwischen isolierten Transaktionssystemen, separaten Graphknoten und Vektorsuchindizes zusammengefügt werden.

Spanner Omni macht diesen Kompromiss überflüssig. Es kombiniert globale verteilte Konsistenz, ISO-GQL-Graphabfragen und relationale Leistung in einem einzigen, herunterladbaren lokalen Container. Unabhängig von Ihrer Domain garantiert Spanner Omni, dass Ihre Edge-Agents eine unerschütterliche Grundwahrheit haben.