Codelab – פיתוח אפליקציה להמלצות על תנוחות יוגה לפי הקשר בעזרת Firestore, ‏ Vector Search, ‏ Langchain ו-Gemini (גרסת Node.js)

1. מבוא

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

במהלך הקודלאב, נשתמש בגישה הדרגתית לפי השלבים הבאים:

  1. שימוש במערך נתונים קיים של Hugging Face עם תנוחות יוגה (בפורמט JSON).
  2. משפרים את מערך הנתונים באמצעות תיאור שדה נוסף שמשתמש ב-Gemini כדי ליצור תיאורים לכל אחת מהתנוחות.
  3. טעינת הנתונים של תנוחות היוגה כאוסף של מסמכים באוסף של Firestore עם הטמעות שנוצרו.
  4. יצירת אינדקס מורכב ב-Firestore כדי לאפשר חיפוש וקטור.
  5. אפשר להשתמש בחיפוש וקטורים באפליקציית Node.js שמרכזת את כל הרכיבים, כפי שמתואר בהמשך:

84e1cbf29cbaeedc.png

מה עליכם לעשות

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

מה תלמדו

  • איך משתמשים ב-Gemini כדי ליצור תוכן טקסטואלי, ובהקשר של סדנת הקוד הזו, ליצור תיאורים של תנוחות יוגה
  • איך טוענים רשומות ממערך נתונים משופר מ-Hugging Face ל-Firestore יחד עם Vector Embeddings
  • איך משתמשים בחיפוש וקטורים ב-Firestore כדי לחפש נתונים על סמך שאילתה בשפה טבעית
  • איך משתמשים ב-Google Cloud Text to Speech API כדי ליצור תוכן אודיו

מה צריך להכין

  • דפדפן האינטרנט Chrome
  • חשבון Gmail
  • פרויקט ב-Cloud שבו החיוב מופעל

סדנת הקוד הזו מיועדת למפתחים מכל הרמות (כולל למתחילים), והיא כוללת שימוש ב-JavaScript וב-Node.js באפליקציה לדוגמה. עם זאת, לא נדרש ידע ב-JavaScript וב-Node.js כדי להבין את המושגים המוצגים.

2. לפני שמתחילים

יצירת פרויקט

  1. בדף לבחירת הפרויקט במסוף Google Cloud, בוחרים או יוצרים פרויקט ב-Google Cloud.
  2. הקפידו לוודא שהחיוב מופעל בפרויקט שלכם ב-Cloud. כך בודקים אם החיוב מופעל בפרויקט
  3. נשתמש ב-Cloud Shell, סביבת שורת פקודה שפועלת ב-Google Cloud ומגיעה עם bq טעון מראש. לוחצים על Activate Cloud Shell בחלק העליון של מסוף Google Cloud.

תמונה של לחצן הפעלת Cloud Shell

  1. אחרי שמתחברים ל-Cloud Shell, בודקים שכבר בוצע אימות ושהמזהה של הפרויקט מוגדר כפרויקט באמצעות הפקודה הבאה:
gcloud auth list
  1. מריצים את הפקודה הבאה ב-Cloud Shell כדי לוודא שהפקודה gcloud מכירה את הפרויקט.
gcloud config list project
  1. אם הפרויקט לא מוגדר, משתמשים בפקודה הבאה כדי להגדיר אותו:
gcloud config set project <YOUR_PROJECT_ID>
  1. מפעילים את ממשקי ה-API הנדרשים באמצעות הפקודה שמופיעה בהמשך. הפעולה עשויה להימשך כמה דקות, אז חשוב להמתין.
gcloud services enable firestore.googleapis.com \
                       compute.googleapis.com \
                       cloudresourcemanager.googleapis.com \
                       servicenetworking.googleapis.com \
                       run.googleapis.com \
                       cloudbuild.googleapis.com \
                       cloudfunctions.googleapis.com \
                       aiplatform.googleapis.com \
                       texttospeech.googleapis.com

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

