使用 Spanner Omni 在裝置端執行資料代理和 GraphRAG

1. 總覽

本程式碼實驗室將介紹 Spanner Omni 的基本概念、為何它是自主 AI 代理的終極資料基礎,以及如何大致端對端實作邊緣部署的代理式匹克球戰術教練和比賽模擬器,由 Spanner Omni (關聯式 + 圖形 GQL)、Python Flask 和 Gemini 3.8 Flash 提供技術支援。

問題

現代互動式體育平台和即時遊戲環境,需要同時處理三種不同的工作負載:

高並行作業交易:

球場預訂、球員會員資格、帳單和錦標賽賽程,需要嚴格的 ACID 可序列化,以消除重複預訂衝突。

Connected Graph 遙測:

連續來回擊球序列、轉換馬可夫鏈和區域涵蓋範圍圖,需要低延遲遍歷,不需龐大的 ETL 管道。

邊緣回應時間不到一毫秒:

快節奏的 60 FPS 物理模擬,其中廣域雲端網路往返 (100 至 200 毫秒) 會造成無法接受的抖動。

過去,工程團隊會將分散的架構拼湊在一起,藉此解決這個問題,例如:使用 PostgreSQL 處理交易資料、Neo4j 處理圖表、Redis 處理邊緣快取,以及背景同步處理管道。這會導致架構漂移、作業複雜度提高,以及最終一致性延遲。

我們的做法

我們在邊緣部署 Google Cloud Spanner Omni,並搭配 Google Gemini 3.8 Flash,建構出 Spanner Omni Pickleball Game。這個單一引擎運動遊戲平台整合了關聯式商業邏輯、原生 ISO GQL 擊球遙測,以及即時戰術 AI 教練。

Spanner Omni

Spanner Omni 將 Google 的全球分散式資料庫引擎,帶入自行管理、地端部署、多雲端和實體隔離環境,並提供水平擴充、ACID 交易和多模型功能 (關聯式、圖形、向量和數據分析)。Spanner Omni 是 Google Cloud Spanner 的可下載、自行管理版本。無論您選擇在何處執行,都能提供 Spanner 的核心分散式資料庫功能:自動資料分割、以 Paxos 為基礎的複製,以及強大的外部一致性:

acc871a470f63b29.png

課程內容

在本程式碼實驗室中,您將從傳統的孤立資料庫架構,轉換為現代化的統合邊緣資料庫解決方案。您將學習下列內容:

  1. 使用 Docker 在本機部署及自動調度管理 Spanner Omni,管理高效能的單一執行個體關聯式工作負載。
  2. 設計統一的關聯式和 ISO GQL 圖譜結構定義,對應交易商業模式和高維度遊戲遙測資料,不需同步處理外部資料庫。
  3. 在 Python 讀寫交易中實作嚴格的 ACID 可序列化,以程式輔助方式防止競爭狀況,並消除重複預訂衝突。
  4. 建構原生 GraphRAG 管道,運用整合 SQL 的 ISO GQL 圖譜查詢,將即時且符合情境的比對遙測資料直接饋送至 Google Gemini,以便即時提供戰術指導。

需求條件

  • 執行 Linux (Ubuntu 22.04 以上版本)、macOS 或 Windows 的標準本機電腦或開發 VM。
  • 已安裝並執行 Docker Desktop 或 Docker Engine。
  • 已安裝 Python 3.10 以上版本。
  • 文字編輯器或 IDE (例如 VS Code 或 PyCharm)。
  • 有效的 Google Gemini API 金鑰。(注意:Spanner Omni 是在免費方案、自行管理的開發人員評估授權下運作,因此您不需要 Google Cloud Billing 帳戶,即可在本機執行 Spanner Omni)。

2. Spanner Omni 部署和設定

在這個步驟中,我們會啟動本機邊緣資料庫執行個體。Spanner Omni 在高度最佳化的容器中執行,可在本機開發人員筆電上提供全球規模的關聯式功能。

  1. 開啟終端機,建立永久 Docker 磁碟區,用於儲存資料庫結構定義和遊戲遙測資料:
docker volume create spanner_omni_data
  1. 提取並啟動以單一伺服器開發人員模式設定的官方 Spanner Omni 容器映像檔:
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. 確認容器已成功啟動:
docker ps

🛠️ 快速疑難排解:如果啟動失敗,是因為通訊埠 9010 或 9020 已由背景程序或舊版模擬器執行作業使用,請在終端機中執行下列清除指令碼:

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. 關聯式和 ISO GQL 圖表 DDL 結構定義設計

啟用 Spanner Omni 後,即可建立資料庫結構定義。這個結構定義是單一事實來源,可將關聯式商業實體 (例如球場、球員和預訂) 與快速、連結的圖表遙測結構定義 (投籃、區域和轉換) 整合。

