יצירת אפליקציית בנקאות אונליין באמצעות Spanner

1. סקירה כללית

Spanner הוא שירות מנוהל של מסד נתונים עם יכולת התאמה אופקית, שמופץ באופן גלובלי ומתאים לעומסי עבודה תפעוליים רלציוניים ולא רלציוניים. בנוסף ליכולות הליבה שלו, Spanner מציע תכונות מתקדמות ועוצמתיות שמאפשרות לפתח אפליקציות חכמות שמבוססות על נתונים.

ב-codelab הזה נשתמש בהבנה הבסיסית של Spanner כדי להעמיק בשימוש בשילובים המתקדמים שלו לשיפור יכולות עיבוד הנתונים והניתוח, על סמך אפליקציה לבנקאות באינטרנט.

נתמקד בשלוש תכונות מתקדמות עיקריות:

  • שילוב עם Vertex AI: איך משלבים את Spanner עם פלטפורמת ה-AI של Google Cloud, ‏ Vertex AI. תלמדו איך להפעיל מודלים של Vertex AI ישירות מתוך שאילתות Spanner SQL, כדי לבצע טרנספורמציות וחיזויים עוצמתיים בתוך מסד הנתונים. כך אפשר לסווג אוטומטית טרנזקציות באפליקציית הבנקאות שלנו לתרחישי שימוש כמו מעקב אחר תקציב וזיהוי אנומליות.
  • חיפוש טקסט מלא: כאן מוסבר איך מטמיעים את הפונקציונליות של חיפוש טקסט מלא ב-Spanner. תלמדו איך לבצע אינדוקס של נתוני טקסט ולכתוב שאילתות יעילות כדי לבצע חיפושים מבוססי מילות מפתח בנתונים התפעוליים שלכם. כך תוכלו לגלות נתונים חשובים, למשל למצוא ביעילות לקוחות לפי כתובת אימייל במערכת הבנקאית שלנו.
  • שאילתות מאוחדות ב-BigQuery: הסבר על האופן שבו אפשר להשתמש ביכולות של Spanner להרצת שאילתות מאוחדות כדי להריץ שאילתות ישירות על נתונים שנמצאים ב-BigQuery. כך תוכלו לשלב את הנתונים התפעוליים בזמן אמת של Spanner עם מערכי הנתונים האנליטיים של BigQuery כדי לקבל תובנות מקיפות ודוחות מפורטים בלי לשכפל נתונים או להשתמש בתהליכי ETL מורכבים. השילוב הזה מאפשר להפעיל תרחישי שימוש שונים באפליקציית הבנק שלנו, כמו קמפיינים שיווקיים ממוקדים, על ידי שילוב של נתוני לקוחות בזמן אמת עם מגמות היסטוריות רחבות יותר מ-BigQuery.

מה תלמדו

  • איך מגדירים מופע Spanner.
  • איך יוצרים מסד נתונים וטבלאות.
  • איך לטעון נתונים לטבלאות של מסד הנתונים ב-Spanner.
  • איך קוראים למודלים של Vertex AI מ-Spanner.
  • איך לשלוח שאילתות למסד הנתונים שלכם ב-Spanner באמצעות חיפוש משוער וחיפוש טקסט מלא.
  • איך מבצעים שאילתות מאוחדות ב-Spanner מ-BigQuery.
  • איך מוחקים את מכונת Spanner.

הדרישות

  • פרויקט ב-Google Cloud שמקושר לחשבון לחיוב.
  • דפדפן אינטרנט, כמו Chrome או Firefox.

2. הגדרה ודרישות

יצירת פרויקט

אם כבר יש לכם פרויקט ב-Google Cloud עם חיוב מופעל, לוחצים על התפריט הנפתח לבחירת פרויקט בפינה הימנית העליונה של המסוף:

פרויקט קיים

אם בחרתם פרויקט, דלגו אל הפעלת ממשקי ה-API הנדרשים.

אם עדיין אין לכם חשבון Google (Gmail או Google Apps), אתם צריכים ליצור חשבון. נכנסים אל Google Cloud Platform Console‏ (console.cloud.google.com) ויוצרים פרויקט חדש.

בתיבת הדו-שיח שמופיעה, לוחצים על הלחצן 'פרויקט חדש' כדי ליצור פרויקט חדש:

פרויקט חדש

אם עדיין אין לכם פרויקט, תופיע תיבת דו-שיח כמו זו שבהמשך כדי ליצור את הפרויקט הראשון:

תיבת הדו-שיח 'פרויקט'

בתיבת הדו-שיח הבאה ליצירת פרויקט, מזינים את הפרטים של הפרויקט החדש.

חשוב לזכור את מזהה הפרויקט, שהוא שם ייחודי בכל הפרויקטים ב-Google Cloud. בהמשך ה-codelab הזה נתייחס אליו כאל PROJECT_ID.

פרטי הפרויקט

לאחר מכן, אם עדיין לא עשיתם זאת, תצטרכו להפעיל את החיוב במסוף למפתחים כדי להשתמש במשאבי Google Cloud ולהפעיל את Spanner API,‏ Vertex AI API,‏ BigQuery API ו-BigQuery Connection API.

חיוב על פרויקטים

התמחור של Spanner מתועד כאן. עלויות אחרות שקשורות למשאבים אחרים מתועדות בדפי התמחור הספציפיים שלהם.

משתמשים חדשים ב-Google Cloud Platform זכאים לתקופת ניסיון בחינם בשווי 300$.

הגדרה של Google Cloud Shell

ב-codelab הזה נשתמש ב-Google Cloud Shell, סביבת שורת פקודה שפועלת בענן.

המכונה הווירטואלית הזו מבוססת על Debian, וטעונים בה כל הכלים הדרושים למפתחים. יש בה ספריית בית בנפח מתמיד של 5GB והיא פועלת ב-Google Cloud, מה שמשפר מאוד את הביצועים והאימות ברשת. כלומר, כל מה שצריך בשביל ה-codelab הזה הוא דפדפן.

כדי להפעיל את Cloud Shell ממסוף Cloud, פשוט לוחצים על 'הפעלת Cloud Shell' הסמל של Cloud Shell (הקצאת המשאבים והחיבור לסביבה אמורים להימשך רק כמה רגעים).

Cloud Shell

אחרי שמתחברים ל-Cloud Shell, אמור להופיע אימות שכבר בוצע, ושהפרויקט כבר הוגדר ל-PROJECT_ID.

gcloud auth list

הפלט אמור להיראות כך:

Credentialed Accounts

ACTIVE: *
ACCOUNT: <myaccount>@<mydomain>.com
gcloud config list project

הפלט אמור להיראות כך:

[core]
project = <PROJECT_ID>

אם הפרויקט לא מוגדר מסיבה כלשהי, מריצים את הפקודה הבאה:

gcloud config set project <PROJECT_ID>

מחפש את PROJECT_ID? בודקים באיזה מזהה השתמשתם בשלבי ההגדרה או מחפשים אותו בלוח הבקרה של Cloud Console:

מזהה פרויקט

ב-Cloud Shell מוגדרים גם כמה משתני סביבה כברירת מחדל, שיכולים להיות שימושיים כשמריצים פקודות בעתיד.

echo $GOOGLE_CLOUD_PROJECT

הפלט אמור להיראות כך:

