Codelab - สร้างแอปแนะนำท่าโยคะตามบริบทด้วย Firestore, Vector Search, Langchain และ Gemini (เวอร์ชัน Node.js)

1. บทนำ

ในโค้ดแล็บนี้ คุณจะได้สร้างแอปพลิเคชันที่ใช้การค้นหาเวกเตอร์เพื่อแนะนำท่าโยคะ

คุณจะใช้แนวทางแบบทีละขั้นตอนต่อไปนี้ผ่าน Codelab

  1. ใช้ชุดข้อมูลท่าโยคะ (รูปแบบ JSON) ที่มีอยู่ของ Hugging Face
  2. ปรับปรุงชุดข้อมูลด้วยคำอธิบายช่องเพิ่มเติมที่ใช้ Gemini เพื่อสร้างคำอธิบายสำหรับท่าทางแต่ละท่า
  3. โหลดข้อมูลท่าโยคะเป็นคอลเล็กชันเอกสารในคอลเล็กชัน Firestore ที่มีข้อมูลฝังที่สร้างขึ้น
  4. สร้างดัชนีผสมใน Firestore เพื่ออนุญาตการค้นหาเวกเตอร์
  5. ใช้การค้นหาเวกเตอร์ในแอปพลิเคชัน Node.js ที่รวมทุกอย่างเข้าด้วยกันดังที่แสดงด้านล่าง

84e1cbf29cbaeedc.png

สิ่งที่ต้องทำ

  • ออกแบบ สร้าง และทำให้เว็บแอปพลิเคชันที่ใช้การค้นหาเวกเตอร์เพื่อแนะนำท่าโยคะใช้งานได้

สิ่งที่คุณจะได้เรียนรู้

  • วิธีใช้ Gemini เพื่อสร้างเนื้อหาข้อความและสร้างคำอธิบายสำหรับท่าโยคะในบริบทของโค้ดแล็บนี้
  • วิธีโหลดระเบียนจากชุดข้อมูลที่ปรับปรุงแล้วจาก Hugging Face ลงใน Firestore พร้อมกับการฝังเวกเตอร์
  • วิธีใช้การค้นหาเวกเตอร์ของ Firestore เพื่อค้นหาข้อมูลตามการค้นหาด้วยภาษาที่เป็นธรรมชาติ
  • วิธีใช้ Google Cloud Text to Speech API เพื่อสร้างเนื้อหาเสียง

สิ่งที่ต้องมี

  • เว็บเบราว์เซอร์ Chrome
  • บัญชี Gmail
  • โปรเจ็กต์ที่อยู่ในระบบคลาวด์ที่เปิดใช้การเรียกเก็บเงิน

Codelab นี้ออกแบบมาสำหรับนักพัฒนาซอฟต์แวร์ทุกระดับ (รวมถึงผู้เริ่มต้น) โดยใช้ JavaScript และ Node.js ในแอปพลิเคชันตัวอย่าง อย่างไรก็ตาม คุณไม่จำเป็นต้องมีความรู้เกี่ยวกับ JavaScript และ Node.js เพื่อทําความเข้าใจแนวคิดที่นำเสนอ

2. ก่อนเริ่มต้น

สร้างโปรเจ็กต์

  1. ในคอนโซล Google Cloud ให้เลือกหรือสร้างโปรเจ็กต์ Google Cloud ในหน้าตัวเลือกโปรเจ็กต์
  2. ตรวจสอบว่าเปิดใช้การเรียกเก็บเงินสำหรับโปรเจ็กต์ Cloud แล้ว ดูวิธีตรวจสอบว่าเปิดใช้การเรียกเก็บเงินในโปรเจ็กต์หรือไม่
  3. คุณจะใช้ Cloud Shell ซึ่งเป็นสภาพแวดล้อมบรรทัดคำสั่งที่ทำงานใน Google Cloud และโหลด bq ไว้ล่วงหน้า คลิก "เปิดใช้งาน 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

