Data Agents و GraphRAG درون‌دستگاهی با Spanner Omni

۱. نمای کلی

این codelab اصول Spanner Omni را پوشش می‌دهد، اینکه چرا به‌عنوان پایه نهایی داده برای عامل‌های هوش مصنوعی خودکار عمل می‌کند، و یک پیاده‌سازی تقریبی سرتاسری از یک مربی تاکتیکی Pickleball و شبیه‌ساز مسابقه که در لبه مستقر شده و توسط Spanner Omni (رابطه‌ای + Graph GQL)، Python Flask، و Gemini 3.8 Flash پشتیبانی می‌شود.

مشکل

پلاتفرم‌های ورزشی تعاملی مدرن و محیط‌های بازی هم‌زمان نیازمند مدیریت هم‌زمان سه حجم کاری متفاوت هستند:

تراکنش‌های عملیاتی با همزمانی بالا:

رزرو زمین، عضویت بازیکنان، صورت‌حساب، و زمان‌بندی تورنمنت‌ها که برای حذف تداخل‌های رزرو تکراری به سریال‌سازی دقیق ACID نیاز دارند.

دور سنجی نمودار متصل:

دنباله‌های پیوسته ضربه رالی، زنجیره‌های مارکوف انتقال، و نمودارهای پوشش منطقه که به پیمایش با تأخیر کم بدون خطوط لوله ETL حجیم نیاز دارند.

پاسخ‌گویی Edge در کمتر از میلی‌ثانیه:

شبیه‌سازی‌های فیزیک سریع ۶۰ فریم در ثانیه که در آن رفت‌وبرگشت‌های شبکه ابری گسترده (۱۰۰ تا ۲۰۰ میلی‌ثانیه) لرزش غیرقابل‌قبولی ایجاد می‌کند.

در گذشته، تیم‌های مهندسی این مشکل را با اتصال معماری‌های تکه‌تکه حل می‌کردند که چیزی شبیه به این بود: PostgreSQL برای داده‌های تراکنشی، Neo4j برای نمودارهای شات، Redis برای ذخیره‌سازی موقت لبه، و خطوط لوله همگام‌سازی پس‌زمینه. این امر باعث انحراف معماری، پیچیدگی عملیاتی بالا، و درنهایت تأخیر در سازگاری می‌شود.

چگونه این کار را انجام می‌دهیم!

با استقرار Google Cloud Spanner Omni در لبه و جفت کردن آن با Google Gemini 3.8 Flash، بازی Spanner Omni Pickleball را ساختیم — یک پلاتفرم بازی ورزشی تک‌موتوره که منطق کسب‌وکار رابطه‌ای، تله‌متری شات ISO GQL بومی، و مربیگری هوش مصنوعی تاکتیکی هم‌زمان را متحد می‌کند.

Spanner Omni

Spanner Omni موتور پایگاه داده توزیع‌شده در مقیاس سیاره‌ای Google را (همراه با مقیاس‌پذیری افقی، تراکنش‌های ACID، و قابلیت‌های چندمدلی (رابطه‌ای، گراف، بردار، و تجزیه‌وتحلیل)) به محیط‌های خودمدیریتی، درون‌محیطی، چندابری، و ایزوله می‌آورد. ‫Spanner Omni نسخه بارگیری‌شدنی و خودمدیریتی Google Cloud Spanner است. این نسخه قابلیت‌های اصلی پایگاه داده توزیع‌شده Spanner (تکه‌تکه کردن خودکار، تکرار مبتنی بر Paxos، و سازگاری خارجی قوی) را در هر جایی که برای اجرا انتخاب کنید ارائه می‌دهد:

acc871a470f63b29.png

آنچه خواهید آموخت