如要建立作業和遙測資料表,請按照下列步驟操作:

  1. 使用 Spanner CLI 或應用程式用戶端連線至資料庫
  2. 執行下列 DDL 陳述式

我們正在應用程式程式碼中解決這個問題,您目前不需要採取任何行動:

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

💡 為何提交時間戳記很重要:指定 OPTIONS (allow_commit_timestamp=true) 可在所有本機節點副本中,啟用單調遞增、精確到微秒的排序。這點對於同步處理高速遊戲事件 (例如界線判定影片片段) 和對應的資料庫擊球遙測記錄而言,至關重大。

4. ACID 並行控制和重複預訂防範機制

在熱門的運動場地預訂入口網站,多位使用者經常會爭相預訂同一個球場時段。為避免競爭狀況,我們會在 Spanner Omni 中編寫 Python 讀寫交易,確保絕對可序列化,防止重複預訂。

您現在不需要採取任何行動,只要觀察程式碼即可:

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. 即時 60 FPS 物理引擎和可調整的 DUPR 機器人引擎

應用程式的前端採用 2.5D HTML5 Canvas 模擬,以每秒 60 影格 (FPS) 的速度轉譯。這項轉譯作業與資料庫層完全分離,可維持流暢效能,只有在拉力賽結束時才會清除遙測資料。

模擬器配備可調整的 OmniBot AI 引擎,可根據實際 DUPR (Dynamic Universal Pickleball Rating) 等級,利用難度矩陣調整難度。

1640056a824b47e6.png

目前您無須採取任何行動,只要查看詳細資料即可。

6. ISO GQL Shot 遙測圖處理

每次來回擊球結束時,前端會將連結的軌跡圖發布至後端端點 /api/rally-end。每個鏡頭都會記錄為關聯節點,而序列則透過轉場邊緣動態連結。

您目前無須採取任何行動,只要查看程式碼即可:

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

教練或分析程式可運用 Spanner 原生的 ISO GQL 圖譜查詢引擎,立即查詢複雜、深層的結構模式,不必執行龐大的資料庫彙整作業:

-- 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 戰術指導和 Grounded RAG Chat

現在,我們要啟用應用程式的大腦:Google Gemini 3.8 Flash。Python 後端會使用模型,提供三項即時 AI 功能,並直接以 Spanner Omni 做為基準。

系統會使用 Google Gemini 3.8 Flash 執行一些獨特的即時 AI 工作流程:

即時來回擊球分析和戰術裁判:

每得一分,Gemini 就會分析擊球鏈、機器人難度和球場位置。

Spanner 多資料表建立基準 AI Concierge 和 Match Analyst:

使用者可以查詢球場時間表、設施費率,並以自然語言詢問「我對上 OmniBot 的表現如何?」。後端會動態查詢 Spanner Omni 的四個網域 (Courts、Bookings、GameSessions 和 Shots),為 Gemini 提供基準,確保不會產生幻覺。

即時成效數據分析 API (/api/performance):

直接從 Spanner Omni 的 GameSessions 資料表匯總即時勝敗比率、平均來回深度和近期遊戲工作階段,並顯示在玩家資訊主頁。

目前您無須採取任何行動,只要查看詳細資料即可。

8. 設定、執行及測試!

請在本機電腦上啟動完整應用程式,驗證系統端對端。

  1. 開啟主機終端機,並複製專案存放區:
git clone https://github.com/AbiramiSukumaran/pickleball

cd pickleball
  1. 請按照下列說明文件中的步驟,取得 Gemini API 金鑰:

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

  1. 在編輯器中開啟 .env 檔案,並設定環境詳細資料:
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. 安裝 Python 依附元件並執行應用程式:
pip install -r requirements.txt

python app.py
  1. 開啟網路瀏覽器並前往 http://localhost:5000,即可開始遊玩!你可以在「預訂」分頁預約球場、與可調整難度的 DUPR OmniBot 對戰,並在場邊諮詢即時 Gemini Match Analyst。

開啟網路瀏覽器,或點選連結,前往 localhost 執行的應用程式!

a2d18f8288607c66.png

9. 恭喜!

完成本程式碼實驗室後,您將見證資料庫工程的典範轉移。多年來,在邊緣 (工廠、軍事區或球場邊的體育網路) 建構即時回應的代理,往往需要做出痛苦的設計妥協:在孤立的交易系統、獨立的圖形節點和向量搜尋索引之間,拼湊出脆弱的 ETL 管道。

Spanner Omni 則打破了這項取捨。它將全球規模的分散式一致性、ISO GQL 圖形查詢和關聯式效能,整合至單一可下載的本機容器。無論您在哪個領域,Spanner Omni 都能確保邊緣代理擁有可靠的基準真相。