Codelab: Compila una app contextual de recomendación de posturas de yoga con Firestore, Búsqueda de vectores, Langchain y Gemini (versión de Node.js)

1. Introducción

En este codelab, compilarás una aplicación que use la búsqueda vectorial para recomendar posturas de yoga.

En el codelab, seguirás un enfoque paso a paso de la siguiente manera:

  1. Utiliza un conjunto de datos de poses de yoga existente de Hugging Face (formato JSON).
  2. Mejora el conjunto de datos con una descripción de campo adicional que use Gemini para generar descripciones para cada una de las poses.
  3. Carga los datos de las posturas de yoga como una colección de documentos en la colección de Firestore con incorporaciones generadas.
  4. Crea un índice compuesto en Firestore para permitir la Búsqueda de vectores.
  5. Utiliza la búsqueda vectorial en una aplicación de Node.js que combine todo, como se muestra a continuación:

84e1cbf29cbaeedc.png

Actividades

  • Diseña, compila y, luego, implementa una aplicación web que emplee Vector Search para recomendar posturas de yoga.

Qué aprenderás

  • Cómo usar Gemini para generar contenido de texto y, en el contexto de este codelab, generar descripciones de posturas de yoga
  • Cómo cargar registros de un conjunto de datos mejorado de Hugging Face en Firestore junto con las incorporaciones de vectores
  • Cómo usar la Búsqueda de vectores de Firestore para buscar datos según una consulta de lenguaje natural
  • Cómo usar la API de Text to Speech de Google Cloud para generar contenido de audio

Requisitos

  • Navegador web Chrome
  • Una cuenta de Gmail
  • Un proyecto de Cloud con la facturación habilitada

Este codelab, diseñado para desarrolladores de todos los niveles (incluidos los principiantes), usa JavaScript y Node.js en su aplicación de ejemplo. Sin embargo, no se requiere conocimiento de JavaScript ni Node.js para comprender los conceptos presentados.

2. Antes de comenzar

Crea un proyecto

  1. En la página del selector de proyectos de la consola de Google Cloud, selecciona o crea un proyecto de Google Cloud.
  2. Asegúrate de que la facturación esté habilitada para tu proyecto de Cloud. Obtén información para verificar si la facturación está habilitada en un proyecto .
  3. Usarás Cloud Shell, un entorno de línea de comandos que se ejecuta en Google Cloud y que viene precargado con bq. Haz clic en Activar Cloud Shell en la parte superior de la consola de Google Cloud.

Imagen del botón Activar Cloud Shell

  1. Una vez que te conectes a Cloud Shell, verifica que ya te hayas autenticado y que el proyecto esté configurado con tu ID de proyecto con el siguiente comando:
gcloud auth list
  1. En Cloud Shell, ejecuta el siguiente comando para confirmar que el comando gcloud conoce tu proyecto.
gcloud config list project
  1. Si tu proyecto no está configurado, usa el siguiente comando para hacerlo:
gcloud config set project <YOUR_PROJECT_ID>
  1. Habilita las APIs requeridas con el siguiente comando. Este proceso puede tardar unos minutos, así que ten paciencia.
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

Si el comando se ejecuta correctamente, deberías ver un mensaje similar al siguiente:

Operation "operations/..." finished successfully.

La alternativa al comando gcloud es buscar cada producto en la consola o usar este vínculo.

Si falta alguna API, puedes habilitarla durante el transcurso de la implementación.

Consulta la documentación para ver los comandos y el uso de gcloud.

Clona el repositorio y configura el entorno

El siguiente paso es clonar el repositorio de muestra al que haremos referencia en el resto del codelab. Si estás en Cloud Shell, ingresa el siguiente comando desde tu directorio principal:

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

Para iniciar el editor, haz clic en Abrir editor en la barra de herramientas de la ventana de Cloud Shell. Haz clic en la barra de menú en la esquina superior izquierda y selecciona File → Open Folder, como se muestra a continuación:

66221fd0d0e5202f.png

Selecciona la carpeta yoga-poses-recommender-nodejs y deberías ver que se abre con los siguientes archivos, como se muestra a continuación:

7dbe126ee112266d.png

Ahora debemos configurar las variables de entorno que usaremos. Haz clic en el archivo env-template y deberías ver el contenido como se muestra a continuación:

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

Actualiza los valores de PROJECT_ID y LOCATION según lo que hayas seleccionado cuando creaste el proyecto de Google Cloud y la región de la base de datos de Firestore. Idealmente, nos gustaría que los valores de LOCATION sean los mismos para el proyecto de Google Cloud y la base de datos de Firestore, por ejemplo, us-central1.

