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

1. บทนำ

ใน Codelab นี้ คุณจะได้สร้างแอปพลิเคชันที่ใช้ Vector Search เพื่อแนะนำท่าโยคะ

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

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

84e1cbf29cbaeedc.png

สิ่งที่คุณต้องทำ

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

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

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

สิ่งที่คุณต้องมี

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

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 ในเอกสารประกอบ

โคลนที่เก็บและตั้งค่าสภาพแวดล้อม

ขั้นตอนถัดไปคือการโคลนที่เก็บตัวอย่างที่เราจะอ้างอิงในส่วนที่เหลือของ Codelab สมมติว่าคุณอยู่ใน 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

สำหรับ Codelab นี้ เราจะใช้ค่าต่อไปนี้ (ยกเว้น 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 Console

หากยังไม่เคยเริ่มต้นใช้งานฐานข้อมูล Firestore ในโปรเจ็กต์ ให้สร้างฐานข้อมูล default โดยคลิก Create Database ในระหว่างการสร้างฐานข้อมูล ให้ใช้ค่าต่อไปนี้

  • โหมด Firestore: Native.
  • เลือกประเภทสถานที่ตั้งเป็น Region แล้วเลือกสถานที่ตั้ง us-central1 สำหรับภูมิภาค
  • สำหรับกฎความปลอดภัย ให้ใช้ Test rules
  • สร้างฐานข้อมูล

61d0277510803c8d.png

ในส่วนถัดไป เราจะวางรากฐานสำหรับการสร้างคอลเล็กชันชื่อ poses ในฐานข้อมูล Firestore เริ่มต้น คอลเล็กชันนี้จะเก็บข้อมูลตัวอย่าง (เอกสาร) หรือข้อมูลท่าโยคะ ซึ่งเราจะนำไปใช้ในแอปพลิเคชัน

เพียงเท่านี้ก็ตั้งค่าฐานข้อมูล Firestore เสร็จเรียบร้อย

4. เตรียมชุดข้อมูลท่าโยคะ

งานแรกของเราคือการเตรียมชุดข้อมูลท่าโยคะที่เราจะใช้สำหรับแอปพลิเคชัน เราจะเริ่มต้นด้วยชุดข้อมูล Hugging Face ที่มีอยู่ แล้วปรับปรุงด้วยข้อมูลเพิ่มเติม

ดูชุดข้อมูล Hugging Face สำหรับท่าโยคะ โปรดทราบว่าแม้ว่า Codelab นี้จะใช้ชุดข้อมูลชุดหนึ่ง แต่คุณก็สามารถใช้ชุดข้อมูลอื่นๆ และทำตามเทคนิคเดียวกันที่แสดงไว้เพื่อปรับปรุงชุดข้อมูลได้

298cfae7f23e4bef.png

หากไปที่ส่วน Files and versions เราจะได้รับไฟล์ข้อมูล JSON สำหรับท่าทางทั้งหมด

3fe6e55abdc032ec.png

เราได้ดาวน์โหลด yoga_poses.json และส่งไฟล์ดังกล่าวให้คุณแล้ว ไฟล์นี้ชื่อ yoga_poses_alldata.json และอยู่ในโฟลเดอร์ /data

ไปที่ไฟล์ data/yoga_poses.json ใน Cloud Shell Editor แล้วดูรายการออบเจ็กต์ 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 พร้อมกับการสร้างการฝัง

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

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

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

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

504cabdb99a222a5.png

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

9f37aa199c4b547a.png

คลิกเอกสารใดก็ได้เพื่อตรวจสอบฟิลด์ นอกจากฟิลด์ที่เรานําเข้าแล้ว คุณจะเห็นฟิลด์ embedding ซึ่งเป็นฟิลด์เวกเตอร์ที่มีค่าที่เราสร้างขึ้นผ่านโมเดลการฝัง text-embedding-004 Vertex AI

f0ed92124519beaf.png

ตอนนี้เราได้อัปโหลดบันทึกลงในฐานข้อมูล Firestore พร้อมฝังแล้ว เราสามารถไปยังขั้นตอนถัดไปและดูวิธีทำการค้นหาความคล้ายกันของเวกเตอร์ใน Firestore ได้

6. นำเข้าท่าโยคะทั้งหมดไปยังคอลเล็กชันฐานข้อมูล Firestore

ตอนนี้เราจะสร้างคอลเล็กชัน poses ซึ่งเป็นรายการท่าโยคะทั้งหมด 160 ท่า โดยเราได้สร้างไฟล์นำเข้าฐานข้อมูลที่คุณนำเข้าได้โดยตรง การดำเนินการนี้จะช่วยประหยัดเวลาในห้องทดลอง กระบวนการสร้างฐานข้อมูลที่มีคำอธิบายและการฝังจะเหมือนกับที่เราเห็นในส่วนก่อนหน้า

นำเข้าฐานข้อมูลโดยทำตามขั้นตอนด้านล่าง

  1. สร้าง Bucket ในโปรเจ็กต์ด้วยคำสั่ง gsutil ที่ระบุไว้ด้านล่าง แทนที่ตัวแปร <PROJECT_ID> ในคำสั่งด้านล่างด้วยรหัสโปรเจ็กต์ Google Cloud
gsutil mb -l us-central1 gs://<PROJECT_ID>-my-bucket
  1. เมื่อสร้าง Bucket แล้ว เราต้องคัดลอกการส่งออกฐานข้อมูลที่เราเตรียมไว้ลงใน Bucket นี้ก่อนจึงจะนำเข้าไปยังฐานข้อมูล 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 ซึ่งแสดงอยู่ด้านล่าง โดยทั่วไปแล้ว จะสร้างคลาสการฝังที่จะใช้ในการสร้างการฝังสำหรับคำค้นหาของผู้ใช้ จากนั้นจะสร้างการเชื่อมต่อกับฐานข้อมูลและคอลเล็กชัน 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 ซึ่งรับพรอมต์ที่ส่งจากไฟล์ index.html 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 สร้างการฝัง และทำการค้นหาความคล้ายกันของเวกเตอร์ตามคำค้นหาของผู้ใช้ได้สำเร็จ

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