<PROJECT_ID>

הפעלת ממשקי ה-API הנדרשים

מפעילים את ממשקי ה-API של Spanner,‏ Vertex AI ו-BigQuery בפרויקט:

gcloud services enable spanner.googleapis.com
gcloud services enable aiplatform.googleapis.com
gcloud services enable bigquery.googleapis.com
gcloud services enable bigqueryconnection.googleapis.com

סיכום

בשלב הזה, הגדרתם את הפרויקט אם עדיין לא היה לכם פרויקט, הפעלתם את Cloud Shell והפעלתם את ממשקי ה-API הנדרשים.

הבא בתור

בשלב הבא תגדירו את מופע Spanner.

3. הגדרת מכונת Spanner

יצירת מכונת Spanner

בשלב הזה מגדירים מופע Spanner בשביל ה-codelab. כדי לעשות זאת, פותחים את Cloud Shell ומריצים את הפקודה הבאה:

export SPANNER_INSTANCE=cloudspanner-onlinebanking
gcloud spanner instances create $SPANNER_INSTANCE \
  --config=regional-us-central1 \
  --description="Spanner Online Banking" \
  --nodes=1 \
  --edition=ENTERPRISE \
  --default-backup-schedule-type=NONE

הפלט אמור להיראות כך:

Creating instance...done.

סיכום

בשלב הזה יצרתם את מופע Spanner.

הבא בתור

בשלב הבא, תכינו את האפליקציה הראשונית ותיצרו את מסד הנתונים והסכימה.

4. יצירה של מסד נתונים וסכימה

הכנת הבקשה הראשונית

בשלב הזה תיצרו את מסד הנתונים ואת הסכימה באמצעות הקוד.

קודם כל, יוצרים אפליקציית Java בשם onlinebanking באמצעות Maven:

mvn -B archetype:generate \
  -DarchetypeGroupId=org.apache.maven.archetypes \
  -DgroupId=com.google.codelabs \
  -DartifactId=onlinebanking \
  -DjavaCompilerVersion=1.8 \
  -DjunitVersion=4.13.2 \
  -DarchetypeVersion=1.5

מבצעים Checkout ומעתיקים את קובצי הנתונים שנוסיף למסד הנתונים (כאן אפשר לראות את מאגר הקוד):

git clone https://github.com/GoogleCloudPlatform/cloud-spanner-samples.git
cp -r ./cloud-spanner-samples/banking/data ./onlinebanking

עוברים לתיקיית האפליקציה:

cd onlinebanking

פותחים את קובץ Maven pom.xml. כדי להשתמש ב-Maven BOM לניהול הגרסה של ספריות Google Cloud, מוסיפים את הקטע dependency management:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>libraries-bom</artifactId>
      <version>26.56.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

כך ייראו העורך והקובץ: Cloud Shell

מוודאים שהקטע dependencies כולל את הספריות שהאפליקציה תשתמש בהן:

<dependencies>
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-nop</artifactId>
    <version>2.0.9</version>
  </dependency>
  <dependency>
    <groupId>com.opencsv</groupId>
    <artifactId>opencsv</artifactId>
    <version>5.10</version>
  </dependency>
  <dependency>
    <groupId>com.google.cloud</groupId>
    <artifactId>google-cloud-spanner</artifactId>
  </dependency>
  <dependency>
    <groupId>com.google.cloud</groupId>
    <artifactId>google-cloud-bigquery</artifactId>
  </dependency>
  <dependency>
    <groupId>com.google.cloud</groupId>
    <artifactId>google-cloud-bigqueryconnection</artifactId>
  </dependency>
</dependencies>

לבסוף, מחליפים את תוספי ה-build כדי שהאפליקציה תאוגד לחבילה של JAR שאפשר להפעיל:

<build>
  <plugins>
    <plugin>
      <artifactId>maven-resources-plugin</artifactId>
      <version>3.3.1</version>
      <executions>
        <execution>
          <id>copy-resources</id>
          <phase>process-resources</phase>
          <goals>
            <goal>copy-resources</goal>
          </goals>
          <configuration>
            <outputDirectory>${project.build.directory}/${project.artifactId}-resources</outputDirectory>
            <resources>
              <resource>
                <directory>resources</directory>
                <filtering>true</filtering>
              </resource>
            </resources>
          </configuration>
        </execution>
      </executions>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-dependency-plugin</artifactId>
      <version>3.8.1</version>
      <executions>
        <execution>
          <id>copy-dependencies</id>
          <phase>prepare-package</phase>
          <goals>
            <goal>copy-dependencies</goal>
          </goals>
          <configuration>
            <outputDirectory>${project.build.directory}/${project.artifactId}-resources/lib</outputDirectory>
            <overWriteReleases>false</overWriteReleases>
            <overWriteSnapshots>false</overWriteSnapshots>
            <overWriteIfNewer>true</overWriteIfNewer>
          </configuration>
        </execution>
      </executions>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <version>3.4.2</version>
      <configuration>
        <finalName>${project.artifactId}</finalName>
        <outputDirectory>${project.build.directory}</outputDirectory>
        <archive>
          <index>false</index>
          <manifest>
            <mainClass>com.google.codelabs.App</mainClass>
            <addClasspath>true</addClasspath>
            <classpathPrefix>${project.artifactId}-resources/lib/</classpathPrefix>
          </manifest>
        </archive>
      </configuration>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-failsafe-plugin</artifactId>
      <version>3.2.5</version>
      <executions>
        <execution>
          <goals>
            <goal>integration-test</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>3.2.5</version>
      <configuration>
        <useSystemClassLoader>false</useSystemClassLoader>
      </configuration>
    </plugin>
  </plugins>
</build>

שומרים את השינויים שביצעתם בקובץ pom.xml על ידי בחירה באפשרות 'שמירה' בתפריט 'קובץ' של עורך Cloud Shell או על ידי הקשה על Ctrl+S.

אחרי שהתלות מוכנה, מוסיפים קוד לאפליקציה כדי ליצור סכימה, כמה אינדקסים (כולל חיפוש) ומודל AI שמחובר לנקודת קצה מרוחקת. במהלך ה-Codelab הזה, תשתמשו בארטיפקטים האלה ותוסיפו עוד שיטות לכיתה הזו.

פותחים את App.java בקטע onlinebanking/src/main/java/com/google/codelabs ומחליפים את התוכן בקוד הבא:

package com.google.codelabs;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;

import com.google.api.gax.longrunning.OperationFuture;
import com.google.cloud.bigquery.BigQuery;
import com.google.cloud.bigquery.BigQueryOptions;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseAdminClient;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.DatabaseId;
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.SpannerOptions;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;

public class App {