Para los fines de este codelab, usaremos los siguientes valores (excepto, por supuesto, PROJECT_ID y LOCATION, que debes configurar según tu configuración).

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

Guarda este archivo como .env en la misma carpeta que el archivo env-template.

Ve al menú principal en la parte superior izquierda del IDE de Cloud Shell y, luego, a Terminal → New Terminal.

Navega a la carpeta raíz del repositorio que clonaste con el siguiente comando:

cd yoga-poses-recommender-nodejs

Instala las dependencias de Node.js con el siguiente comando:

npm install

¡Genial! Ya está todo listo para pasar a la tarea de configurar la base de datos de Firestore.

3. Configura Firestore

Cloud Firestore es una base de datos de documentos sin servidores y completamente administrada que usaremos como backend para los datos de nuestra aplicación. Los datos de Cloud Firestore están estructurados en colecciones de documentos.

Inicialización de la base de datos de Firestore

Visita la página de Firestore en la consola de Cloud.

Si no inicializaste una base de datos de Firestore en el proyecto, crea la base de datos default haciendo clic en Create Database. Durante la creación de la base de datos, usa los siguientes valores:

  • Modo de Firestore: Native.
  • Ubicación: Usa la configuración de ubicación predeterminada.
  • Para las reglas de seguridad, usa Test rules.
  • Crea la base de datos.

504cabdb99a222a5.png

En la siguiente sección, prepararemos los fundamentos para crear una colección llamada poses en nuestra base de datos de Firestore predeterminada. Esta colección contendrá datos de muestra (documentos) o información sobre posturas de yoga, que luego usaremos en nuestra aplicación.

Esto completa la sección de configuración de la base de datos de Firestore.

4. Prepara el conjunto de datos de posturas de yoga

Nuestra primera tarea es preparar el conjunto de datos de posturas de yoga que usaremos para la aplicación. Comenzaremos con un conjunto de datos existente de Hugging Face y, luego, lo mejoraremos con información adicional.

Consulta el conjunto de datos de Hugging Face para posturas de yoga. Ten en cuenta que, si bien este codelab usa uno de los conjuntos de datos, en realidad puedes usar cualquier otro y seguir las mismas técnicas que se muestran para mejorarlo.

298cfae7f23e4bef.png

Si vamos a la sección Files and versions, podemos obtener el archivo de datos JSON de todas las poses.

3fe6e55abdc032ec.png

Descargamos el archivo yoga_poses.json y te lo proporcionamos. Este archivo se llama yoga_poses_alldata.json y se encuentra en la carpeta /data.

Ve al archivo data/yoga_poses.json en el editor de Cloud Shell y observa la lista de objetos JSON, en la que cada objeto JSON representa una postura de yoga. Tenemos un total de 3 registros y, a continuación, se muestra un registro de muestra:

{
   "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"]
 }

Esta es una gran oportunidad para presentar Gemini y cómo podemos usar el modelo predeterminado para generar un campo description para él.

En el editor de Cloud Shell, ve al archivo generate-descriptions.js. El contenido de este archivo se muestra a continuación:

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();

Esta aplicación agregará un nuevo campo description a cada registro JSON de la postura de yoga. Obtendrá la descripción a través de una llamada al modelo de Gemini, donde le proporcionaremos la instrucción necesaria. El campo se agrega al archivo JSON y el archivo nuevo se escribe en el archivo data/yoga_poses_with_descriptions.json.

Veamos los pasos principales:

  1. En la función main(), verás que invoca la función add_descriptions_to_json y proporciona el archivo de entrada y el archivo de salida esperados.
  2. La función add_descriptions_to_json hace lo siguiente para cada registro JSON, es decir, la información de la publicación de yoga:
  3. Extrae pose_name, sanskrit_name, expertise_level y pose_types.
  4. Invoca la función callGemini que construye una instrucción y, luego, invoca la clase del modelo LangchainVertexAI para obtener el texto de la respuesta.
  5. Luego, este texto de respuesta se agrega al objeto JSON.
  6. Luego, la lista de objetos JSON actualizada se escribe en el archivo de destino.

Ejecutemos esta aplicación. Inicia una nueva ventana de terminal (Ctrl + Mayúsculas + C) y escribe el siguiente comando:

npm run generate-descriptions

Si se te solicita alguna autorización, bríndala.

Verás que la aplicación comienza a ejecutarse. Agregamos una demora de 30 segundos entre los registros para evitar cualquier cuota de límite de frecuencia que pueda haber en las cuentas de Google Cloud nuevas, así que ten paciencia.

A continuación, se muestra un ejemplo de una ejecución en curso:

469ede91ba007c1f.png