หากต้องการเปิดเครื่องมือแก้ไข ให้คลิก "เปิดเครื่องมือแก้ไข" ในแถบเครื่องมือของหน้าต่าง 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 ในคอนโซลระบบคลาวด์

หากคุณไม่เคยเริ่มต้นฐานข้อมูล 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 ให้ไปที่ไฟล์ 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 พร้อมกับการสร้างการฝัง

5. นําเข้าข้อมูลไปยัง Firestore และสร้างการฝังเวกเตอร์

เรามีไฟล์ data/yoga_poses_with_description.json แล้ว ตอนนี้ต้องป้อนข้อมูลไฟล์ลงในฐานข้อมูล Firestore และที่สำคัญคือสร้างการฝังเวกเตอร์สำหรับแต่ละระเบียน การฝังเวกเตอร์จะมีประโยชน์ในภายหลังเมื่อเราต้องทำการค้นหาความคล้ายคลึงกับคำค้นหาของผู้ใช้ที่ระบุเป็นภาษาธรรมชาติ

ขั้นตอนในการดำเนินการมีดังนี้

  1. เราจะแปลงรายการออบเจ็กต์ JSON เป็นรายการออบเจ็กต์ เอกสารแต่ละรายการจะมีแอตทริบิวต์ 2 รายการ ได้แก่ content และ metadata ออบเจ็กต์ข้อมูลเมตาจะมีออบเจ็กต์ JSON ทั้งหมดที่มีแอตทริบิวต์ เช่น name, description, sanskrit_name เป็นต้น content จะเป็นสตริงข้อความที่ต่อเชื่อมช่องต่างๆ
  2. เมื่อเรามีรายการเอกสารแล้ว เราจะใช้คลาสการฝังของ Vertex AI เพื่อสร้างการฝังสำหรับฟิลด์เนื้อหา ระบบจะเพิ่มการฝังนี้ลงในระเบียนเอกสารแต่ละรายการ จากนั้นเราจะใช้ 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.

หากต้องการตรวจสอบว่าได้แทรกระเบียนและสร้างการฝังเรียบร้อยแล้วหรือไม่ ให้ไปที่หน้า Firestore ในคอนโซลระบบคลาวด์

504cabdb99a222a5.png

คลิกฐานข้อมูล (เริ่มต้น) ซึ่งจะแสดงคอลเล็กชัน test-poses และเอกสารหลายรายการในคอลเล็กชันนั้น เอกสารแต่ละรายการคือท่าโยคะ 1 ท่า

9f37aa199c4b547a.png

คลิกเอกสารใดก็ได้เพื่อตรวจสอบช่อง นอกจากช่องที่เรานําเข้าแล้ว คุณจะเห็นช่อง 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

การนําเข้าจะใช้เวลา 2-3 วินาที เมื่อพร้อมแล้ว คุณสามารถตรวจสอบฐานข้อมูล 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 ซึ่งแสดงอยู่ด้านล่าง ในระดับสูง การดำเนินการนี้จะสร้างคลาสการฝังที่จะใช้สร้างการฝังสําหรับคําค้นหาของผู้ใช้ จากนั้นจะสร้างการเชื่อมต่อกับฐานข้อมูลและคอลเล็กชัน 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}`);
 }
}

ก่อนเรียกใช้ตัวอย่างการค้นหา 2-3 รายการ คุณต้องสร้างดัชนีคอมโพสิท 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"

คุณควรได้รับคําแนะนํา 2-3 รายการ ตัวอย่างการเรียกใช้แสดงอยู่ด้านล่าง

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.

เราขอแนะนำให้คุณดูทั้ง 2 ไฟล์ เริ่มต้นด้วยไฟล์ 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>>

เรียกใช้คําสั่งข้างต้นจากโฟลเดอร์รูทของแอปพลิเคชัน ระบบอาจขอให้คุณเปิดใช้ 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, สร้างการฝัง และทำการค้นหาความคล้ายคลึงของเวกเตอร์ตามการค้นหาของผู้ใช้เรียบร้อยแล้ว

เอกสารอ้างอิง