Codelab - Firestore, 벡터 검색, Langchain, Gemini를 사용하여 문맥 요가 자세 추천 앱 빌드하기 (Node.js 버전)

1. 소개

이 Codelab에서는 벡터 검색을 사용하여 요가 자세를 추천하는 애플리케이션을 빌드합니다.

이 Codelab에서는 다음과 같이 단계별로 접근합니다.

  1. 요가 자세의 기존 Hugging Face 데이터 세트 (JSON 형식)를 활용합니다.
  2. Gemini를 사용하여 각 포즈에 대한 설명을 생성하는 추가 필드 설명으로 데이터 세트를 개선합니다.
  3. 생성된 임베딩을 사용하여 Firestore 컬렉션에 요가 자세 데이터를 문서 컬렉션으로 로드합니다.
  4. 벡터 검색을 허용하도록 Firestore에 복합 색인을 만듭니다.
  5. 아래와 같이 모든 것을 통합하는 Node.js 애플리케이션에서 벡터 검색을 활용합니다.

84e1cbf29cbaeedc.png

실행할 작업

  • 벡터 검색을 사용하여 요가 자세를 추천하는 웹 애플리케이션을 설계, 빌드, 배포합니다.

학습할 내용

  • Gemini를 사용하여 텍스트 콘텐츠를 생성하고 이 Codelab의 맥락에서 요가 자세에 대한 설명을 생성하는 방법
  • Hugging Face의 향상된 데이터 세트에서 벡터 임베딩과 함께 레코드를 Firestore로 로드하는 방법
  • Firestore 벡터 검색을 사용하여 자연어 쿼리를 기반으로 데이터를 검색하는 방법
  • 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. bq가 미리 로드되어 제공되는 Google Cloud에서 실행되는 명령줄 환경인 Cloud Shell을 사용합니다. Google Cloud 콘솔 상단에서 Cloud Shell 활성화를 클릭합니다.

Cloud Shell 활성화 버튼 이미지

  1. Cloud Shell에 연결되면 다음 명령어를 사용하여 이미 인증되었는지, 프로젝트가 프로젝트 ID로 설정되어 있는지 확인합니다.
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 창의 툴바에서 편집기 열기를 클릭합니다. 왼쪽 상단의 메뉴 바를 클릭하고 아래와 같이 File(파일) → Open Folder(폴더 열기)를 선택합니다.

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

Google Cloud 프로젝트 및 Firestore 데이터베이스 리전을 만들 때 선택한 대로 PROJECT_IDLOCATION의 값을 업데이트합니다. LOCATION의 값이 Google Cloud 프로젝트와 Firestore 데이터베이스에서 동일한 것이 좋습니다(예: us-central1).

이 Codelab에서는 다음 값을 사용합니다. 단, 구성에 따라 설정해야 하는 PROJECT_IDLOCATION은 예외입니다.

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-template 파일과 동일한 폴더에 .env로 저장하세요.

Cloud Shell IDE의 왼쪽 상단에 있는 기본 메뉴로 이동한 다음 Terminal → New Terminal를 클릭합니다.

다음 명령어를 통해 클론한 저장소의 루트 폴더로 이동합니다.

cd yoga-poses-recommender-nodejs

다음 명령어를 통해 Node.js 종속 항목을 설치합니다.

npm install

좋습니다. 이제 Firestore 데이터베이스를 설정하는 작업으로 넘어갈 준비가 되었습니다.

3. Firestore 설정

Cloud Firestore는 애플리케이션 데이터의 백엔드로 사용할 완전 관리형 서버리스 문서 데이터베이스입니다. Cloud Firestore의 데이터는 문서컬렉션으로 구성됩니다.

Firestore 데이터베이스 초기화

Cloud 콘솔에서 Firestore 페이지로 이동합니다.