Una vez que se hayan mejorado los 3 registros con la llamada a Gemini, se generará un archivo data/yoga_poses_with_description.json. Puedes consultarlo.

Ya tenemos todo listo con nuestro archivo de datos y el siguiente paso es comprender cómo propagar una base de datos de Firestore con él, junto con la generación de incorporaciones.

5. Importa datos a Firestore y genera incorporaciones de vectores

Tenemos el archivo data/yoga_poses_with_description.json y ahora debemos propagarlo en la base de datos de Firestore y, lo que es más importante, generar las incorporaciones de vectores para cada uno de los registros. Las incorporaciones de vectores serán útiles más adelante cuando tengamos que hacer una búsqueda de similitud con la consulta del usuario que se proporcionó en lenguaje natural.

Los pasos para hacerlo son los siguientes:

  1. Convertiremos la lista de objetos JSON en una lista de objetos. Cada documento tendrá dos atributos: content y metadata. El objeto de metadatos contendrá todo el objeto JSON que tenga atributos como name, description, sanskrit_name, etcétera. content será un texto de cadena que será una concatenación de algunos campos.
  2. Una vez que tengamos una lista de documentos, usaremos la clase de incorporaciones de Vertex AI para generar la incorporación del campo de contenido. Esta incorporación se agregará a cada registro de documento y, luego, usaremos la API de Firestore para guardar esta lista de objetos de documentos en la colección (usamos la variable TEST_COLLECTION que apunta a test-poses).

A continuación, se muestra el código de import-data.js (se truncaron partes del código para evitar desarrollos extensos):

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();

Ejecutemos esta aplicación. Inicia una nueva ventana de terminal (Ctrl + Mayúsculas + C) y escribe el siguiente comando:

npm run import-data

Si todo sale bien, deberías ver un mensaje similar al siguiente:

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.

Para verificar si los registros se insertaron correctamente y se generaron las incorporaciones, visita la página de Firestore en la consola de Cloud.

504cabdb99a222a5.png

Haz clic en la base de datos (predeterminada). Se debería mostrar la colección test-poses y varios documentos en ella. Cada documento es una postura de yoga.

9f37aa199c4b547a.png

Haz clic en cualquiera de los documentos para investigar los campos. Además de los campos que importamos, también encontrarás el campo embedding, que es un campo vectorial cuyo valor generamos a través del modelo de incorporación de Vertex AI text-embedding-004.

f0ed92124519beaf.png

Ahora que tenemos los registros subidos a la base de datos de Firestore con las incorporaciones en su lugar, podemos pasar al siguiente paso y ver cómo realizar la búsqueda de similitud de vectores en Firestore.

6. Importa poses de yoga completas a la colección de la base de datos de Firestore

Ahora, crearemos la colección poses, que es una lista completa de 160 posturas de yoga, para la que generamos un archivo de importación de base de datos que puedes importar directamente. Esto se hace para ahorrar tiempo en el lab. El proceso para generar la base de datos que contiene la descripción y las incorporaciones es el mismo que vimos en la sección anterior.

Sigue los pasos que se indican a continuación para importar la base de datos:

  1. Crea un bucket en tu proyecto con el comando gsutil que se indica a continuación. Reemplaza la variable <PROJECT_ID> en el siguiente comando por el ID de tu proyecto de Google Cloud.
gsutil mb -l us-central1 gs://<PROJECT_ID>-my-bucket
  1. Ahora que se creó el bucket, debemos copiar la exportación de la base de datos que preparamos en este bucket antes de poder importarla a la base de datos de Firebase. Usa el siguiente comando:
gsutil cp -r gs://yoga-database-firestore-export-bucket/2025-01-27T05:11:02_62615  gs://<PROJECT_ID>-my-bucket

Ahora que tenemos los datos para importar, podemos pasar al paso final de importarlos a la base de datos de Firebase (default) que creamos.

  1. Usa el siguiente comando de gcloud:
gcloud firestore import gs://<PROJECT_ID>-my-bucket/2025-01-27T05:11:02_62615

La importación tardará unos segundos y, una vez que esté lista, podrás validar tu base de datos de Firestore y la colección. Para ello, ve a https://console.cloud.google.com/firestore/databases, selecciona la base de datos default y la colección poses, como se muestra a continuación:

561f3cb840de23d8.png

Esto completa la creación de la colección de Firestore que usaremos en nuestra aplicación.

7. Realiza una búsqueda de similitud de vectores en Firestore

Para realizar la búsqueda de similitud de vectores, tomaremos la consulta del usuario. Un ejemplo de esta consulta puede ser "Suggest me some exercises to relieve back pain".