در این codelab، از معماری‌های پایگاه داده سنتی سیلوشده به یک راه‌حل پایگاه داده لبه‌ای مدرن و یکپارچه منتقل خواهید شد. با این موارد آشنا خواهید شد:

  1. ‫Spanner Omni را به‌صورت محلی بااستفاده از Docker پیاده‌سازی و سازمان‌دهی کنید تا حجم‌های کاری رابطه‌ای تک‌نمونه‌ای با عملکرد بالا را مدیریت کنید.
  2. یک طرح گراف GQL ISO و رابطه‌ای یکپارچه طراحی کنید که مدل‌های کسب‌وکار تراکنشی را در کنار تله‌متری بازی با ابعاد بالا بدون نیاز به همگام‌سازی پایگاه داده خارجی نگاشت کند.
  3. قابلیت سریال‌سازی ACID سخت‌گیرانه را در تراکنش‌های خواندن-نوشتن Python پیاده‌سازی کنید تا به‌صورت برنامه‌نویسی‌شده از شرایط رقابتی جلوگیری کنید و تداخل‌های رزرو تکراری را حذف کنید.
  4. یک خط لوله GraphRAG بومی بسازید که از پُرسمان‌های گراف ISO GQL ادغام‌شده با SQL برای تغذیه مستقیم تله‌متری مسابقه هم‌زمان و زمینه‌سازی‌شده به Google Gemini برای مربیگری تاکتیکی فوری استفاده می‌کند.

الزامات

  • یک ماشین محلی استاندارد یا VM توسعه که Linux (Ubuntu 22.04+)‎،‏ macOS، یا Windows را اجرا می‌کند.
  • Docker Desktop یا Docker Engine نصب شده و درحال اجرا باشد.
  • Python نسخه ۳.۱۰ یا بالاتر نصب شده باشد.
  • ویرایشگر نوشتار یا IDE (مانند VS Code یا PyCharm).
  • کلید Gemini API فعال Google. (توجه: برای اجرای Spanner Omni به‌صورت محلی، به حساب صورت‌حساب Google Cloud نیازی نیست زیرا تحت پروانه ارزیابی توسعه‌دهنده خودمدیریت‌شده و ردیف رایگان اجرا می‌شود).

۲. پیکربندی و استقرار Spanner Omni

در این مرحله، نمونه پایگاه داده محلی Edge را راه‌اندازی خواهیم کرد. Spanner Omni در یک محفظه بسیار بهینه‌سازی‌شده اجرا می‌شود و قابلیت‌های رابطه‌ای در مقیاس سیاره‌ای را در رایانه کیفی توسعه‌دهنده محلی شما ارائه می‌دهد.

  1. پایانه را باز کنید و یک Docker volume پایدار برای ذخیره طرح‌واره‌های پایگاه داده و دورسنجی بازی ایجاد کنید:
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

۳. طراحی طرح‌واره DDL گراف GQL رابطه‌ای و ISO

با فعال بودن 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) امکان ترتیب‌بندی یکنواخت و دقیق در حد میکروثانیه را در همه نسخه‌های گره محلی فراهم می‌کند. این امر برای همگام‌سازی رویدادهای بازی با سرعت بالا (مانند فیلم ویدئویی تماس خطی) با گزارش‌های دورسنجی شات پایگاه داده مربوطه بسیار مهم است.

۴. کنترل همزمانی 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)

۵. فیزیک ۶۰ فریم در ثانیه هم‌زمان و موتور روبات DUPR قابل‌تنظیم

نمای جلویی برنامه ما از شبیه‌سازی 2.5D HTML5 Canvas با نرخ ۶۰ فریم در ثانیه (FPS) استفاده می‌کند. این پردازش کاملاً جدا از لایه‌های پایگاه داده اجرا می‌شود تا عملکردی بسیار روان حفظ شود و دورسنجی فقط پس‌از پایان رالی‌ها پاک‌سازی می‌شود.

این شبیه‌ساز دارای یک موتور هوش مصنوعی OmniBot قابل‌تنظیم است که از ماتریس دشواری براساس سطوح واقعی DUPR (رتبه‌بندی جهانی پیکل‌بال پویا) استفاده می‌کند.

1640056a824b47e6.png

اکنون لازم نیست اقدامی انجام دهید، فقط جزئیات را مشاهده کنید.

‫۶. ISO GQL Shot Telemetry Graph Processing