  // Create the Spanner database and schema
  public static void create(DatabaseAdminClient dbAdminClient, DatabaseId db,
      String location, String model) {
    System.out.println("Creating Spanner database...");
    List<String> statements = Arrays.asList(
      "CREATE TABLE Customers (\n"
          + "  CustomerId INT64 NOT NULL,\n"
          + "  FirstName STRING(256) NOT NULL,\n"
          + "  LastName STRING(256) NOT NULL,\n"
          + "  FullName STRING(512) AS (FirstName || ' ' || LastName) STORED,\n"
          + "  Email STRING(512) NOT NULL,\n"
          + "  EmailTokens TOKENLIST AS\n"
          + "    (TOKENIZE_SUBSTRING(Email, ngram_size_min=>2, ngram_size_max=>3,\n"
          + "      relative_search_types=>[\"all\"])) HIDDEN,\n"
          + "  Address STRING(MAX)\n"
          + ") PRIMARY KEY (CustomerId)",

      "CREATE INDEX CustomersByEmail\n"
          + "ON Customers(Email)",

      "CREATE SEARCH INDEX CustomersFuzzyEmail\n"
          + "ON Customers(EmailTokens)",

      "CREATE TABLE Accounts (\n"
          + "  AccountId INT64 NOT NULL,\n"
          + "  CustomerId INT64 NOT NULL,\n"
          + "  AccountType STRING(256) NOT NULL,\n"
          + "  Balance NUMERIC NOT NULL,\n"
          + "  OpenDate TIMESTAMP NOT NULL\n"
          + ") PRIMARY KEY (AccountId)",

      "CREATE INDEX AccountsByCustomer\n"
          + "ON Accounts (CustomerId)",

      "CREATE TABLE TransactionLedger (\n"
          + "  TransactionId INT64 NOT NULL,\n"
          + "  AccountId INT64 NOT NULL,\n"
          + "  TransactionType STRING(256) NOT NULL,\n"
          + "  Amount NUMERIC NOT NULL,\n"
          + "  Timestamp TIMESTAMP NOT NULL"
          + "  OPTIONS(allow_commit_timestamp=true),\n"
          + "  Category STRING(256),\n"
          + "  Description STRING(MAX),\n"
          + "  CategoryTokens TOKENLIST AS (TOKENIZE_FULLTEXT(Category)) HIDDEN,\n"
          + "  DescriptionTokens TOKENLIST AS (TOKENIZE_FULLTEXT(Description)) HIDDEN\n"
          + ") PRIMARY KEY (AccountId, TransactionId),\n"
          + "INTERLEAVE IN PARENT Accounts ON DELETE CASCADE",

      "CREATE INDEX TransactionLedgerByAccountType\n"
          + "ON TransactionLedger(AccountId, TransactionType)",

      "CREATE INDEX TransactionLedgerByCategory\n"
          + "ON TransactionLedger(AccountId, Category)",

      "CREATE SEARCH INDEX TransactionLedgerTextSearch\n"
          + "ON TransactionLedger(CategoryTokens, DescriptionTokens)",

      "CREATE MODEL TransactionCategoryModel\n"
          + "INPUT (prompt STRING(MAX))\n"
          + "OUTPUT (content STRING(MAX))\n"
          + "REMOTE OPTIONS (\n"
          + "  endpoint = '//aiplatform.googleapis.com/projects/" + db.getInstanceId().getProject()
              + "/locations/" + location + "/publishers/google/models/" + model + "',\n"
          + "  default_batch_size = 1\n"
          + ")");
    OperationFuture<Database, CreateDatabaseMetadata> op = dbAdminClient.createDatabase(
        db.getInstanceId().getInstance(),
        db.getDatabase(),
        statements);
    try {
      Database dbOperation = op.get();
      System.out.println("Created Spanner database [" + dbOperation.getId() + "]");
    } catch (ExecutionException e) {
      throw (SpannerException) e.getCause();
    } catch (InterruptedException e) {
      throw SpannerExceptionFactory.propagateInterrupt(e);
    }
  }

  static void printUsageAndExit() {
    System.out.println("Online Online Banking Application 1.0.0");
    System.out.println("Usage:");
    System.out.println("  java -jar target/onlinebanking.jar <command> [command_option(s)]");
    System.out.println("");
    System.out.println("Examples:");
    System.out.println("  java -jar target/onlinebanking.jar create");
    System.out.println("      - Create a sample Spanner database and schema in your "
        + "project.\n");
    System.exit(1);
  }

  public static void main(String[] args) {
    if (args.length < 1) {
      printUsageAndExit();
    }

    String instanceId = System.getProperty("SPANNER_INSTANCE", System.getenv("SPANNER_INSTANCE"));
    String databaseId = System.getProperty("SPANNER_DATABASE", System.getenv("SPANNER_DATABASE"));
    String location = System.getenv().getOrDefault("SPANNER_LOCATION", "us-central1");
    String model = System.getenv().getOrDefault("SPANNER_MODEL", "gemini-2.0-flash-lite");
    if (instanceId == null || databaseId == null) {
      System.err.println("Missing one or more required environment variables: SPANNER_INSTANCE or "
          + "SPANNER_DATABASE");
      System.exit(1);
    }

    BigQueryOptions bigqueryOptions = BigQueryOptions.newBuilder().build();
    BigQuery bigquery = bigqueryOptions.getService();

    SpannerOptions spannerOptions = SpannerOptions.newBuilder().build();
    try (Spanner spanner = spannerOptions.getService()) {
      String command = args[0];
      DatabaseId db = DatabaseId.of(spannerOptions.getProjectId(), instanceId, databaseId);
      DatabaseClient dbClient = spanner.getDatabaseClient(db);
      DatabaseAdminClient dbAdminClient = spanner.getDatabaseAdminClient();

      switch (command) {
        case "create":
          create(dbAdminClient, db, location, model);
          break;
        default:
          printUsageAndExit();
      }
    }
  }
}

שומרים את השינויים שבוצעו בקובץ App.java.

בודקים את הישויות השונות שהקוד יוצר ובוחרים את קובץ ה-JAR של האפליקציה:

mvn package

הפלט אמור להיראות כך:

[INFO] Building jar: /home/your_user/onlinebanking/target/onlinebanking.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

מריצים את האפליקציה כדי לראות את פרטי השימוש:

java -jar target/onlinebanking.jar

הפלט אמור להיראות כך:

Online Banking Application 1.0.0
Usage:
  java -jar target/onlinebanking.jar <command> [command_option(s)]

Examples:
  java -jar target/onlinebanking.jar create
      - Create a sample Spanner database and schema in your project.

יצירת מסד הנתונים והסכימה

מגדירים את משתני הסביבה הנדרשים של האפליקציה:

export SPANNER_INSTANCE=cloudspanner-onlinebanking
export SPANNER_DATABASE=onlinebanking

כדי ליצור את מסד הנתונים ואת הסכימה, מריצים את הפקודה create:

java -jar target/onlinebanking.jar create

הפלט אמור להיראות כך:

Creating Spanner database...
Created Spanner database [<DATABASE_RESOURCE_NAME>]

בדיקת הסכימה ב-Spanner

במסוף Spanner, עוברים למופע ולמסד הנתונים שנוצרו זה עתה.

אמורות להופיע כל 3 הטבלאות – Accounts, Customers ו-TransactionLedger.

הצגת סכימה

הפעולה הזו יוצרת את סכימת מסד הנתונים, כולל הטבלאות Accounts, Customers ו-TransactionLedger, יחד עם אינדקסים משניים לאחזור נתונים אופטימלי והפניה למודל Vertex AI.

תרשים קשרים בין ישויות

הטבלה TransactionLedger משולבת בטבלה Accounts כדי לשפר את ביצועי השאילתות של טרנזקציות ספציפיות לחשבון באמצעות שיפור הלוקאליות של הנתונים.