Consulta el archivo search-data.js. La función clave que debes observar es la función search, que se muestra a continuación. En un nivel alto, crea una clase de incorporación que se usará para generar la incorporación de la consulta del usuario. Luego, establece una conexión con la base de datos y la colección de Firestore. Luego, en la colección, invoca el método findNearest, que realiza una búsqueda de similitud de vectores.

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

Antes de ejecutarlo con algunos ejemplos de consultas, primero debes generar un índice compuesto de Firestore, que es necesario para que tus consultas de búsqueda se realicen correctamente. Si ejecutas la aplicación sin crear el índice, se mostrará un error que indica que primero debes crear el índice con el comando para crearlo.

A continuación, se muestra el comando gcloud para crear el índice compuesto:

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

El índice tardará unos minutos en completarse, ya que hay más de 150 registros en la base de datos. Una vez que se complete, puedes ver el índice con el siguiente comando:

gcloud firestore indexes composite list

Deberías ver el índice que acabas de crear en la lista.

Prueba el siguiente comando ahora:

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

Se te deberían proporcionar algunas recomendaciones. A continuación, se muestra una ejecución de ejemplo:

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']

Una vez que esto funcione, habremos entendido cómo usar la base de datos de vectores de Firestore para subir registros, generar incorporaciones y realizar una búsqueda de similitud vectorial. Ahora podemos crear una aplicación web que integre la búsqueda de vectores en un frontend web.

8. La aplicación web

La aplicación web de Flask en Python está disponible en el archivo app.js y el archivo HTML del frontend está presente en views/index.html..

Te recomendamos que revises ambos archivos. Comienza primero con el archivo app.js que contiene el controlador /search, que toma la instrucción que se pasó desde el archivo index.html HTML del frontend. Luego, invoca el método de búsqueda, que realiza la búsqueda de similitud de vectores que analizamos en la sección anterior.

Luego, la respuesta se envía a index.html con la lista de recomendaciones. Luego, index.html muestra las recomendaciones como tarjetas diferentes.

Ejecuta la aplicación de manera local

Inicia una nueva ventana de terminal (Ctrl + Mayúsculas + C) o cualquier ventana de terminal existente y escribe el siguiente comando:

npm run start

A continuación, se muestra una ejecución de muestra:

...
Server listening on port 8080

Una vez que esté en funcionamiento, visita la URL principal de la aplicación haciendo clic en el botón Vista previa en la Web que se muestra a continuación:

de297d4cee10e0bf.png

Deberías ver el archivo index.html que se entrega como se muestra a continuación:

20240a0e885ac17b.png

Proporciona una consulta de muestra (por ejemplo, Provide me some exercises for back pain relief) y haz clic en el botón Search. Esto debería recuperar algunas recomendaciones de la base de datos. También verás un botón Play Audio, que generará una transmisión de audio basada en la descripción, que puedes escuchar directamente.

789b4277dc40e2be.png

9. Implementación en Google Cloud Run (opcional)

Nuestro último paso será implementar esta aplicación en Google Cloud Run. A continuación, se muestra el comando de implementación. Antes de implementarlo, asegúrate de reemplazar los valores que se muestran en negrita. Estos son valores que podrás recuperar del archivo .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>>

Ejecuta el comando anterior desde la carpeta raíz de la aplicación. Es posible que también se te solicite que habilites las APIs de Google Cloud y que confirmes varios permisos. Hazlo.

El proceso de implementación tardará entre 5 y 7 minutos en completarse, así que ten paciencia.

3a6d86fd32e4a5e.png

Una vez que se implemente correctamente, el resultado de la implementación proporcionará la URL del servicio de Cloud Run. Tendrá el siguiente formato:

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

Visita esa URL pública y deberías ver la misma aplicación web implementada y ejecutándose correctamente.

84e1cbf29cbaeedc.png

También puedes visitar Cloud Run desde la consola de Google Cloud y verás la lista de servicios en Cloud Run. El servicio yogaposes debe ser uno de los servicios (si no es el único) que aparece allí.

f2b34a8c9011be4c.png

Para ver los detalles del servicio, como la URL, las configuraciones, los registros y mucho más, haz clic en el nombre específico del servicio (yogaposes en nuestro caso).

faaa5e0c02fe0423.png

Con esto, se completa el desarrollo y la implementación de nuestra aplicación web de recomendación de posturas de yoga en Cloud Run.

10. Felicitaciones

¡Felicitaciones! Compilaste de manera correcta una aplicación que sube un conjunto de datos a Firestore, genera las incorporaciones y realiza una búsqueda de similitud de vectores en función de la consulta del usuario.

Documentos de referencia