1. はじめに
この Codelab では、ベクトル検索を使用してヨガのポーズを推奨するアプリケーションを作成します。
この Codelab では、次の手順でアプローチします。
- ヨガのポーズの既存の Hugging Face データセット(JSON 形式)を利用します。
- Gemini を使用して各ポーズの説明を生成する追加のフィールドの説明でデータセットを強化します。
- 生成されたエンベディングを含む Firestore コレクションのドキュメントのコレクションとして、ヨガのポーズのデータを読み込みます。
- Firestore に複合インデックスを作成して、ベクトル検索を可能にします。
- 次のように、すべてをまとめた Node.js アプリケーションでベクトル検索を利用します。

演習内容
- ベクトル検索を使用してヨガのポーズを推奨するウェブ アプリケーションを設計、構築、デプロイします。
学習内容
- Gemini を使用してテキスト コンテンツを生成する方法と、この Codelab のコンテキスト内でヨガのポーズの説明を生成する方法
- Hugging Face の拡張データセットからレコードをベクトル エンベディングとともに Firestore に読み込む方法
- Firestore ベクトル検索を使用して自然言語クエリに基づいてデータを検索する方法
- Google Cloud Text to Speech API を使用して音声コンテンツを生成する方法
必要なもの
- Chrome ウェブブラウザ
- Gmail アカウント
- 課金が有効になっている Cloud プロジェクト
この Codelab は、初心者を含むあらゆるレベルのデベロッパーを対象としており、サンプル アプリケーションで JavaScript と Node.js を使用します。ただし、提示されるコンセプトを理解するために JavaScript と Node.js の知識は必要ありません。
2. 始める前に
プロジェクトを作成する
- Google Cloud コンソールのプロジェクト選択ページで、Google Cloud プロジェクトを選択または作成します。
- Cloud プロジェクトに対して課金が有効になっていることを確認します。詳しくは、プロジェクトで課金が有効になっているかどうかを確認する方法をご覧ください。
- Cloud Shell(Google Cloud で動作するコマンドライン環境)を使用します。この環境には bq がプリロードされています。Google Cloud コンソールの上部にある [Cloud Shell をアクティブにする] をクリックします。
![[Cloud Shell をアクティブにする] ボタンの画像](https://codelabs.developers.google.com/static/yoga-pose-firestore-vectorsearch-nodejs/img/7875ca05ca6f7cab.png?hl=ja)
- Cloud Shell に接続したら、次のコマンドを使用して、すでに認証済みであることと、プロジェクトがプロジェクト ID に設定されていることを確認します。
gcloud auth list
- Cloud Shell で次のコマンドを実行して、gcloud コマンドがプロジェクトを認識していることを確認します。
gcloud config list project
- プロジェクトが設定されていない場合は、次のコマンドを使用して設定します。
gcloud config set project <YOUR_PROJECT_ID>
- 次のコマンドを使用して、必要な 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 ウィンドウのツールバーにある [エディタを開く] をクリックします。左上のメニューバーをクリックし、[ファイル] → [フォルダを開く] を選択します。

yoga-poses-recommender-nodejs フォルダを選択すると、次のようにフォルダが開き、次のファイルが表示されます。

次に、使用する環境変数を設定する必要があります。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_ID と LOCATION の値を更新してください。理想的には、Google Cloud プロジェクトと Firestore データベースの LOCATION の値が同じ(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-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. - [ロケーション タイプ] で
Regionを選択し、リージョンのロケーションとしてus-central1を選択します。 - セキュリティ ルールには
Test rulesを使用します。 - データベースを作成します。

次のセクションでは、デフォルトの Firestore データベースに poses という名前のコレクションを作成するための準備を行います。このコレクションには、アプリケーションで使用するサンプルデータ(ドキュメント)またはヨガポーズの情報が格納されます。
これで、Firestore データベースの設定セクションは完了です。
4. ヨガのポーズのデータセットを準備する
最初のタスクは、アプリケーションで使用するヨガポーズのデータセットを準備することです。既存の Hugging Face データセットから始め、追加情報で強化します。
ヨガのポーズの Hugging Face データセットをご覧ください。この Codelab ではデータセットの 1 つを使用しますが、実際には他のデータセットを使用し、ここで説明する手法に沿ってデータセットを強化することもできます。

Files and versions セクションに移動すると、すべてのポーズの JSON データファイルを取得できます。

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 ファイルに書き込まれます。
主な手順は次のとおりです。
main()関数では、add_descriptions_to_json関数が呼び出され、入力ファイルと出力ファイルが指定されていることがわかります。add_descriptions_to_json関数は、各 JSON レコード(ヨガの投稿情報)に対して次の処理を行います。pose_name、sanskrit_name、expertise_level、pose_typesを抽出します。- プロンプトを作成する
callGemini関数を呼び出し、LangchainVertexAI モデルクラスを呼び出してレスポンス テキストを取得します。 - このレスポンス テキストは、JSON オブジェクトに追加されます。
- 更新された JSON オブジェクトのリストが宛先ファイルに書き込まれます。
このアプリケーションを実行してみましょう。新しいターミナル ウィンドウ(Ctrl+Shift+C)を起動し、次のコマンドを入力します。
npm run generate-descriptions
承認を求められた場合は、承認を行ってください。
アプリケーションの実行が開始されます。新しい Google Cloud アカウントにレート上限の割り当てがある場合に備えて、レコード間に 30 秒の遅延を追加しました。しばらくお待ちください。
以下は実行中のサンプルの例です。

3 つのレコードすべてが Gemini 呼び出しで拡張されると、data/yoga_poses_with_description.json ファイルが生成されます。そちらをご覧ください。
データファイルの準備が整いました。次のステップでは、エンベディングの生成とともに、Firestore データベースにデータを入力する方法について説明します。
5. Firestore にデータをインポートしてベクトル エンベディングを生成する
data/yoga_poses_with_description.json ファイルが用意できたので、このファイルを使用して Firestore データベースにデータを入力し、各レコードのベクトル エンベディングを生成する必要があります。ベクトル エンベディングは、自然言語で指定されたユーザー クエリを使用して類似性検索を行う場合に役立ちます。
手順は次のとおりです。
- JSON オブジェクトのリストをオブジェクトのリストに変換します。各ドキュメントには、
contentとmetadataの 2 つの属性があります。メタデータ オブジェクトには、name、description、sanskrit_nameなどの属性を持つ JSON オブジェクト全体が含まれます。contentは、いくつかのフィールドを連結した文字列テキストになります。 - ドキュメントのリストを取得したら、Vertex AI Embeddings クラスを使用して content フィールドのエンベディングを生成します。このエンベディングは各ドキュメント レコードに追加され、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 ページにアクセスします。

(デフォルトの)データベースをクリックします。これにより、test-poses コレクションと、そのコレクションの下にある複数のドキュメントが表示されます。各ドキュメントは 1 つのヨガのポーズです。

いずれかのドキュメントをクリックして、フィールドを調べます。インポートしたフィールドに加えて、embedding フィールド(ベクトル フィールド)もあります。このフィールドの値は、text-embedding-004 Vertex AI Embedding モデルを使用して生成しました。

これで、エンベディングを含むレコードが Firestore データベースにアップロードされました。次のステップに進み、Firestore でベクトル類似性検索を行う方法を確認しましょう。
6. 完全なヨガのポーズを Firestore データベース コレクションにインポートします。
次に、160 個のヨガポーズの完全なリストである poses コレクションを作成します。このコレクションについては、直接インポートできるデータベース インポート ファイルを生成しました。これは、ラボでの時間を節約するために行います。説明とエンベディングを含むデータベースを生成するプロセスは、前のセクションで説明したプロセスと同じです。
以下の手順に沿ってデータベースをインポートします。
- 次の
gsutilコマンドを使用して、プロジェクトにバケットを作成します。次のコマンドの<PROJECT_ID>変数を、Google Cloud プロジェクト ID に置き換えます。
gsutil mb -l us-central1 gs://<PROJECT_ID>-my-bucket
- バケットが作成されたので、Firebase データベースにインポートする前に、準備したデータベース エクスポートをこのバケットにコピーする必要があります。次のコマンドを使用します。
gsutil cp -r gs://yoga-database-firestore-export-bucket/2025-01-27T05:11:02_62615 gs://<PROJECT_ID>-my-bucket
インポートするデータが用意できたので、作成した Firebase データベース(default)にデータをインポートする最終手順に進みます。
- 次の gcloud コマンドを使用します。
gcloud firestore import gs://<PROJECT_ID>-my-bucket/2025-01-27T05:11:02_62615
インポートには数秒かかります。準備ができたら、https://console.cloud.google.com/firestore/databases にアクセスして Firestore データベースとコレクションを検証します。次に示すように、default データベースと poses コレクションを選択します。

これで、アプリケーションで使用する 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 にアクセスします。

次のように、提供された index.html ファイルが表示されます。

サンプルクエリ(例 : Provide me some exercises for back pain relief)を入力し、[Search] ボタンをクリックします。これにより、データベースからいくつかの推奨事項が取得されます。また、Play Audio ボタンも表示されます。このボタンをクリックすると、説明に基づいて音声ストリームが生成され、直接聞くことができます。

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 APIs を有効にするよう求められたり、さまざまな権限について確認を求められたりすることがあります。その場合は、指示に従ってください。
デプロイ プロセスが完了するまでに 5 ~ 7 分ほどかかりますので、しばらくお待ちください。

デプロイが正常に完了すると、デプロイの出力に Cloud Run サービスの URL が表示されます。形式は次のようになります。
Service URL: https://yogaposes-<UNIQUEID>.us-central1.run.app
そのパブリック URL にアクセスすると、同じウェブ アプリケーションがデプロイされ、正常に実行されていることがわかります。

Google Cloud コンソールから Cloud Run にアクセスして、Cloud Run のサービスリストを表示することもできます。yogaposes サービスは、そこにリストされているサービス(唯一のサービスではない場合)の 1 つである必要があります。

特定のサービス名(この例では yogaposes)をクリックすると、URL、構成、ログなどのサービスの詳細を表示できます。

これで、Cloud Run でのヨガのポーズの Recommender ウェブ アプリケーションの開発とデプロイが完了しました。
10. 完了
おめでとうございます。データセットを Firestore にアップロードし、エンベディングを生成して、ユーザーのクエリに基づいてベクトル類似度検索を行うアプリケーションを構築できました。