הטמענו אינדקסים משניים (CustomersByEmail,‏ CustomersFuzzyEmail,‏ AccountsByCustomer,‏ TransactionLedgerByAccountType,‏ TransactionLedgerByCategory,‏ TransactionLedgerTextSearch) כדי לבצע אופטימיזציה של דפוסי גישה נפוצים לנתונים שמשמשים בסדנת התכנות הזו, כמו חיפושים של לקוחות לפי כתובת אימייל מדויקת או משוערת, אחזור חשבונות לפי לקוח ושאילתות וחיפושים יעילים של נתוני עסקאות.

ה-TransactionCategoryModel משתמש ב-Vertex AI כדי לאפשר קריאות SQL ישירות למודל LLM, שמשמש לסיווג דינמי של טרנזקציות ב-codelab הזה.

סיכום

בשלב הזה יצרתם את מסד הנתונים ואת הסכימה ב-Spanner.

הבא בתור

בשלב הבא, טוענים את נתוני האפליקציה לדוגמה.

5. טען נתונים

עכשיו תוסיפו פונקציונליות לטעינת נתונים לדוגמה מקובצי CSV למסד הנתונים.

פותחים את App.java ומתחילים בהחלפת הייבוא:

package com.google.codelabs;

import java.io.FileReader;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ExecutionException;

import com.google.api.gax.longrunning.OperationFuture;
import com.google.cloud.Timestamp;
import com.google.cloud.bigquery.BigQuery;
import com.google.cloud.bigquery.BigQueryOptions;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseAdminClient;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.DatabaseId;
import com.google.cloud.spanner.Key;
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.SpannerOptions;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.Struct;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import com.opencsv.CSVReader;

לאחר מכן מוסיפים את שיטות ההוספה לכיתה App:

  // Insert customers from CSV
  public static void insertCustomers(DatabaseClient dbClient) {
    System.out.println("Inserting customers...");
    dbClient
        .readWriteTransaction()
        .run(transaction -> {
          int count = 0;
          List<Statement> statements = new ArrayList<>();
          try (CSVReader reader = new CSVReader(new FileReader("data/customers.csv"))) {
            reader.skip(1);
            String[] line;
            while ((line = reader.readNext()) != null) {
              Statement statement = Statement.newBuilder(
                  "INSERT INTO Customers (CustomerId, FirstName, LastName, Email, Address) "
                      + "VALUES (@customerId, @firstName, @lastName, @email, @address)")
                  .bind("customerId").to(Long.parseLong(line[0]))
                  .bind("firstName").to(line[1])
                  .bind("lastName").to(line[2])
                  .bind("email").to(line[3])
                  .bind("address").to(line[4])
                  .build();
              statements.add(statement);
              count++;
            }
            transaction.batchUpdate(statements);
            System.out.println("Inserted " + count + " customers");
            return null;
          }
        });
  }

  // Insert accounts from CSV
  public static void insertAccounts(DatabaseClient dbClient) {
    System.out.println("Inserting accounts...");
    dbClient
        .readWriteTransaction()
        .run(transaction -> {
          int count = 0;
          List<Statement> statements = new ArrayList<>();
          try (CSVReader reader = new CSVReader(new FileReader("data/accounts.csv"))) {
            reader.skip(1);
            String[] line;
            while ((line = reader.readNext()) != null) {
              Statement statement = Statement.newBuilder(
                "INSERT INTO Accounts (AccountId, CustomerId, AccountType, Balance, OpenDate) "
                    + "VALUES (@accountId, @customerId, @accountType, @balance, @openDate)")
                .bind("accountId").to(Long.parseLong(line[0]))
                .bind("customerId").to(Long.parseLong(line[1]))
                .bind("accountType").to(line[2])
                .bind("balance").to(new BigDecimal(line[3]))
                .bind("openDate").to(line[4])
                .build();
              statements.add(statement);
              count++;
            }
            transaction.batchUpdate(statements);
            System.out.println("Inserted " + count + " accounts");
            return null;
          }
        });
  }

  // Insert transactions from CSV
  public static void insertTransactions(DatabaseClient dbClient) {
    System.out.println("Inserting transactions...");
    dbClient
        .readWriteTransaction()
        .run(transaction -> {
          int count = 0;
          List<Statement> statements = new ArrayList<>();
          try (CSVReader reader = new CSVReader(new FileReader("data/transactions.csv"))) {
            reader.skip(1);
            String[] line;

            // Specify timestamps that are within last 30 days
            Random random = new Random();
            Instant startTime = Instant.now().minus(15, ChronoUnit.DAYS);
            Instant currentTimestamp = startTime;

            Map<Long, BigDecimal> balanceChanges = new HashMap<>();
            while ((line = reader.readNext()) != null) {
              long accountId = Long.parseLong(line[1]);
              String transactionType = line[2];
              BigDecimal amount = new BigDecimal(line[3]);
              int randomMinutes = random.nextInt(60) + 1;
              currentTimestamp = currentTimestamp.plus(Duration.ofMinutes(randomMinutes));
              Timestamp timestamp = Timestamp.ofTimeSecondsAndNanos(
                  currentTimestamp.getEpochSecond(), currentTimestamp.getNano());
              Statement statement = Statement.newBuilder(
                "INSERT INTO TransactionLedger (TransactionId, AccountId, TransactionType, Amount,"
                    + "Timestamp, Category, Description) "
                    + "VALUES (@transactionId, @accountId, @transactionType, @amount, @timestamp,"
                    + "@category, @description)")
                .bind("transactionId").to(Long.parseLong(line[0]))
                .bind("accountId").to(accountId)
                .bind("transactionType").to(transactionType)
                .bind("amount").to(amount)
                .bind("timestamp").to(timestamp)
                .bind("category").to(line[5])
                .bind("description").to(line[6])
                .build();
              statements.add(statement);

              // Track balance changes per account
              BigDecimal balanceChange = balanceChanges.getOrDefault(accountId,
                  BigDecimal.ZERO);
              if ("Credit".equalsIgnoreCase(transactionType)) {
                balanceChanges.put(accountId, balanceChange.add(amount));
              } else if ("Debit".equalsIgnoreCase(transactionType)) {
                balanceChanges.put(accountId, balanceChange.subtract(amount));
              } else {
                System.err.println("Unsupported transaction type: " + transactionType);
                continue;
              }

              count++;
            }

            // Apply final balance updates
            for (Map.Entry<Long, BigDecimal> entry : balanceChanges.entrySet()) {
              long accountId = entry.getKey();
              BigDecimal balanceChange = entry.getValue();

              Struct row = transaction.readRow(
                  "Accounts",
                  Key.of(accountId),
                  List.of("Balance"));
              if (row != null) {
                BigDecimal currentBalance = row.getBigDecimal("Balance");
                BigDecimal updatedBalance = currentBalance.add(balanceChange);
                Statement statement = Statement.newBuilder(
                  "UPDATE Accounts SET Balance = @balance WHERE AccountId = @accountId")
                  .bind("accountId").to(accountId)
                  .bind("balance").to(updatedBalance)
                  .build();
                statements.add(statement);
              }
            }

            transaction.batchUpdate(statements);
            System.out.println("Inserted " + count + " transactions");
          }
          return null;
        });
  }