در پایان هر رالی، فرانت‌اند نمودار مسیر متصل را به نقطه پایانی پسین ما /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...

با بهره‌گیری از موتور پُرسمان نمودار ISO GQL بومی Spanner، مربی یا برنامه تجزیه‌وتحلیل می‌تواند الگوهای ساختاری پیچیده و عمیق را فوراً و بدون اجرای پیوندهای حجیم پایگاه داده پُرسمان کند:

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

‫۷. Gemini AI Tactical Coaching & Grounded RAG Chat

اکنون، مغز برنامه‌مان را فعال می‌کنیم: Google Gemini 3.8 Flash. بخش پشتی Python ما از این مدل برای ارائه سه قابلیت هوش مصنوعی هم‌زمان استفاده می‌کند که مستقیماً توسط Spanner Omni پایه‌گذاری شده‌اند.

این سیستم از Google Gemini 3.8 Flash برای برخی‌از گردش‌های کاری متمایز هوش مصنوعی هم‌زمان استفاده می‌کند:

تفکیک رالی هم‌زمان و داور تاکتیکی:

پس‌از هر امتیاز، Gemini زنجیره ضربات، سطح دشواری ربات، و موقعیت‌یابی در زمین را تحلیل می‌کند.

دستیار هوش مصنوعی مبتنی بر چند جدول Spanner و تحلیل‌گر مسابقه:

کاربران می‌توانند زمان‌بندی زمین‌ها و نرخ‌های تسهیلات را جستجو کنند و به زبان طبیعی بپرسند «How did I do against OmniBot?‎» (درمقابل OmniBot چطور عمل کردم؟). بخش پشتی به‌طور پویا Spanner Omni را در چهار دامنه (Courts،‏ Bookings،‏ GameSessions، و Shots) پرسمان می‌کند تا Gemini را بدون هیچ‌گونه توهمی زمینه‌سازی کند.

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

نسبت برد-باخت زنده، میانگین عمق رالی، و جلسه‌های بازی اخیر را مستقیماً از جدول GameSessions در Spanner Omni برای داشبورد بازیکن جمع‌آوری می‌کند.

اکنون لازم نیست اقدامی انجام دهید، فقط جزئیات را مشاهده کنید.

‫۸. پیکربندی، اجرا، و آزمایش!

بیایید برنامه کامل را در دستگاه محلی‌تان راه‌اندازی کنیم تا سیستم را به‌طور کامل درستی‌سنجی کنیم.

  1. پایانه میزبان را باز کنید و مخزن پروژه را همسانه‌سازی کنید:
git clone https://github.com/AbiramiSukumaran/pickleball

cd pickleball
  1. با دنبال کردن مراحل موجود در مستندات زیر، «کلید میانای برنامه‌سازی کاربردی Gemini» خود را دریافت کنید:

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 هم‌زمان خود در کنار زمین مشورت کنید.

مرورگر وب را باز کنید یا روی پیوند برنامه درحال اجرا در میزبان محلی‌تان کلیک کنید!

a2d18f8288607c66.png

‫۹. تبریک!

با تکمیل این codelab، شاهد تغییر الگو در مهندسی پایگاه داده بوده‌اید. سال‌ها است که ساختن عامل‌های پاسخگو و هم‌زمان در لبه (در کف کارخانه‌ها، مناطق نظامی، یا شبکه‌های ورزشی کنار زمین) باعث سازش‌های دردناک در طراحی شده است: چسباندن خطوط لوله ETL شکننده بین سیستم‌های تراکنشی مجزا، گره‌های گراف جداگانه، و نمایه‌های جستجوی برداری.

Spanner Omni این موازنه را ازبین می‌برد. این ابزار سازگاری توزیع‌شده در مقیاس جهانی، پُرسمان گراف ISO GQL، و عملکرد رابطه‌ای را در یک محفظه محلی قابل‌بارگیری ترکیب می‌کند. صرف‌نظر از دامنه شما، Spanner Omni تضمین می‌کند که کارگزاران لبه شما واقعیت عینی تزلزل‌ناپذیری داشته باشند.