이전에 프로젝트에서 Firestore 데이터베이스를 초기화하지 않은 경우 Create Database 아이콘을 클릭하여 default 데이터베이스를 만듭니다. 데이터베이스를 만들 때 다음 값을 사용합니다.

  • Firestore 모드: Native.
  • 위치: 기본 위치 설정을 사용합니다.
  • 보안 규칙의 경우 Test rules을 선택합니다.
  • 데이터베이스를 만듭니다.

504cabdb99a222a5.png

다음 섹션에서는 기본 Firestore 데이터베이스에 poses라는 이름의 컬렉션을 만들기 위한 기반을 설정합니다. 이 컬렉션에는 샘플 데이터 (문서) 또는 요가 자세 정보가 저장되며, 이 정보는 애플리케이션에서 사용됩니다.

이제 Firestore 데이터베이스 설정 섹션이 완료되었습니다.

4. 요가 자세 데이터 세트 준비

첫 번째 작업은 애플리케이션에 사용할 요가 자세 데이터 세트를 준비하는 것입니다. 기존 Hugging Face 데이터 세트로 시작하여 추가 정보로 개선해 보겠습니다.

요가 자세용 Hugging Face 데이터 세트를 확인하세요. 이 Codelab에서는 데이터 세트 중 하나를 사용하지만, 실제로는 다른 데이터 세트를 사용하고 설명된 것과 동일한 기법을 따라 데이터 세트를 개선할 수 있습니다.

298cfae7f23e4bef.png

Files and versions 섹션으로 이동하면 모든 포즈의 JSON 데이터 파일을 가져올 수 있습니다.

3fe6e55abdc032ec.png

yoga_poses.json 파일을 다운로드하여 제공해 드렸습니다. 이 파일의 이름은 yoga_poses_alldata.json이며 /data 폴더에 있습니다.

Cloud Shell 편집기에서 data/yoga_poses.json 파일로 이동하여 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();

이 애플리케이션은 각 요가 자세 JSON 레코드에 새 description 필드를 추가합니다. 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

승인을 요청하는 메시지가 표시되면 승인해 주세요.

애플리케이션이 실행되기 시작합니다. 새 Google Cloud 계정에 있을 수 있는 비율 한도 할당량을 방지하기 위해 레코드 간에 30초의 지연을 추가했으니 기다려 주시기 바랍니다.

진행 중인 샘플 실행은 아래와 같습니다.

469ede91ba007c1f.png

Gemini 호출로 세 레코드가 모두 개선되면 data/yoga_poses_with_description.json 파일이 생성됩니다. 확인해 보세요.

이제 데이터 파일이 준비되었으며 다음 단계에서는 임베딩 생성과 함께 Firestore 데이터베이스를 데이터 파일로 채우는 방법을 알아봅니다.

5. Firestore로 데이터 가져오기 및 벡터 임베딩 생성

data/yoga_poses_with_description.json 파일이 있으므로 이제 이 파일로 Firestore 데이터베이스를 채우고 중요한 것은 각 레코드의 벡터 임베딩을 생성해야 합니다. 벡터 임베딩은 나중에 자연어로 제공된 사용자 쿼리로 유사성 검색을 수행해야 할 때 유용합니다.

단계는 다음과 같습니다.

  1. JSON 객체 목록을 객체 목록으로 변환합니다. 각 문서에는 contentmetadata라는 두 가지 속성이 있습니다. 메타데이터 객체에는 name, description, sanskrit_name 등의 속성이 있는 전체 JSON 객체가 포함됩니다. content는 몇 개의 필드를 연결한 문자열 텍스트입니다.
  2. 문서 목록이 있으면 Vertex AI 임베딩 클래스를 사용하여 콘텐츠 필드의 임베딩을 생성합니다. 이 임베딩은 각 문서 레코드에 추가된 후 Firestore API를 사용하여 컬렉션에 이 문서 객체 목록을 저장합니다 (test-poses를 가리키는 TEST_COLLECTION 변수를 사용함).

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.