מוסיפים עוד משפט case בשיטה main להוספה בתוך switch (command):

        case "insert":
          String insertType = (args.length >= 2) ? args[1] : "";
          if (insertType.equals("customers")) {
            insertCustomers(dbClient);
          } else if (insertType.equals("accounts")) {
            insertAccounts(dbClient);
          } else if (insertType.equals("transactions")) {
            insertTransactions(dbClient);
          } else {
            insertCustomers(dbClient);
            insertAccounts(dbClient);
            insertTransactions(dbClient);
          }
          break;

לבסוף, מוסיפים את הפונקציה insert לשיטה printUsageAndExit:

    System.out.println("  java -jar target/onlinebanking.jar insert");
    System.out.println("      - Insert sample Customers, Accounts, and Transactions into the "
        + "database.\n");

שומרים את השינויים שביצעתם בקובץ App.java.

בונים מחדש את האפליקציה:

mvn package

מריצים את הפקודה insert כדי להוסיף את הנתונים לדוגמה:

java -jar target/onlinebanking.jar insert

הפלט אמור להיראות כך:

Inserting customers...
Inserted 100 customers
Inserting accounts...
Inserted 125 accounts
Inserting transactions...
Inserted 200 transactions

במסוף Spanner, חוזרים אל Spanner Studio עבור המופע ומסד הנתונים. לאחר מכן בוחרים את הטבלה TransactionLedger ולוחצים על 'נתונים' בסרגל הצד כדי לוודא שהנתונים נטענו. צריכות להיות 200 שורות בטבלה.

לנתונים

סיכום

בשלב הזה, הוספתם את הנתונים לדוגמה למסד הנתונים.

הבא בתור

בשלב הבא, תשתמשו בשילוב של Vertex AI כדי לסווג באופן אוטומטי עסקאות בנקאיות ישירות ב-Spanner SQL.

6. סיווג נתונים באמצעות Vertex AI

בשלב הזה, תשתמשו ביכולות של Vertex AI כדי לסווג אוטומטית את העסקאות הפיננסיות ישירות ב-Spanner SQL. באמצעות Vertex AI, אתם יכולים לבחור מודל קיים שעבר אימון מראש או לאמן ולפרוס מודל משלכם. אפשר לראות את המודלים הזמינים ב-Vertex AI Model Garden.

ב-codelab הזה נשתמש באחד מהמודלים של Gemini, ‏ Gemini Flash Lite. הגרסה הזו של Gemini חסכונית אבל עדיין יכולה להתמודד עם רוב עומסי העבודה היומיומיים.