Operation "operations/..." finished successfully.

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

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

במסמכי העזרה מפורטות הפקודות של gcloud והשימוש בהן.

שכפול המאגר והגדרת הגדרות הסביבה

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

git clone https://github.com/rominirani/yoga-poses-recommender-nodejs

כדי להפעיל את העורך, לוחצים על Open Editor בסרגל הכלים שבחלון של Cloud Shell. לוחצים על סרגל התפריטים בפינה הימנית העליונה ובוחרים באפשרות 'קובץ' → 'פתיחת תיקייה', כפי שמוצג בהמשך:

66221fd0d0e5202f.png

בוחרים בתיקייה yoga-poses-recommender-nodejs והיא אמורה להיפתח עם הקבצים הבאים, כפי שמוצג בהמשך:

7dbe126ee112266d.png

עכשיו צריך להגדיר את משתני הסביבה שבהם נשתמש. לוחצים על הקובץ env-template ותוכלו לראות את התוכן כמו שמוצג בהמשך:

PROJECT_ID=<YOUR_GOOGLE_CLOUD_PROJECT_ID>
LOCATION=us-<GOOGLE_CLOUD_REGION_NAME>
GEMINI_MODEL_NAME=<GEMINI_MODEL_NAME>
EMBEDDING_MODEL_NAME=<GEMINI_EMBEDDING_MODEL_NAME>
IMAGE_GENERATION_MODEL_NAME=<IMAGEN_MODEL_NAME>
DATABASE=<FIRESTORE_DATABASE_NAME>
COLLECTION=<FIRESTORE_COLLECTION_NAME>
TEST_COLLECTION=test-poses
TOP_K=3

יש לעדכן את הערכים של PROJECT_ID ושל LOCATION בהתאם לבחירה שלך בזמן יצירת הפרויקט ב-Google Cloud ואזור מסד הנתונים של Firestore. באופן אידיאלי, הערכים של LOCATION צריכים להיות זהים בפרויקט Google Cloud ובמסד הנתונים של Firestore, למשל us-central1.

