1. סקירה כללית
ב-Codelab הזה נסביר את העקרונות הבסיסיים של Spanner Omni, למה הוא משמש כבסיס הנתונים האולטימטיבי לסוכני AI אוטונומיים, ואיך מטמיעים באופן גס מקצה לקצה מאמן טקטי וסימולטור משחקים של פיקלבול, שפועלים בקצה הרשת ומופעלים על ידי Spanner Omni (יחסי + Graph GQL), Python Flask ו-Gemini 3.8 Flash.
בעיה
פלטפורמות ספורט אינטראקטיביות מודרניות וסביבות גיימינג בזמן אמת דורשות טיפול בשלושה עומסי עבודה שונים בו-זמנית:
טרנזקציות תפעוליות עם רמת בו-זמניות גבוהה:
הזמנת מגרשים, מינויים של שחקנים, חיובים ולוחות זמנים של טורנירים שדורשים יכולת סריאליזציה קפדנית של ACID כדי למנוע התנגשויות של הזמנות כפולות.
טלמטריה של גרף מחובר:
רצפים רציפים של צילומי ראלי, שרשראות מרקוב של מעברים ותרשימי כיסוי אזוריים שדורשים מעבר עם זמן אחזור קצר בלי צינורות עיבוד נתונים גדולים של 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 ויכולות מרובות מודלים (יחסי, גרף, וקטור וניתוח נתונים) – לסביבות בניהול עצמי, מקומיות, מרובות עננים ועם air gap. Spanner Omni היא גרסה של Google Cloud Spanner שניתנת להורדה ומנוהלת באופן עצמאי. הוא מספק את היכולות העיקריות של מסד הנתונים המבוזר של Spanner – חלוקה אוטומטית ל-shards, שכפול מבוסס-Paxos ועקביות חזקה עם העולם האמיתי – בכל מקום שבו תבחרו להריץ אותו:

מה תלמדו
ב-Codelab הזה תעברו מארכיטקטורות מסורתיות של מסדי נתונים מבודדים לפתרון מודרני ומאוחד של מסדי נתונים בקצה הרשת. בסדנה תלמדו:
- פריסה ותזמור של Spanner Omni באופן מקומי באמצעות Docker, כדי לנהל עומסי עבודה יחסיים עם ביצועים גבוהים במכונה אחת.
- תכננו סכימת גרף מאוחדת של ISO GQL ונתונים רלציוניים, שממפה מודלים עסקיים של טרנזקציות לצד טלמטריית משחקים בממדים גבוהים, בלי צורך בסנכרון של מסד נתונים חיצוני.
- הטמעת Strict ACID Serializability בתוך עסקאות קריאה וכתיבה של Python כדי למנוע באופן פרוגרמטי מרוץ תהליכים ולבטל התנגשויות של הזמנה כפולה.
- בונים צינור עיבוד נתונים מקורי של GraphRAG שמשתמש בשאילתות תרשים של ISO GQL שמשולבות ב-SQL כדי להזין נתוני טלמטריה של התאמה מבוססת-הקשר בזמן אמת ישירות ל-Google Gemini, לצורך אימון טקטי מיידי.
דרישות
- מכונה מקומית רגילה או מכונה וירטואלית לפיתוח שמריצה את Linux (גרסה Ubuntu 22.04 ואילך), macOS או Windows.
- Docker Desktop או Docker Engine מותקנים ופועלים.
- Python 3.10 ומעלה מותקן.
- כלי לעריכת טקסט או סביבת פיתוח משולבת (IDE) (כמו VS Code או PyCharm).
- מפתח Gemini API פעיל של Google. (הערה: לא צריך חשבון לחיוב ב-Google Cloud כדי להריץ את Spanner Omni באופן מקומי, כי הוא פועל במסגרת רישיון הערכה למפתחים בניהול עצמי בתוכנית בחינם).
2. פריסה והגדרה של Spanner Omni
בשלב הזה נפעיל את המכונה של מסד הנתונים המקומי בקצה הרשת. Spanner Omni פועל בתוך קונטיינר שעבר אופטימיזציה, ומאפשר לכם להשתמש ביכולות של מסד נתונים רלציוני בקנה מידה עולמי במחשב הנייד המקומי של המפתח.
- פותחים את הטרמינל ויוצרים נפח Docker מתמיד כדי לאחסן את הסכימות של מסד הנתונים ואת נתוני הטלמטריה של המשחק:
docker volume create spanner_omni_data
- שולפים ומפעילים את קובץ האימג' הרשמי של קונטיינר 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
- מוודאים שהמאגר הופעל בהצלחה:
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. עיצוב סכימת DDL של גרף GQL יחסי ו-ISO
כשה-Spanner Omni פעיל, אנחנו יכולים ליצור את סכימת מסד הנתונים. הסכימה הזו משמשת כמקור האמת היחיד (SSOT) שלנו, ומאחדת ישויות עסקיות יחסיות (כמו מגרשים, שחקנים והזמנות) עם סכימות טלמטריה מהירות ומקושרות של תרשימים (זריקות, אזורים ומעברים).
כדי ליצור את הטבלאות של נתוני התפעול והטלמטריה:
- מתחברים למסד הנתונים באמצעות Spanner CLI או לקוח האפליקציה
- מריצים את הצהרות ה-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 ומניעת הזמנה כפולה
בפורטלים פעילים להזמנת מגרשי ספורט, משתמשים רבים מתחרים לעיתים קרובות על הזמנת אותן משבצות זמן. כדי למנוע מרוץ תהליכים, נכתוב טרנזקציית קריאה-כתיבה ב-Python ב-Spanner Omni כדי להבטיח סריאליזציה מוחלטת שמונעת הזמנה כפולה.
לא נדרשת פעולה מצידכם כרגע, רק תסתכלו על הקוד:
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 (דירוג פיקלבול אוניברסלי דינמי) בעולם האמיתי.