יש לנו כרגע מספר עסקאות פיננסיות שאנחנו רוצים לסווג (groceries,‏ transportation וכו') בהתאם לתיאור. כדי לעשות את זה, אפשר לרשום מודל ב-Spanner ואז להשתמש ב-ML.PREDICT כדי לקרוא למודל ה-AI.

באפליקציית בנקאות, יכול להיות שנרצה לסווג עסקאות כדי לקבל תובנות מעמיקות יותר לגבי התנהגות הלקוחות, וכך להתאים אישית את השירותים, לזהות חריגות בצורה יעילה יותר או לספק ללקוחות את האפשרות לעקוב אחרי התקציב שלהם מחודש לחודש.

השלב הראשון כבר בוצע כשנוצרו מסד הנתונים והסכימה, וכך נוצר מודל כמו זה:

יצירת הצהרת מודל

בשלב הבא נוסיף לאפליקציה method לקריאה ל-ML.PREDICT.

פותחים את App.java ומוסיפים את השיטה categorize:

  // Use Vertex AI to set the category of transactions
  public static void categorize(DatabaseClient dbClient) {
    System.out.println("Categorizing transactions...");
    try {
      // Create a prompt to instruct the LLM how to categorize the transactions
      String categories = String.join(", ", Arrays.asList("Entertainment", "Gifts", "Groceries",
          "Investment", "Medical", "Movies", "Online Shopping", "Other", "Purchases", "Refund",
          "Restaurants", "Salary", "Transfer", "Transportation", "Utilities"));
      String prompt = "Categorize the following financial activity into one of these "
          + "categories: " +  categories + ". Return Other if the description cannot be mapped to "
          + "one of these categories.  Only return the exact category string, no other text or "
          + "punctuation or reasoning. Description: ";
      String sql = "UPDATE TransactionLedger SET Category = (\n"
          + "  SELECT content FROM ML.PREDICT(MODEL `TransactionCategoryModel`, (\n"
          + "    SELECT CONCAT('" + prompt + "', CASE WHEN TRIM(Description) = ''\n"
          + "    THEN 'Other' ELSE Description END) AS prompt\n"
          + "  ))\n"
          + ") WHERE TRUE";

      // Use partitioned update to batch update a large number of rows
      dbClient.executePartitionedUpdate(Statement.of(sql));
      System.out.println("Completed categorizing transactions");
    } catch (SpannerException e) {
      throw e;
    }
  }

מוסיפים עוד משפט case בשיטה main method for categorize:

        case "categorize":
          categorize(dbClient);
          break;

לבסוף, מוסיפים את ההוראות לשימוש בשיטת printUsageAndExit:

    System.out.println("  java -jar target/onlinebanking.jar categorize");
    System.out.println("      - Use AI to categorize transactions in the database.\n");

שומרים את השינויים שביצעתם בקובץ App.java.

בונים מחדש את האפליקציה:

mvn package

כדי לסווג את העסקאות במסד הנתונים, מריצים את הפקודה categorize:

java -jar target/onlinebanking.jar categorize

הפלט אמור להיראות כך:

Categorizing transactions...
Completed categorizing transactions

ב-Spanner Studio, מריצים את ההצהרה Preview Data עבור הטבלה TransactionLedger. עכשיו העמודה Category אמורה להתמלא בכל השורות.

הצגת נתונים לפי קטגוריות

אחרי שסיווגנו את העסקאות, אנחנו יכולים להשתמש במידע הזה לשאילתות פנימיות או לשאילתות שמוצגות ללקוחות. בשלב הבא נראה איך אפשר לגלות כמה לקוח מסוים מוציא על קטגוריה מסוימת במהלך החודש.

סיכום

בשלב הזה השתמשתם במודל שעבר אימון מראש כדי לבצע סיווג של הנתונים באמצעות AI.

הבא בתור

לאחר מכן, תשתמשו בטוקניזציה כדי לבצע חיפושים משוערים וחיפושים בטקסט מלא.

7. שאילתה באמצעות חיפוש טקסט מלא

הוספת קוד השאילתה

‫Spanner מספק הרבה שאילתות חיפוש של טקסט מלא. בשלב הזה תבצעו חיפוש בהתאמה מדויקת, ואז חיפוש לא מדויק וחיפוש טקסט מלא.

פותחים את App.java ומתחילים בהחלפת הייבוא:

package com.google.codelabs;

import java.io.FileReader;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import com.google.api.gax.longrunning.OperationFuture;
import com.google.cloud.Timestamp;
import com.google.cloud.bigquery.BigQuery;
import com.google.cloud.bigquery.BigQueryOptions;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseAdminClient;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.DatabaseId;
import com.google.cloud.spanner.Key;
import com.google.cloud.spanner.ReadOnlyTransaction;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.SpannerOptions;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.Struct;
import com.google.cloud.spanner.TimestampBound;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import com.opencsv.CSVReader;

לאחר מכן מוסיפים את שיטות השאילתה:

  // Get current account balance(s) by customer
  public static void getBalance(DatabaseClient dbClient, long customerId) {
    String query = "SELECT AccountId, Balance\n"
        + "FROM Accounts\n"
        + "WHERE CustomerId = @customerId";
    Statement statement = Statement.newBuilder(query)
        .bind("customerId").to(customerId)
        .build();

    // Ignore ongoing transactions, use stale reads as seconds-old data is sufficient
    TimestampBound stalenessBound = TimestampBound.ofMaxStaleness(5, TimeUnit.SECONDS);
    try (ReadOnlyTransaction transaction = dbClient.singleUseReadOnlyTransaction(stalenessBound);
        ResultSet resultSet = transaction.executeQuery(statement);) {
      System.out.println("Account balances for customer " + customerId + ":");
      while (resultSet.next()) {
        System.out.println("  Account " + resultSet.getLong("AccountId") + ": "
            + resultSet.getBigDecimal("Balance"));
      }
    }
  }

  // Find customers by email
  public static void findCustomers(DatabaseClient dbClient, String email) {
    // Query using fuzzy search (ngrams) to allow for spelling mistakes
    String query = "SELECT CustomerId, Email\n"
        + "FROM Customers\n"
        + "WHERE SEARCH_NGRAMS(EmailTokens, @email)\n"
        + "ORDER BY SCORE_NGRAMS(EmailTokens, @email) DESC\n"
        + "LIMIT 10";
    Statement statement = Statement.newBuilder(query)
        .bind("email").to(email)
        .build();

    try (ReadOnlyTransaction transaction = dbClient.singleUseReadOnlyTransaction();
        ResultSet resultSet = transaction.executeQuery(statement)) {
      System.out.println("Customer emails matching " + email + " (top 10 matches):");
      while (resultSet.next()) {
        System.out.println("  Customer " + resultSet.getLong("CustomerId") + ": "
            + resultSet.getString("Email"));
      }
    }
  }

  // Get total monthly spending for a customer by category
  public static void getSpending(DatabaseClient dbClient, long customerId, String category) {
    // Query category using full-text search
    String query = "SELECT SUM(Amount) as TotalSpending\n"
        + "FROM TransactionLedger t\n"
        + "JOIN Accounts a\n"
        + "  ON t.AccountId = a.AccountId\n"
        + "WHERE t.TransactionType = 'Debit'\n"
        + "  AND a.CustomerId = @customerId\n"
        + "  AND t.Timestamp >= TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -30 DAY)\n"
        + "  AND (SEARCH(t.CategoryTokens, @category) OR SEARCH(t.DescriptionTokens, @category))";
    Statement statement = Statement.newBuilder(query)
        .bind("customerId").to(customerId)
        .bind("category").to(category)
        .build();

    try (ReadOnlyTransaction transaction = dbClient.singleUseReadOnlyTransaction();
        ResultSet resultSet = transaction.executeQuery(statement);) {
      System.out.println("Total spending for customer " + customerId + " under category "
          + category + ":");
      while (resultSet.next()) {
        BigDecimal totalSpending = BigDecimal.ZERO;
        if (!resultSet.isNull("TotalSpending")) {
          totalSpending = resultSet.getBigDecimal("TotalSpending");
        }
        System.out.println("  " + totalSpending);
      }
    }
  }

מוסיפים עוד הצהרה של case בשיטה main לשאילתה:

        case "query":
          String queryType = (args.length >= 2) ? args[1] : "";
          if (queryType.equals("balance")) {
            long customerId = (args.length >= 3) ? Long.parseLong(args[2]) : 1L;
            getBalance(dbClient, customerId);
          } else if (queryType.equals("email")) {
            String email = (args.length >= 3) ? args[2] : "";
            findCustomers(dbClient, email);
          } else if (queryType.equals("spending")) {
            long customerId = (args.length >= 3) ? Long.parseLong(args[2]) : 1L;
            String category = (args.length >= 4) ? args[3] : "";
            getSpending(dbClient, customerId, category);
          } else {
            printUsageAndExit();
          }
          break;

לבסוף, מוסיפים את הפקודות של השאילתה לשיטה printUsageAndExit:

    System.out.println("  java -jar target/onlinebanking.jar query balance 1");
    System.out.println("      - Query customer account balance(s) by customer id.\n");
    System.out.println("  java -jar target/onlinebanking.jar query email madi");
    System.out.println("      - Find customers by email using fuzzy search.\n");
    System.out.println("  java -jar target/onlinebanking.jar query spending 1 groceries");
    System.out.println("      - Query customer spending by customer id and category using "
        + "full-text search.\n");

שומרים את השינויים שביצעתם בקובץ App.java.

בונים מחדש את האפליקציה:

mvn package

חיפוש התאמה מדויקת של יתרות בחשבונות לקוחות

שאילתה בהתאמה מדויקת מחפשת שורות תואמות שזהות למונח מסוים.

כדי לשפר את הביצועים, נוסף כבר אינדקס כשיוצרים את מסד הנתונים ואת הסכימה:

  "CREATE INDEX AccountsByCustomer\n"
          + "ON Accounts (CustomerId)",

השיטה getBalance משתמשת באינדקס הזה באופן מרומז כדי למצוא לקוחות שתואמים ל-customerId שצוין, וגם מבצעת הצטרפות לחשבונות ששייכים ללקוח הזה.

כך נראית השאילתה כשהיא מופעלת ישירות ב-Spanner Studio: שאילתה ידנית לגבי היתרה

כדי להציג את יתרות החשבון של לקוח מספר 1, מריצים את הפקודה:

java -jar target/onlinebanking.jar query balance 1

הפלט אמור להיראות כך:

Account balances for customer 1:
  Account 1: 9875.25
  Account 7: 9900
  Account 110: 38200

יש 100 לקוחות, כך שאפשר גם לשלוח שאילתה לגבי יתרות של חשבונות לקוחות אחרים על ידי ציון מספר לקוח אחר:

java -jar target/onlinebanking.jar query balance 5
java -jar target/onlinebanking.jar query balance 10
java -jar target/onlinebanking.jar query balance 99

ביצוע חיפוש משוער של כתובות אימייל של לקוחות

חיפושים משוערים מאפשרים למצוא התאמות משוערות למונחי חיפוש, כולל גרסאות איות ושגיאות הקלדה.

אינדקס n-gram כבר נוסף כשיוצרים את מסד הנתונים ואת הסכימה:

CREATE TABLE Customers (
  ...
  EmailTokens TOKENLIST AS (TOKENIZE_SUBSTRING(Email,
    ngram_size_min=>2,
    ngram_size_max=>3,
    relative_search_types=>["all"])) HIDDEN,
) PRIMARY KEY(CustomerId);

CREATE SEARCH INDEX CustomersFuzzyEmail ON Customers(EmailTokens);

השיטה findCustomers משתמשת ב-SEARCH_NGRAMS וב-SCORE_NGRAMS כדי לשלוח שאילתות לאינדקס הזה ולמצוא לקוחות לפי כתובת אימייל. מכיוון שהטור של האימייל עבר טוקניזציה של n-gram, השאילתה הזו יכולה להכיל שגיאות איות ועדיין להחזיר תשובה נכונה. התוצאות מוצגות לפי ההתאמה הכי טובה.

כדי למצוא כתובות אימייל תואמות של לקוחות שמכילות madi, מריצים את הפקודה:

java -jar target/onlinebanking.jar query email madi

הפלט אמור להיראות כך:

Customer emails matching madi (top 10 matches):
  Customer 39: madison.perez@example.com
  Customer 64: mason.gray@example.com
  Customer 91: mabel.alexander@example.com

התשובה הזו מציגה את ההתאמות הכי קרובות שכוללות את המחרוזת madi או מחרוזת דומה, בסדר מדורג.

כך נראית השאילתה אם היא מופעלת ישירות ב-Spanner Studio: חיפוש ידני של madi

חיפוש משוער יכול לעזור גם בשגיאות איות, כמו שגיאות איות של emily:

java -jar target/onlinebanking.jar query email emily
java -jar target/onlinebanking.jar query email emliy
java -jar target/onlinebanking.jar query email emilee

הפלט אמור להיראות כך:

Customer emails matching emliy (top 10 matches):
  Customer 31: emily.lopez@example.com

בכל מקרה, כתובת האימייל הצפויה של הלקוח מוחזרת כתוצאה הראשונה.

התכונה של חיפוש טקסט מלא ב-Spanner משמשת לאחזור רשומות על סמך מילות מפתח או ביטויים. הוא יכול לתקן שגיאות איות או לחפש מילים נרדפות.

אינדקס חיפוש טקסט מלא כבר נוסף כשיוצרים את מסד הנתונים והסכימה:

CREATE TABLE TransactionLedger (
  ...
  CategoryTokens TOKENLIST AS (TOKENIZE_FULLTEXT(Category)) HIDDEN,
  DescriptionTokens TOKENLIST AS (TOKENIZE_FULLTEXT(Description)) HIDDEN,
) PRIMARY KEY(AccountId, TransactionId),
  INTERLEAVE IN PARENT Accounts ON DELETE CASCADE;

CREATE SEARCH INDEX TransactionLedgerTextSearch ON TransactionLedger(CategoryTokens, DescriptionTokens);

השיטה getSpending משתמשת בפונקציית החיפוש SEARCH בטקסט מלא כדי להתאים לאינדקס הזה. הכלי מחפש את כל ההוצאות (חיובים) ב-30 הימים האחרונים לפי מזהה הלקוח שצוין.

כדי לקבל את סך ההוצאות בחודש האחרון של לקוח 1 בקטגוריה groceries, מריצים את הפקודה:

java -jar target/onlinebanking.jar query spending 1 groceries

הפלט אמור להיראות כך:

Total spending for customer 1 under category groceries:
  50

אפשר גם לראות את ההוצאות בקטגוריות אחרות (שסווגו בשלב הקודם), או להשתמש במספר לקוח אחר:

java -jar target/onlinebanking.jar query spending 1 transportation
java -jar target/onlinebanking.jar query spending 1 restaurants
java -jar target/onlinebanking.jar query spending 12 entertainment

סיכום

בשלב הזה, ביצעתם שאילתות בהתאמה מדויקת וגם חיפושים לא מדויקים וחיפושים בטקסט מלא.

הבא בתור

בשלב הבא, תשולב מערכת Spanner עם Google BigQuery כדי להריץ שאילתות מאוחדות, שיאפשרו לכם לשלב את נתוני Spanner בזמן אמת עם נתוני BigQuery.

‫8. הרצת שאילתות מאוחדות באמצעות BigQuery

יצירת מערך הנתונים ב-BigQuery

בשלב הזה, תשלבו נתונים מ-BigQuery ומ-Spanner באמצעות שאילתות מאוחדות.

כדי לעשות את זה, בשורת הפקודה של Cloud Shell, יוצרים קודם מערך נתונים MarketingCampaigns:

bq mk --location=us-central1 MarketingCampaigns

הפלט אמור להיראות כך:

Dataset '<PROJECT_ID>:MarketingCampaigns' successfully created.

וטבלה CustomerSegments במערך הנתונים:

bq mk --table MarketingCampaigns.CustomerSegments CampaignId:STRING,CampaignName:STRING,CustomerId:INT64

הפלט אמור להיראות כך:

Table '<PROJECT_ID>:MarketingCampaigns.CustomerSegments' successfully created.

לאחר מכן, יוצרים חיבור מ-BigQuery ל-Spanner:

bq mk --connection \
  --connection_type=CLOUD_SPANNER \
  --properties="{\"database\": \"projects/$GOOGLE_CLOUD_PROJECT/instances/cloudspanner-onlinebanking/databases/onlinebanking\", \"useParallelism\": true, \"useDataBoost\": true}" \
  --location=us-central1 \
  spanner-connection

הפלט אמור להיראות כך:

Connection <PROJECT_NUMBER>.us-central1.spanner-connection successfully created

לבסוף, מוסיפים כמה לקוחות לטבלת BigQuery שאפשר לצרף לנתוני Spanner:

bq query --use_legacy_sql=false '
INSERT INTO MarketingCampaigns.CustomerSegments (CampaignId, CampaignName, CustomerId)
VALUES
  ("campaign1", "Spring Promotion", 1),
  ("campaign1", "Spring Promotion", 3),
  ("campaign1", "Spring Promotion", 5),
  ("campaign1", "Spring Promotion", 7),
  ("campaign1", "Spring Promotion", 9),
  ("campaign1", "Spring Promotion", 11)'

הפלט אמור להיראות כך:

Waiting on bqjob_r76a7ce76c5ec948f_0000019644bda052_1 ... (0s) Current status: DONE
Number of affected rows: 6

כדי לוודא שהנתונים זמינים, אפשר להריץ שאילתה ב-BigQuery:

bq query --use_legacy_sql=false "SELECT * FROM MarketingCampaigns.CustomerSegments"

הפלט אמור להיראות כך:

+------------+------------------+------------+
| CampaignId |   CampaignName   | CustomerId |
+------------+------------------+------------+
| campaign1  | Spring Promotion |          1 |
| campaign1  | Spring Promotion |          5 |
| campaign1  | Spring Promotion |          7 |
| campaign1  | Spring Promotion |          9 |
| campaign1  | Spring Promotion |         11 |
| campaign1  | Spring Promotion |          3 |
+------------+------------------+------------+

הנתונים האלה ב-BigQuery מייצגים נתונים שנוספו באמצעות תהליכי עבודה שונים של הבנק. לדוגמה, רשימת הלקוחות שפתחו לאחרונה חשבונות או נרשמו למבצע שיווקי. כדי לקבוע את רשימת הלקוחות שאליהם אנחנו רוצים לפנות בקמפיין השיווקי, אנחנו צריכים להריץ שאילתה על הנתונים האלה ב-BigQuery וגם על הנתונים בזמן אמת ב-Spanner. שאילתה מאוחדת מאפשרת לנו לעשות את זה בשאילתה אחת.

הרצת שאילתה מאוחדת באמצעות BigQuery

בשלב הבא נוסיף לאפליקציה שיטה לקריאה ל-EXTERNAL_QUERY כדי לבצע את השאילתה המאוחדת. כך תוכלו לאחד ולנתח נתוני לקוחות ב-BigQuery וב-Spanner, למשל לזהות אילו לקוחות עומדים בקריטריונים של קמפיין השיווק שלנו על סמך ההוצאות האחרונות שלהם.

פותחים את App.java ומתחילים בהחלפת הייבוא:

package com.google.codelabs;

import java.io.FileReader;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import com.google.api.gax.longrunning.OperationFuture;
import com.google.cloud.Timestamp;
import com.google.cloud.bigquery.BigQuery;
import com.google.cloud.bigquery.BigQueryException;
import com.google.cloud.bigquery.BigQueryOptions;
import com.google.cloud.bigquery.connection.v1.ConnectionName;
import com.google.cloud.bigquery.JobException;
import com.google.cloud.bigquery.QueryJobConfiguration;
import com.google.cloud.bigquery.TableResult;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseAdminClient;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.DatabaseId;
import com.google.cloud.spanner.Key;
import com.google.cloud.spanner.ReadOnlyTransaction;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.SpannerOptions;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.Struct;
import com.google.cloud.spanner.TimestampBound;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import com.opencsv.CSVReader;

לאחר מכן מוסיפים את אמצעי התשלום campaign:

  // Get customers for quarterly marketing campaign in BigQuery using Spanner data
  public static void campaign(BigQuery bq, DatabaseId db, String location, String campaignId,
      int threshold) {
    // The BigQuery dataset, table, and Spanner connection must already exist for this to succeed
    ConnectionName connection = ConnectionName.of(db.getInstanceId().getProject(), location,
        "spanner-connection");

    // Use a federated query to bring Spanner data into BigQuery
    String bqQuery = "SELECT cs.CampaignName, c.CustomerId, c.FullName, t.TotalSpending\n"
        + "FROM MarketingCampaigns.CustomerSegments cs\n"
        + "JOIN EXTERNAL_QUERY('" + connection.toString() + "',\n"
        + "  \"SELECT t.AccountId, SUM(t.Amount) AS TotalSpending"
        + "   FROM TransactionLedger t"
        + "   WHERE t.Timestamp >= TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -90 DAY)"
        + "   GROUP BY t.AccountId"
        + "   HAVING SUM(t.Amount) > " + threshold + "\"\n"
        + ") t ON cs.CustomerId = t.AccountId\n"
        + "JOIN EXTERNAL_QUERY('" + connection.toString() + "',\n"
        + "  \"SELECT CustomerId, FullName"
        + "   FROM Customers\"\n"
        + ") c ON c.CustomerId = cs.CustomerId\n"
        + "WHERE cs.CampaignId = '" + campaignId + "'";
    try {
      QueryJobConfiguration queryConfig = QueryJobConfiguration.newBuilder(bqQuery).build();
      TableResult results = bq.query(queryConfig);

      System.out.println("Customers for campaign (" + campaignId + "):");
      results.iterateAll().forEach(row -> {
        System.out.println("  " + row.get("FullName").getStringValue()
            + " (" + row.get("CustomerId").getStringValue() + ")");
      });
    } catch (JobException e) {
      throw (BigQueryException) e.getCause();
    } catch (InterruptedException e) {
      throw SpannerExceptionFactory.propagateInterrupt(e);
    }
  }

מוסיפים עוד הצהרת case בשיטה main לקמפיין:

        case "campaign":
          String campaignId = (args.length >= 2) ? args[1] : "";
          int threshold = (args.length >= 3) ? Integer.parseInt(args[2]) : 5000;
          campaign(bigquery, db, location, campaignId, threshold);
          break;

לבסוף, מוסיפים את אופן השימוש בקמפיין לשיטה printUsageAndExit:

    System.out.println("  java -jar target/onlinebanking.jar campaign campaign1 5000");
    System.out.println("      - Use Federated Queries (BigQuery) to find customers that match a "
        + "marketing campaign by name based on a recent spending threshold.\n");

שומרים את השינויים שביצעתם בקובץ App.java.

בונים מחדש את האפליקציה:

mvn package

מריצים שאילתה מאוחדת כדי לקבוע אילו לקוחות צריכים להיכלל בקמפיין השיווקי (campaign1) אם הם הוציאו לפחות $5000 ב-3 החודשים האחרונים, על ידי הפעלת הפקודה campaign:

java -jar target/onlinebanking.jar campaign campaign1 5000

הפלט אמור להיראות כך:

Customers for campaign (campaign1):
  Alice Smith (1)
  Eve Davis (5)
  Kelly Thomas (11)

עכשיו אפשר לטרגט את הלקוחות האלה עם מבצעים או תמריצים בלעדיים.

לחלופין, אפשר לחפש מספר גדול יותר של לקוחות שהגיעו לסף הוצאות נמוך יותר ב-3 החודשים האחרונים:

java -jar target/onlinebanking.jar campaign campaign1 2500

הפלט אמור להיראות כך:

Customers for campaign (campaign1):
  Alice Smith (1)
  Charlie Williams (3)
  Eve Davis (5)
  Ivy Taylor (9)
  Kelly Thomas (11)

סיכום

בשלב הזה, הפעלתם בהצלחה שאילתות מאוחדות מ-BigQuery שהביאו נתונים מ-Spanner בזמן אמת.

הבא בתור

אחרי כן, תוכלו למחוק את המשאבים שנוצרו בסדנת הקוד הזו כדי להימנע מחיובים.

‫9. ניקוי (אופציונלי)

השלב הזה הוא אופציונלי. אם רוצים להמשיך להתנסות במופע Spanner, אין צורך לנקות אותו בשלב הזה. עם זאת, הפרויקט שבו אתם משתמשים ימשיך להיות מחויב על המופע. אם אין לכם יותר צורך במכונה הזו, כדאי למחוק אותה עכשיו כדי להימנע מהחיובים האלה. בנוסף למופע Spanner, ה-codelab הזה יצר גם מערך נתונים וחיבור ל-BigQuery, שצריך לנקות אותם כשאין בהם יותר צורך.

מוחקים את מכונת Spanner:

gcloud spanner instances delete cloudspanner-onlinebanking

מאשרים שרוצים להמשיך (מקלידים Y):

Delete instance [cloudspanner-onlinebanking]. Are you sure?

Do you want to continue (Y/n)?

מוחקים את החיבור ל-BigQuery ואת מערך הנתונים:

bq rm --connection --location=us-central1 spanner-connection
bq rm -r MarketingCampaigns

מאשרים את המחיקה של מערך הנתונים ב-BigQuery (מקלידים Y):

rm: remove dataset '<PROJECT_ID>:MarketingCampaigns'? (y/N)

‫10. מזל טוב

🚀 יצרתם מכונת Cloud Spanner חדשה, יצרתם מסד נתונים ריק, טענתם נתונים לדוגמה, ביצעתם פעולות ושאילתות מתקדמות, ובאופן אופציונלי מחקתם את מכונת Cloud Spanner.

מה נכלל

  • איך מגדירים מופע Spanner.
  • איך יוצרים מסד נתונים וטבלאות.
  • איך לטעון נתונים לטבלאות של מסד הנתונים ב-Spanner.
  • איך קוראים למודלים של Vertex AI מ-Spanner.
  • איך לשלוח שאילתות למסד הנתונים שלכם ב-Spanner באמצעות חיפוש משוער וחיפוש טקסט מלא.
  • איך מבצעים שאילתות מאוחדות ב-Spanner מ-BigQuery.
  • איך מוחקים את מכונת Spanner.

מה השלב הבא?