לצורך הקודלאב הזה, נשתמש בערכים הבאים (חוץ מ-PROJECT_ID ו-LOCATION, שצריך להגדיר בהתאם להגדרות שלכם.

PROJECT_ID=<YOUR_GOOGLE_CLOUD_PROJECT_ID>
LOCATION=us-<GOOGLE_CLOUD_REGION_NAME>
GEMINI_MODEL_NAME=gemini-1.5-flash-002
EMBEDDING_MODEL_NAME=text-embedding-004
IMAGE_GENERATION_MODEL_NAME=imagen-3.0-fast-generate-001
DATABASE=(default)
COLLECTION=poses
TEST_COLLECTION=test-poses
TOP_K=3

עליך לשמור את הקובץ הזה בתור .env באותה תיקייה שבה נמצא הקובץ env-template.

עוברים לתפריט הראשי בפינה הימנית העליונה ב-Cloud Shell IDE ואז לוחצים על Terminal → New Terminal.

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

cd yoga-poses-recommender-nodejs

מתקינים את יחסי התלות של Node.js באמצעות הפקודה:

npm install

מצוין! עכשיו אנחנו מוכנים להמשיך למשימה של הגדרת מסד הנתונים של Firestore.

3. הגדרת Firestore

Cloud Firestore הוא מסד נתונים מנוהל למסמכים ללא שרת (serverless), שבו נשתמש כקצה עורפי לנתוני האפליקציה שלנו. הנתונים ב-Cloud Firestore מאורגנים בקולקציות של מסמכים.

הפעלה של מסד נתונים ב-Firestore

נכנסים לדף Firestore במסוף Cloud.

אם עדיין לא ביצעתם את האיפוס של מסד הנתונים של Firestore בפרויקט, צריך ליצור את מסד הנתונים default בלחיצה על Create Database. במהלך יצירת מסד הנתונים, צריך להשתמש בערכים הבאים:

  • מצב Firestore: Native.
  • מיקום: משתמשים בהגדרות ברירת המחדל של המיקום.
  • עבור כללי האבטחה, בוחרים באפשרות Test rules.
  • יוצרים את מסד הנתונים.

504cabdb99a222a5.png

בקטע הבא נגדיר את הבסיס ליצירת אוסף בשם poses במסד הנתונים שמוגדר כברירת מחדל ב-Firestore. האוסף הזה יכיל נתונים לדוגמה (מסמכים) או מידע על תנוחות יוגה, שבהם נשתמש באפליקציה שלנו.

זהו סוף הקטע של הגדרת מסד הנתונים של Firestore.

4. הכנת מערך הנתונים של תנוחות יוגה

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

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

298cfae7f23e4bef.png

אם נעבור לקטע Files and versions, נוכל לקבל את קובץ הנתונים בפורמט JSON של כל תנוחות הישיבה.

3fe6e55abdc032ec.png

הורדנו את yoga_poses.json ושלחנו לך את הקובץ הזה. שם הקובץ הוא yoga_poses_alldata.json והוא נמצא בתיקייה /data.

עוברים לקובץ data/yoga_poses.json בעורך של Cloud Shell ומעיינים ברשימת אובייקטי ה-JSON, שבהם כל אובייקט JSON מייצג תנוחת יוגה. יש לנו סך של 3 רשומות, ורשומת לדוגמה מוצגת בהמשך:

{
   "name": "Big Toe Pose",
   "sanskrit_name": "Padangusthasana",
   "photo_url": "https://pocketyoga.com/assets/images/full/ForwardBendBigToe.png",
   "expertise_level": "Beginner",
   "pose_type": ["Standing", "Forward Bend"]
 }

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

ב-Cloud Shell Editor, עוברים לקובץ generate-descriptions.js. התוכן של הקובץ הזה מוצג בהמשך:

import { VertexAI } from "@langchain/google-vertexai";
import fs from 'fs/promises'; // Use fs/promises for async file operations
import dotenv from 'dotenv';
import pRetry from 'p-retry';
import { promisify } from 'util';

const sleep = promisify(setTimeout);

// Load environment variables
dotenv.config();

async function callGemini(poseName, sanskritName, expertiseLevel, poseTypes) {

   const prompt = `
   Generate a concise description (max 50 words) for the yoga pose: ${poseName}
   Also known as: ${sanskritName}
   Expertise Level: ${expertiseLevel}
   Pose Type: ${poseTypes.join(', ')}

   Include key benefits and any important alignment cues.
   `;

   try {
     // Initialize Vertex AI Gemini model
     const model = new VertexAI({
       model: process.env.GEMINI_MODEL_NAME,
       location: process.env.LOCATION,
       project: process.env.PROJECT_ID,
     });
      // Invoke the model
     const response = await model.invoke(prompt);
      // Return the response
     return response;
   } catch (error) {
     console.error("Error calling Gemini:", error);
     throw error; // Re-throw the error for handling in the calling function
   }
 }

// Configure logging (you can use a library like 'winston' for more advanced logging)
const logger = {
 info: (message) => console.log(`INFO - ${new Date().toISOString()} - ${message}`),
 error: (message) => console.error(`ERROR - ${new Date().toISOString()} - ${message}`),
};

async function generateDescription(poseName, sanskritName, expertiseLevel, poseTypes) {
 const prompt = `
   Generate a concise description (max 50 words) for the yoga pose: ${poseName}
   Also known as: ${sanskritName}
   Expertise Level: ${expertiseLevel}
   Pose Type: ${poseTypes.join(', ')}

   Include key benefits and any important alignment cues.
   `;

 const req = {
   contents: [{ role: 'user', parts: [{ text: prompt }] }],
 };

 const runWithRetry = async () => {
   const resp = await generativeModel.generateContent(req);
   const response = await resp.response;
   const text = response.candidates[0].content.parts[0].text;
   return text;
 };

 try {
   const text = await pRetry(runWithRetry, {
     retries: 5,
     onFailedAttempt: (error) => {
       logger.info(
         `Attempt ${error.attemptNumber} failed. There are ${error.retriesLeft} retries left. Waiting ${error.retryDelay}ms...`
       );
     },
     minTimeout: 4000, // 4 seconds (exponential backoff will adjust this)
     factor: 2, // Exponential factor
   });
   return text;
 } catch (error) {
   logger.error(`Error generating description for ${poseName}: ${error}`);
   return '';
 }
}

async function addDescriptionsToJSON(inputFile, outputFile) {
 try {
   const data = await fs.readFile(inputFile, 'utf-8');
   const yogaPoses = JSON.parse(data);

   const totalPoses = yogaPoses.length;
   let processedCount = 0;

   for (const pose of yogaPoses) {
     if (pose.name !== ' Pose') {
       const startTime = Date.now();
       pose.description = await callGemini(
         pose.name,
         pose.sanskrit_name,
         pose.expertise_level,
         pose.pose_type
       );

       const endTime = Date.now();
       const timeTaken = (endTime - startTime) / 1000;
       processedCount++;
       logger.info(`Processed: ${processedCount}/${totalPoses} - ${pose.name} (${timeTaken.toFixed(2)} seconds)`);
     } else {
       pose.description = '';
       processedCount++;
       logger.info(`Processed: ${processedCount}/${totalPoses} - ${pose.name} (${timeTaken.toFixed(2)} seconds)`);
     }

     // Add a delay to avoid rate limit
     await sleep(30000); // 30 seconds
   }

   await fs.writeFile(outputFile, JSON.stringify(yogaPoses, null, 2));
   logger.info(`Descriptions added and saved to ${outputFile}`);
 } catch (error) {
   logger.error(`Error processing JSON file: ${error}`);
 }
}

async function main() {
 const inputFile = './data/yoga_poses.json';
 const outputFile = './data/yoga_poses_with_descriptions.json';

 await addDescriptionsToJSON(inputFile, outputFile);
}

main();

האפליקציה הזו תוסיף שדה description חדש לכל רשומת JSON של תנוחת יוגה. התיאור יתקבל באמצעות קריאה למודל Gemini, שבו נציג לו את ההנחיה הנדרשת. השדה מתווסף לקובץ ה-JSON והקובץ החדש נכתב לקובץ data/yoga_poses_with_descriptions.json.

הנה השלבים העיקריים:

  1. בפונקציה main(), תוכלו לראות שהיא מפעילה את הפונקציה add_descriptions_to_json ומספקת את קובץ הקלט ואת קובץ הפלט הצפויים.
  2. הפונקציה add_descriptions_to_json מבצעת את הפעולות הבאות לכל רשומת JSON, כלומר פרטי הפוסט בנושא יוגה:
  3. הוא מחלץ את pose_name, sanskrit_name, expertise_level ו-pose_types.
  4. הפונקציה מפעילה את הפונקציה callGemini שמרכיבת הנחיה, ואז מפעילה את הכיתה של מודל LangchainVertexAI כדי לקבל את טקסט התשובה.
  5. לאחר מכן, טקסט התגובה הזה מתווסף לאובייקט ה-JSON.
  6. לאחר מכן, רשימת האובייקטים המעודכנת בפורמט JSON נכתבת בקובץ היעד.

נבקש ממך להריץ את האפליקציה הזו. פותחים חלון מסוף חדש (Ctrl+Shift+C) ומריצים את הפקודה הבאה:

npm run generate-descriptions

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

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

בהמשך מוצגת דוגמה להרצה מתמשכת:

469ede91ba007c1f.png

אחרי שכל 3 הרשומות ישופרו באמצעות קריאת Gemini, ייווצר קובץ data/yoga_poses_with_description.json. אפשר לעיין בזה.

עכשיו אנחנו מוכנים עם קובץ הנתונים, והשלב הבא הוא להבין איך לאכלס איתו מסד נתונים של Firestore, יחד עם יצירת הטמעות (embeddings).

5. ייבוא נתונים ל-Firestore ויצירת הטמעות של וקטורים

יש לנו את הקובץ data/yoga_poses_with_description.json ועכשיו אנחנו צריכים לאכלס את מסד הנתונים של Firestore באמצעותו, וחשוב גם ליצור את הטמעות הווקטורים לכל אחד מהרשומות. הטמעות הווקטורים יהיו שימושיות בשלב מאוחר יותר, כשנצטרך לבצע חיפוש של דמיון ביניהם לבין שאילתת המשתמש שסופקה בשפה טבעית.

כדי לעשות זאת, צריך לפעול לפי השלבים הבאים:

  1. נעביר את רשימת אובייקטי ה-JSON לרשימת אובייקטים. לכל מסמך יהיו שני מאפיינים: content ו-metadata. אובייקט המטא-נתונים יכיל את אובייקט ה-JSON כולו, עם מאפיינים כמו name, description, sanskrit_name וכו'. השדה content יהיה מחרוזת טקסט שתהיה שרשור של כמה שדות.
  2. אחרי שנקבל רשימה של מסמכים, נשתמש בכיתה Vertex AI Embeddings כדי ליצור את הטמעת הנתונים (embedding) בשדה התוכן. ההטמעה הזו תתווסף לכל רשומת מסמך, ואז נשתמש ב-Firestore API כדי לשמור את הרשימה הזו של אובייקטי המסמכים באוסף (אנחנו משתמשים במשתנה TEST_COLLECTION שמצביע על test-poses).

הקוד של import-data.js מופיע בהמשך (חלקים מהקוד קוצרו כדי לקצר את הטקסט):

import { Firestore,
        FieldValue,
} from '@google-cloud/firestore';
import { VertexAIEmbeddings } from "@langchain/google-vertexai";
import * as dotenv from 'dotenv';
import fs from 'fs/promises';

// Load environment variables
dotenv.config();

// Configure logging
const logger = {
 info: (message) => console.log(`INFO - ${new Date().toISOString()} - ${message}`),
 error: (message) => console.error(`ERROR - ${new Date().toISOString()} - ${message}`),
};

async function loadYogaPosesDataFromLocalFile(filename) {
 try {
   const data = await fs.readFile(filename, 'utf-8');
   const poses = JSON.parse(data);
   logger.info(`Loaded ${poses.length} poses.`);
   return poses;
 } catch (error) {
   logger.error(`Error loading dataset: ${error}`);
   return null;
 }
}

function createFirestoreDocuments(poses) {
 const documents = [];
 for (const pose of poses) {
   // Convert the pose to a string representation for pageContent
   const pageContent = `
name: ${pose.name || ''}
description: ${pose.description || ''}
sanskrit_name: ${pose.sanskrit_name || ''}
expertise_level: ${pose.expertise_level || 'N/A'}
pose_type: ${pose.pose_type || 'N/A'}
   `.trim();

   // The metadata will be the whole pose
   const metadata = pose;
   documents.push({ pageContent, metadata });
 }
 logger.info(`Created ${documents.length} Langchain documents.`);
 return documents;
}

async function main() {
 const allPoses = await loadYogaPosesDataFromLocalFile('./data/yoga_poses_with_descriptions.json');
 const documents = createFirestoreDocuments(allPoses);
 logger.info(`Successfully created Firestore documents. Total documents: ${documents.length}`);

 const embeddings = new VertexAIEmbeddings({
   model: process.env.EMBEDDING_MODEL_NAME,
 });
  // Initialize Firestore
 const firestore = new Firestore({
   projectId: process.env.PROJECT_ID,
   databaseId: process.env.DATABASE,
 });

 const collectionName = process.env.TEST_COLLECTION;

 for (const doc of documents) {
   try {
     // 1. Generate Embeddings
     const singleVector = await embeddings.embedQuery(doc.pageContent);

     // 2. Store in Firestore with Embeddings
     const firestoreDoc = {
       content: doc.pageContent,
       metadata: doc.metadata, // Store the original data as metadata
       embedding: FieldValue.vector(singleVector), // Add the embedding vector
     };

     const docRef = firestore.collection(collectionName).doc();
     await docRef.set(firestoreDoc);
     logger.info(`Document ${docRef.id} added to Firestore with embedding.`);
   } catch (error) {
     logger.error(`Error processing document: ${error}`);
   }
 }

 logger.info('Finished adding documents to Firestore.');
}

main();

נבקש ממך להריץ את האפליקציה הזו. פותחים חלון מסוף חדש (Ctrl+Shift+C) ומריצים את הפקודה הבאה:

npm run import-data

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

INFO - 2025-01-28T07:01:14.463Z - Loaded 3 poses.
INFO - 2025-01-28T07:01:14.464Z - Created 3 Langchain documents.
INFO - 2025-01-28T07:01:14.464Z - Successfully created Firestore documents. Total documents: 3
INFO - 2025-01-28T07:01:17.623Z - Document P46d5F92z9FsIhVVYgkd added to Firestore with embedding.
INFO - 2025-01-28T07:01:18.265Z - Document bjXXISctkXl2ZRSjUYVR added to Firestore with embedding.
INFO - 2025-01-28T07:01:19.285Z - Document GwzZMZyPfTLtiX6qBFFz added to Firestore with embedding.
INFO - 2025-01-28T07:01:19.286Z - Finished adding documents to Firestore.

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

504cabdb99a222a5.png

לוחצים על מסד הנתונים (ברירת המחדל), שבו אמורה להופיע האוסף test-poses ומספר מסמכים באוסף הזה. כל מסמך הוא תנוחת יוגה אחת.

9f37aa199c4b547a.png

לוחצים על אחד מהמסמכים כדי לבדוק את השדות. בנוסף לשדות שייבאנו, יופיע גם השדה embedding, שהוא שדה וקטור. הערך שלו נוצר באמצעות מודל ההטמעה (Embedding) של Vertex AI‏ text-embedding-004.

f0ed92124519beaf.png

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

6. ייבוא תנוחות יוגה מלאות לאוסף של מסדי נתונים ב-Firestore

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

מייבאים את מסד הנתונים לפי השלבים הבאים:

  1. יוצרים קטגוריה בפרויקט באמצעות הפקודה gsutil שמופיעה בהמשך. מחליפים את המשתנה <PROJECT_ID> בפקודה הבאה במזהה הפרויקט ב-Google Cloud.
gsutil mb -l us-central1 gs://<PROJECT_ID>-my-bucket
  1. עכשיו, אחרי שיצרנו את הקטגוריה, אנחנו צריכים להעתיק אליה את הייצוא של מסד הנתונים שהכינו, כדי שנוכל לייבא אותו למסד הנתונים של Firebase. משתמשים בפקודה הבאה:
gsutil cp -r gs://yoga-database-firestore-export-bucket/2025-01-27T05:11:02_62615  gs://<PROJECT_ID>-my-bucket

עכשיו שיש לנו את הנתונים לייבוא, אנחנו יכולים לעבור לשלב האחרון של ייבוא הנתונים למסד הנתונים של Firebase (default) שיצרנו.

  1. משתמשים בפקודה הבאה של gcloud:
gcloud firestore import gs://<PROJECT_ID>-my-bucket/2025-01-27T05:11:02_62615

הייבוא יימשך כמה שניות. בסיום, תוכלו לאמת את מסד הנתונים ואת הקולקציה ב-Firestore. לשם כך, עוברים אל https://console.cloud.google.com/firestore/databases, בוחרים את מסד הנתונים default ואת הקולקציה poses כפי שמתואר בהמשך:

561f3cb840de23d8.png

זהו, סיימנו ליצור את האוסף ב-Firestore שבו נשתמש באפליקציה.

7. ביצוע חיפוש דמיון וקטורי ב-Firestore

כדי לבצע חיפוש דמיון וקטורי, נקבל את השאילתה מהמשתמש. דוגמה לשאילתה הזו יכולה להיות "Suggest me some exercises to relieve back pain".

כדאי לעיין בקובץ search-data.js. הפונקציה העיקרית שצריך לבדוק היא הפונקציה search שמוצגת בהמשך. באופן כללי, הקוד יוצר סוג הטמעה (embedding) שמשמש ליצירת הטמעה לשאילתת המשתמש. לאחר מכן הוא יוצר חיבור למסד הנתונים ולאוסף של Firestore. לאחר מכן, הוא מפעיל את השיטה findNearest באוסף, שמבצעת חיפוש דמיון וקטורי.

async function search(query) {
 try {

   const embeddings = new VertexAIEmbeddings({
       model: process.env.EMBEDDING_MODEL_NAME,
     });
  
   // Initialize Firestore
   const firestore = new Firestore({
       projectId: process.env.PROJECT_ID,
       databaseId: process.env.DATABASE,
   });

   log.info(`Now executing query: ${query}`);
   const singleVector = await embeddings.embedQuery(query);

   const collectionRef = firestore.collection(process.env.COLLECTION);
   let vectorQuery = collectionRef.findNearest(
   "embedding",
   FieldValue.vector(singleVector), // a vector with 768 dimensions
   {
       limit: process.env.TOP_K,
       distanceMeasure: "COSINE",
   }
   );
   const vectorQuerySnapshot = await vectorQuery.get();

   for (const result of vectorQuerySnapshot.docs) {
     console.log(result.data().content);
   }
 } catch (error) {
   log.error(`Error during search: ${error.message}`);
 }
}

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

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

gcloud firestore indexes composite create --project=<YOUR_PROJECT_ID> --collection-group=poses --query-scope=COLLECTION --field-config=vector-config='{"dimension":"768","flat": "{}"}',field-path=embedding

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

gcloud firestore indexes composite list

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

נסו עכשיו את הפקודה הבאה:

node search-data.js --prompt "Recommend me some exercises for back pain relief"

אמורות להופיע כמה המלצות. הנה דוגמה להרצה:

2025-01-28T07:09:05.250Z - INFO - Now executing query: Recommend me some exercises for back pain relief
name: Sphinx Pose
description: A gentle backbend, Sphinx Pose (Salamba Bhujangasana) strengthens the spine and opens the chest.  Keep shoulders relaxed, lengthen the tailbone, and engage the core for optimal alignment. Beginner-friendly.

sanskrit_name: Salamba Bhujangasana
expertise_level: Beginner
pose_type: ['Prone']
name: Supine Spinal Twist Pose
description: A gentle supine twist (Supta Matsyendrasana), great for beginners.  Releases spinal tension, improves digestion, and calms the nervous system.  Keep shoulders flat on the floor and lengthen your spine throughout the twist.

sanskrit_name: Supta Matsyendrasana
expertise_level: Beginner
pose_type: ['Supine', 'Twist']
name: Reverse Corpse Pose
description: Reverse Corpse Pose (Advasana) is a beginner prone pose.  Lie on your belly, arms at your sides, relaxing completely.  Benefits include stress release and spinal decompression. Ensure your forehead rests comfortably on the mat.

sanskrit_name: Advasana
expertise_level: Beginner
pose_type: ['Prone']

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

8. אפליקציית האינטרנט

אפליקציית האינטרנט של Python Flask זמינה בקובץ app.js וקובץ ה-HTML של הקצה הקדמי נמצא בקובץ views/index.html.

מומלץ לבדוק את שני הקבצים. מתחילים קודם עם הקובץ app.js שמכיל את הטיפול /search, שמקבל את ההנחיה שהועברה מקובץ ה-HTML index.html של הקצה הקדמי. לאחר מכן, המערכת מפעילה את שיטת החיפוש, שמבצעת את החיפוש לפי דמיון וקטורים שבדקנו בקטע הקודם.

לאחר מכן, התשובה נשלחת חזרה אל index.html עם רשימת ההמלצות. לאחר מכן, ההמלצות יוצגו בכרטיסים שונים בindex.html.

הרצת האפליקציה באופן מקומי

פותחים חלון מסוף חדש (Ctrl+Shift+C) או חלון מסוף קיים ומזינים את הפקודה הבאה:

npm run start

דוגמה להרצה:

...
Server listening on port 8080

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

de297d4cee10e0bf.png

אמור להופיע קובץ index.html שמוצג, כפי שמוצג בהמשך:

20240a0e885ac17b.png

מזינים שאילתה לדוגמה (לדוגמה : Provide me some exercises for back pain relief) ולוחצים על הלחצן Search. הפעולה הזו אמורה לאחזר כמה המלצות מהמסד הנתונים. יופיע גם הלחצן Play Audio, שיוצר שידור אודיו על סמך התיאור, שאפשר לשמוע ישירות.

789b4277dc40e2be.png

9. (אופציונלי) פריסת האפליקציה ב-Google Cloud Run

השלב האחרון יהיה פריסת האפליקציה הזו ב-Google Cloud Run. פקודת הפריסה מוצגת בהמשך. לפני הפריסה, חשוב להחליף את הערכים שמוצגים מודגש בהמשך. אלה ערכים שתוכלו לאחזר מהקובץ .env.

gcloud run deploy yogaposes --source . \
  --port=8080 \
  --allow-unauthenticated \
  --region=<<YOUR_LOCATION>> \
  --platform=managed  \
  --project=<<YOUR_PROJECT_ID>> \
--set-env-vars=PROJECT_ID="<<YOUR_PROJECT_ID>>",LOCATION="<<YOUR_LOCATION>>",EMBEDDING_MODEL_NAME="<<EMBEDDING_MODEL_NAME>>",DATABASE="<<FIRESTORE_DATABASE_NAME>>",COLLECTION="<<FIRESTORE_COLLECTION_NAME>>",TOP_K=<<YOUR_TOP_K_VALUE>>

מריצים את הפקודה שלמעלה מהתיקייה ברמה הבסיסית (root) של האפליקציה. יכול להיות שתתבקשו גם להפעיל את ממשקי Google Cloud API, לאשר הרשאות שונות ולספק אישור.

תהליך הפריסה יימשך כ-5 עד 7 דקות, לכן יש להמתין.

3a6d86fd32e4a5e.png

בסיום הפריסה, כתובת ה-URL של שירות Cloud Run תופיע בפלט של הפריסה. הפורמט שלו יהיה:

Service URL: https://yogaposes-<UNIQUEID>.us-central1.run.app

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

84e1cbf29cbaeedc.png

אפשר גם להיכנס אל Cloud Run דרך מסוף Google Cloud, ותראו את רשימת השירותים ב-Cloud Run. השירות yogaposes צריך להיות אחד מהשירותים (אם לא היחיד) שמפורטים שם.

f2b34a8c9011be4c.png

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

faaa5e0c02fe0423.png

זהו, סיימנו את הפיתוח והפריסה של אפליקציית האינטרנט שלנו להמלצות על תנוחות יוגה ב-Cloud Run.

10. מזל טוב

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

מסמכי עזר