לא נדרשת פעולה מצידך כרגע, רק צריך לעיין בפרטים.
6. עיבוד גרף טלמטריה של צילום ISO GQL
בסיום כל ראלי, החזית מפרסמת את גרף המסלול המחובר לנקודת הקצה של הקצה העורפי שלנו /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. אימון טקטי וצ'אט RAG מבוסס-נתונים באמצעות Gemini AI
עכשיו נפעיל את המוח של האפליקציה: Google Gemini 3.8 Flash. ה-Python בקצה העורפי משתמש במודל כדי לספק שלוש יכולות AI בזמן אמת, שמבוססות ישירות על Spanner Omni.
המערכת משתמשת ב-Google Gemini 3.8 Flash לתהליכי עבודה ספציפיים של AI בזמן אמת:
ניתוח מהלכים בזמן אמת ושיפוט טקטי:
אחרי כל נקודה, Gemini מנתח את רצף החבטות, את רמת הקושי של הבוט ואת המיקום במגרש.
Spanner Multi-Table Grounded AI Concierge & Match Analyst:
המשתמשים יכולים לשאול שאלות בשפה טבעית, כמו "מה לוח הזמנים של המגרשים?", "כמה עולה להשתמש במתקנים?" ו"איך שיחקתי נגד OmniBot?". ה-backend שולח שאילתות באופן דינמי ל-Spanner Omni בארבעה דומיינים (מגרשים, הזמנות, סשנים של משחקים וזריקות) כדי לעגן את Gemini ולמנוע הזיות.
Real-Time Performance Analytics API (/api/performance):
הוא אוסף נתונים בזמן אמת על יחסי ניצחונות-הפסדים, עומק ממוצע של ראלי וסשנים אחרונים של משחקים ישירות מהטבלה GameSessions ב-Spanner Omni, ומציג אותם במרכז הבקרה של השחקן.
לא נדרשת פעולה מצידך כרגע, רק צריך לעיין בפרטים.
8. מגדירים, מריצים ובודקים.
בואו נפעיל את האפליקציה המלאה במחשב המקומי כדי לאמת את המערכת מקצה לקצה.
- פותחים את הטרמינל של המארח ומשכפלים את מאגר הפרויקט:
git clone https://github.com/AbiramiSukumaran/pickleball
cd pickleball
- כדי לקבל מפתח Gemini API, פועלים לפי השלבים שמפורטים במסמכי התיעוד הבאים:
https://medium.com/google-cloud/get-api-key-from-google-ai-studio-b2d2059a171d
- פותחים את הקובץ
.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"
- מתקינים את יחסי התלות של Python ומריצים את האפליקציה:
pip install -r requirements.txt
python app.py
- כדי להתחיל לשחק, פותחים את דפדפן האינטרנט ועוברים לכתובת
http://localhost:5000. אתם יכולים להזמין מגרש בכרטיסיית ההזמנות, לשחק נגד DUPR OmniBot שניתן להתאמה אישית, ולהתייעץ עם Gemini Match Analyst בזמן אמת במגרש.
פותחים את דפדפן האינטרנט או לוחצים על הקישור לאפליקציה שפועלת במארח המקומי.

9. מעולה!
ב-Codelab הזה ראיתם שינוי פרדיגמה בהנדסת מסדי נתונים. במשך שנים, כדי לבנות סוכנים רספונסיביים בזמן אמת בקצה (ברצפות ייצור, באזורים צבאיים או ברשתות ספורט במגרשים), היה צריך להתפשר על העיצוב: לחבר צינורות עיבוד נתונים (ETL) שבירים בין מערכות טרנזקציות מבודדות, צומתי גרפים נפרדים ואינדקסים של חיפוש וקטורי.
Spanner Omni מבטל את הצורך בפשרות האלה. הוא משלב עקביות מבוזרת בקנה מידה גלובלי, שאילתות גרפים ב-ISO GQL וביצועים של מסד נתונים יחסי בקונטיינר מקומי אחד שניתן להורדה. לא משנה מה התחום שלכם, Spanner Omni מבטיח שלסוכני הקצה שלכם תהיה אמת קרקע (ground truth) יציבה.