레코드가 성공적으로 삽입되고 임베딩이 생성되었는지 확인하려면 Cloud 콘솔의 Firestore 페이지로 이동하세요.

504cabdb99a222a5.png

(기본) 데이터베이스를 클릭하면 test-poses 컬렉션과 해당 컬렉션 아래의 여러 문서가 표시됩니다. 각 문서는 하나의 요가 자세입니다.

9f37aa199c4b547a.png

문서를 클릭하여 필드를 조사합니다. 가져온 필드 외에도 벡터 필드인 embedding 필드가 있습니다. 이 필드의 값은 text-embedding-004 Vertex AI 임베딩 모델을 통해 생성되었습니다.

f0ed92124519beaf.png

이제 레코드가 임베딩과 함께 Firestore 데이터베이스에 업로드되었으므로 다음 단계로 이동하여 Firestore에서 벡터 유사성 검색을 실행하는 방법을 살펴보겠습니다.

6. 전체 요가 자세를 Firestore 데이터베이스 컬렉션으로 가져오기

이제 160개의 요가 자세의 전체 목록인 poses 컬렉션을 만들겠습니다. 이 컬렉션에는 직접 가져올 수 있는 데이터베이스 가져오기 파일이 생성되어 있습니다. 이는 실습에서 시간을 절약하기 위한 조치입니다. 설명과 임베딩이 포함된 데이터베이스를 생성하는 프로세스는 이전 섹션에서 본 것과 동일합니다.

아래 단계에 따라 데이터베이스를 가져옵니다.

  1. 아래의 gsutil 명령어를 사용하여 프로젝트에 버킷을 만듭니다. 아래 명령어에서 <PROJECT_ID> 변수를 Google Cloud 프로젝트 ID로 바꿉니다.
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

가져오기에 몇 초가 소요되며 준비가 완료되면 https://console.cloud.google.com/firestore/databases로 이동하여 아래와 같이 default 데이터베이스와 poses 컬렉션을 선택하여 Firestore 데이터베이스와 컬렉션을 확인할 수 있습니다.

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}`);
 }
}

몇 가지 쿼리 예시를 사용하여 실행하기 전에 먼저 검색 쿼리가 성공적으로 실행되기 위해 필요한 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.에 있습니다.

두 파일을 모두 살펴보는 것이 좋습니다. 먼저 프런트엔드 HTML index.html 파일에서 전달된 프롬프트를 사용하는 /search 핸들러가 포함된 app.js 파일로 시작합니다. 그러면 이전 섹션에서 살펴본 벡터 유사성 검색을 실행하는 검색 메서드가 호출됩니다.

그러면 응답이 맞춤 콘텐츠 목록과 함께 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

배포가 완료되면 배포 출력에 Cloud Run 서비스 URL이 표시됩니다. 형식은 다음과 같습니다.

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

이 공개 URL을 방문하면 동일한 웹 애플리케이션이 배포되고 실행되는 것을 볼 수 있습니다.

84e1cbf29cbaeedc.png

Google Cloud 콘솔에서 Cloud Run으로 이동하면 Cloud Run의 서비스 목록이 표시됩니다. yogaposes 서비스가 여기에 나열된 서비스 중 하나여야 합니다 (유일한 서비스가 아닐 수도 있음).

f2b34a8c9011be4c.png

특정 서비스 이름 (이 경우 yogaposes)을 클릭하면 URL, 구성, 로그 등 서비스의 세부정보를 볼 수 있습니다.

faaa5e0c02fe0423.png

이제 Cloud Run에서 요가 자세 추천 웹 애플리케이션을 개발하고 배포했습니다.

10. 축하합니다

축하합니다. 데이터 세트를 Firestore에 업로드하고, 임베딩을 생성하고, 사용자 쿼리를 기반으로 벡터 유사성 검색을 실행하는 애플리케이션을 빌드했습니다.

참조 문서