Codelab – tworzenie kontekstowej aplikacji z rekomendacjami dotyczących asan jogi za pomocą Firestore, wyszukiwania wektorowego, Langchain i Gemini (wersja Node.js)

1. Wprowadzenie

W tym ćwiczeniu w Codelabs utworzysz aplikację, która korzysta z wyszukiwania wektorowego, aby rekomendować pozycje jogi.

W ramach tego ćwiczenia będziesz wykonywać czynności krok po kroku:

  1. Użyj istniejącego zbioru danych Hugging Face zawierającego pozycje jogi (format JSON).
  2. Ulepszyć zbiór danych, dodając dodatkowy opis pola, który wykorzystuje Gemini do generowania opisów każdej z poz.
  3. Wczytaj dane dotyczące póz jogi jako kolekcję dokumentów w kolekcji Firestore z wygenerowanymi wektorami zastępczymi.
  4. Utwórz indeks złożony w Firestore, aby umożliwić wyszukiwanie wektorów.
  5. Użyj wyszukiwarki wektorów w aplikacji Node.js, która łączy wszystko razem, jak pokazano poniżej:

84e1cbf29cbaeedc.png

Co musisz zrobić

  • Zaprojektuj, zbuduj i wdróż aplikację internetową, która wykorzystuje wyszukiwanie wektorowe do rekomendowania pozycji jogi.

Czego się nauczysz

  • Jak za pomocą Gemini wygenerować treść tekstową, a w kontekście tego CodeLab – jak wygenerować opisy asan jogi
  • Jak za pomocą wektorów dystrybucyjnych załadować do Firestore rekordy z ulepszonego zbioru danych z Hugging Face
  • Jak używać wyszukiwania wektorowego w Firestore do wyszukiwania danych na podstawie zapytania w języku naturalnym
  • Jak użyć interfejsu Google Cloud Text-to-Speech API do generowania treści audio

Czego potrzebujesz

  • przeglądarka Chrome,
  • konto Gmail,
  • projekt w chmurze z włączonymi płatnościami,

Ten warsztat programistyczny przeznaczony dla deweloperów na wszystkich poziomach zaawansowania (w tym początkujących) wykorzystuje w przykładowej aplikacji język JavaScript i Node.js. Jednak znajomość języka JavaScript i Node.js nie jest wymagana do zrozumienia przedstawionych koncepcji.

2. Zanim zaczniesz

Utwórz projekt

  1. W konsoli Google Cloud na stronie selektora projektu wybierz lub utwórz projekt Google Cloud.
  2. Sprawdź, czy w projekcie Cloud włączone są płatności. Dowiedz się, jak sprawdzić, czy w projekcie są włączone płatności .
  3. Użyjesz Cloud Shell, czyli środowiska wiersza poleceń działającego w Google Cloud, które jest wstępnie wczytane w bq. Kliknij Aktywuj Cloud Shell u góry konsoli Google Cloud.

Obraz przycisku aktywowania Cloud Shell

  1. Po połączeniu z Cloud Shell sprawdź, czy jesteś już uwierzytelniony i czy projekt jest ustawiony na identyfikator Twojego projektu, używając tego polecenia:
gcloud auth list
  1. Aby sprawdzić, czy polecenie gcloud zna Twój projekt, uruchom w Cloud Shell to polecenie:
gcloud config list project
  1. Jeśli projekt nie jest ustawiony, użyj tego polecenia:
gcloud config set project <YOUR_PROJECT_ID>
  1. Włącz wymagane interfejsy API za pomocą polecenia pokazanego poniżej. Może to potrwać kilka minut, więc zachowaj cierpliwość.
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

Po pomyślnym wykonaniu polecenia powinien wyświetlić się komunikat podobny do tego:

Operation "operations/..." finished successfully.

Alternatywą dla polecenia gcloud jest konsola, w której możesz wyszukać poszczególne usługi lub skorzystać z tego linku.

Jeśli pominiesz któryś interfejs API, możesz go włączyć w trakcie implementacji.

Więcej informacji o poleceniach i użytkowaniu gcloud znajdziesz w dokumentacji.

Klonowanie repozytorium i konfigurowanie ustawień środowiska

Następnym krokiem jest sklonowanie przykładowego repozytorium, do którego będziemy się odwoływać w dalszej części tego samouczka. Zakładając, że jesteś w Cloud Shell, uruchom to polecenie w katalogu domowym:

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

Aby uruchomić edytor, na pasku narzędzi w oknie Cloud Shell kliknij Otwórz edytor. W lewym górnym rogu kliknij pasek menu i wybierz Plik → Otwórz folder, jak pokazano na ilustracji:

66221fd0d0e5202f.png

Wybierz folder yoga-poses-recommender-nodejs. Powinien się otworzyć folder z tymi plikami:

7dbe126ee112266d.png

Teraz musimy skonfigurować zmienne środowiskowe, których będziemy używać. Kliknij plik env-template. Powinieneś zobaczyć zawartość pokazaną poniżej:

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

Zaktualizuj wartości PROJECT_IDLOCATION zgodnie z ustawieniami wybranymi podczas tworzenia projektu Google Cloud i bazy danych Firestore. W idealnej sytuacji wartości parametru LOCATION powinny być takie same w przypadku projektu Google Cloud i bazy danych Firestore, np. us-central1.

W tym ćwiczeniu użyjemy tych wartości (z wyjątkiem oczywiście PROJECT_IDLOCATION, które musisz ustawić zgodnie ze swoją konfiguracją).

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

Zapisz ten plik jako .env w tym samym folderze co plik env-template.

W Cloud Shell IDE otwórz menu główne w lewym górnym rogu, a potem kliknij Terminal → New Terminal.

Przejdź do folderu głównego sklonowanego repozytorium za pomocą tego polecenia:

cd yoga-poses-recommender-nodejs

Zainstaluj zależności Node.js za pomocą polecenia:

npm install

Świetnie! Wszystko jest już gotowe do skonfigurowania bazy danych Firestore.

3. Konfigurowanie Firestore

Cloud Firestore to w pełni zarządzana bezserwerowa baza danych dokumentów, której będziemy używać jako backendu dla danych aplikacji. Dane w Cloud Firestore są ustrukturyzowane w kolekcjach dokumentów.

Inicjowanie bazy danych Firestore

Otwórz stronę Firestore w konsoli Cloud.

Jeśli w projekcie nie masz zainicjowanej bazy danych Firestore, utwórz bazę danych default, klikając Create Database. Podczas tworzenia bazy danych użyj tych wartości:

  • Tryb Firestore: Native.
  • Lokalizacja: użyj domyślnych ustawień lokalizacji.
  • W przypadku reguł zabezpieczeń wybierz Test rules.
  • Utwórz bazę danych.

504cabdb99a222a5.png

W następnej sekcji przygotujemy się do utworzenia kolekcji o nazwie poses w domyślnej bazie danych Firestore. Ta kolekcja będzie zawierać przykładowe dane (dokumenty) lub informacje o pozach jogi, których użyjemy w naszej aplikacji.

To koniec sekcji poświęconej konfigurowaniu bazy danych Firestore.

4. Przygotowanie zbioru danych o pozach jogi

Naszym pierwszym zadaniem jest przygotowanie zbioru danych o pozycjach jogi, którego użyjemy w aplikacji. Zaczniemy od istniejącego zbioru danych Hugging Face, a potem wzbogacimy go o dodatkowe informacje.

Zapoznaj się ze zbiorem danych Hugging Face do rozpoznawania pozycji jogi. Pamiętaj, że chociaż w tym laboratorium kodów używamy jednego zbioru danych, możesz użyć dowolnego innego zbioru danych i zastosować te same techniki, aby go wzbogacić.

298cfae7f23e4bef.png

Jeśli przejdziesz do sekcji Files and versions, możesz pobrać plik danych JSON ze wszystkimi pozami.

3fe6e55abdc032ec.png

Pobraliśmy plik yoga_poses.json i przesłałyśmy go do Ciebie. Plik o nazwie yoga_poses_alldata.json znajduje się w folderze /data.

Otwórz plik data/yoga_poses.json w edytorze Cloud Shell i sprawdź listę obiektów JSON, z których każdy reprezentuje jedną pozycję jogi. Mamy łącznie 3 rekordy, a przykładowy rekord wygląda tak:

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

To świetna okazja, aby przedstawić Gemini i pokazać, jak za pomocą modelu domyślnego wygenerować dla niego pole description.

W edytorze Cloud Shell otwórz plik generate-descriptions.js. Poniżej znajduje się zawartość tego pliku:

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

Ta aplikacja doda nowe pole description do każdego rekordu JSON dotyczącego pozycji jogi. Otrzyma opis za pomocą wywołania modelu Gemini, w którym przekażemy niezbędny prompt. Pole zostanie dodane do pliku JSON, a nowy plik zostanie zapisany w pliku data/yoga_poses_with_descriptions.json.

Oto główne kroki:

  1. Funkcja main() wywołuje funkcję add_descriptions_to_json i przekazuje oczekiwany plik wejściowy i wyjściowy.
  2. Funkcja add_descriptions_to_json wykonuje te czynności w przypadku każdego rekordu JSON, np. informacji o poście na temat jogi:
  3. Wyodrębnia pose_name, sanskrit_name, expertise_level i pose_types.
  4. Wywołuje funkcję callGemini, która tworzy prompt, a następnie wywołuje klasę modelu LangchainVertexAI, aby uzyskać tekst odpowiedzi.
  5. Następnie tekst odpowiedzi jest dodawany do obiektu JSON.
  6. Następnie zaktualizowana lista obiektów JSON jest zapisywana w pliku docelowym.

Pozwól nam uruchomić tę aplikację. Uruchom nowe okno terminala (Ctrl+Shift+C) i wpisz to polecenie:

npm run generate-descriptions

Jeśli pojawi się prośba o autoryzację, potwierdź ją.

Aplikacja zacznie się wykonywać. Dodaliśmy 30-sekundową przerwę między rekordami, aby uniknąć limitów szybkości, które mogłyby wystąpić na nowych kontach Google Cloud. Prosimy o cierpliwość.

Poniżej widać przykładowy proces wykonywania:

469ede91ba007c1f.png

Po ulepszeniu wszystkich 3 rekordów za pomocą wywołania Gemini zostanie wygenerowany plik data/yoga_poses_with_description.json. Możesz to sprawdzić.

Mamy już plik danych, więc następnym krokiem jest zapełnienie nim bazy danych Firestore oraz wygenerowanie zaszyfrowanych danych.

5. Importowanie danych do Firestore i generowanie wektorów dystrybucyjnych

Mamy plik data/yoga_poses_with_description.json i teraz musimy wypełnić nim bazę danych Firestore oraz, co ważne, wygenerować wektory zanurzeniowe dla każdego rekordu. Wektory zanurzone będą przydatne później, gdy będziemy musieli przeprowadzić wyszukiwanie podobieństw z użyciem zapytania użytkownika podanego w języku naturalnym.

Aby to zrobić:

  1. Przekształcimy listę obiektów JSON w listę obiektów. Każdy dokument będzie mieć 2 atrybuty: content i metadata. Obiekt metadanych zawiera cały obiekt JSON z atrybutami takimi jak name, description, sanskrit_name itp. Element content będzie ciągiem tekstowym, który będzie zawierał konkatenację kilku pól.
  2. Gdy będziemy mieć listę dokumentów, użyjemy klasy Vertex AI Embeddings do wygenerowania embeddingu dla pola content. Ten element zostanie dodany do każdego rekordu dokumentu, a następnie użyjemy interfejsu Firestore API, aby zapisać tę listę obiektów dokumentu w kolekcji (korzystamy z zmiennej TEST_COLLECTION, która wskazuje na test-poses).

Poniżej znajduje się kod import-data.js (część kodu została skrócona ze względu na zwiężenie):

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

Pozwól nam uruchomić tę aplikację. Uruchom nowe okno terminala (Ctrl+Shift+C) i wpisz to polecenie:

npm run import-data

Jeśli wszystko pójdzie dobrze, powinien wyświetlić się komunikat podobny do tego:

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.

Aby sprawdzić, czy rekordy zostały wstawione i czy zostały wygenerowane elementy embeddingu, otwórz stronę Firestore w konsoli Google Cloud.

504cabdb99a222a5.png

Kliknij bazę danych (domyślną). Powinna się wyświetlić kolekcja test-poses i kilka dokumentów w niej. Każdy dokument to jedna pozycja jogi.

9f37aa199c4b547a.png

Kliknij dowolny dokument, aby sprawdzić pola. Oprócz zaimportowanych pól znajdziesz też pole embedding, które jest polem wektorowym, a jego wartość została wygenerowana za pomocą modelu osadzania Vertex AI text-embedding-004.

f0ed92124519beaf.png

Teraz, gdy rekordy zostały przesłane do bazy danych Firestore z umieszczonymi w niej wektorami dystrybucyjnymi, możemy przejść do następnego kroku i zobaczyć, jak wykonać wyszukiwanie wektorowe w Firestore.

6. Importowanie pełnych pozycji jogi do kolekcji bazy danych Firestore

Utworzymy teraz kolekcję poses, która zawiera pełną listę 160 pozycji jogi. Wygenerowaliśmy plik do importu bazy danych, który możesz zaimportować bezpośrednio. Robimy to, aby zaoszczędzić czas w laboratorium. Proces generowania bazy danych zawierającej opis i wbudowane modele jest taki sam jak w poprzedniej sekcji.

Aby zaimportować bazę danych, wykonaj te czynności:

  1. Utwórz zasobnik w projekcie za pomocą polecenia gsutil podanego poniżej. W poniższym poleceniu zastąp zmienną <PROJECT_ID> identyfikatorem projektu Google Cloud.
gsutil mb -l us-central1 gs://<PROJECT_ID>-my-bucket
  1. Teraz, gdy zasób został utworzony, musimy skopiować przygotowany wcześniej eksport bazy danych do tego zasobu, zanim zaimportujemy go do bazy danych Firebase. Użyj podanego niżej polecenia:
gsutil cp -r gs://yoga-database-firestore-export-bucket/2025-01-27T05:11:02_62615  gs://<PROJECT_ID>-my-bucket

Teraz, gdy mamy już dane do zaimportowania, możemy przejść do ostatniego kroku, czyli zaimportować je do utworzonej przez nas bazy danych Firebase (default).

  1. Użyj polecenia gcloud podanego poniżej:
gcloud firestore import gs://<PROJECT_ID>-my-bucket/2025-01-27T05:11:02_62615

Importowanie zajmie kilka sekund. Gdy zakończy się ono, możesz zweryfikować bazę danych Firestore i kolekcję, klikając https://console.cloud.google.com/firestore/databases i wybierając bazę danych default oraz kolekcję poses, jak pokazano poniżej:

561f3cb840de23d8.png

W ten sposób utworzysz kolekcję Firestore, której użyjesz w swojej aplikacji.

7. Przeprowadzanie wyszukiwania według podobieństwa wektora w Firestore

Aby przeprowadzić wyszukiwanie według podobieństwa wektorowego, pobieramy zapytanie od użytkownika. Przykładem takiego zapytania może być "Suggest me some exercises to relieve back pain".

Zapoznaj się z plikiem search-data.js. Najważniejszą funkcją do sprawdzenia jest funkcja search, którą możesz zobaczyć poniżej. Ogólnie rzecz biorąc, tworzy klasę embeddingu, która służy do generowania embeddingu zapytania użytkownika. Następnie nawiązuje połączenie z bazą danych i kolekcją Firestore. Następnie w zbiorze wywołuje metodę findNearest, która wykonuje wyszukiwanie według podobieństwa wektorowego.

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

Zanim uruchomisz to narzędzie z kilkoma przykładami zapytań, musisz najpierw wygenerować złożony indeks Firehose, który jest potrzebny do wykonania zapytań. Jeśli uruchomisz aplikację bez utworzenia indeksu, wyświetli się błąd z komendą, która poinformuje Cię, że musisz najpierw utworzyć indeks.

Poniżej przedstawiono polecenie gcloud służące do tworzenia indeksu złożonego:

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

Utworzenie indeksu może potrwać kilka minut, ponieważ w bazie danych jest ponad 150 rekordów. Po zakończeniu indeksowania możesz wyświetlić indeks za pomocą polecenia widocznego poniżej:

gcloud firestore indexes composite list

Na liście powinien pojawić się utworzony przez Ciebie indeks.

Wypróbuj teraz to polecenie:

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

Powinieneś/powinnaś otrzymać kilka rekomendacji. Poniżej przedstawiono przykładowy przebieg:

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

Teraz, gdy już wiesz, jak korzystać z bazy danych wektorów Firestore do przesyłania rekordów, generowania wektorów dystrybucyjnych i wyszukiwania podobieństwa wektorów, Możemy teraz utworzyć aplikację internetową, która zintegruje wyszukiwanie wektorów z interfejsem internetowym.

8. Aplikacja internetowa

Aplikacja internetowa Python Flask jest dostępna w pliku app.js, a plik HTML front-endu znajduje się w pliku views/index.html..

Zalecamy zapoznanie się z obu plikami. Zacznij od pliku app.js, który zawiera moduł obsługi /search. Ten moduł obsługuje prompt przekazany z pliku HTML index.html na froncie. Następnie wywołuje metodę wyszukiwania, która przeprowadza wyszukiwanie według podobieństwa wektorowego, omówione w poprzedniej sekcji.

Odpowiedź jest następnie wysyłana z listą rekomendacji do index.html. Następnie index.html wyświetla rekomendacje w postaci różnych kart.

Uruchamianie aplikacji lokalnie

Uruchom nowe okno terminala (Ctrl+Shift+C) lub dowolne istniejące okno terminala i wpisz to polecenie:

npm run start

Poniżej przedstawiamy przykład wykonania:

...
Server listening on port 8080

Po uruchomieniu aplikacji otwórz URL strony głównej aplikacji, klikając przycisk Podgląd w przeglądarce:

de297d4cee10e0bf.png

Powinien wyświetlić się plik index.html, jak pokazano poniżej:

20240a0e885ac17b.png

Podaj przykładowe zapytanie (np. Provide me some exercises for back pain relief) i kliknij przycisk Search. Powinien on pobrać z bazy danych kilka rekomendacji. Zobaczysz też przycisk Play Audio, który wygeneruje strumień audio na podstawie opisu.

789b4277dc40e2be.png

9. (Opcjonalnie) Wdrażanie w Google Cloud Run

Ostatnim krokiem będzie wdrożenie tej aplikacji w Google Cloud Run. Polecenie wdrażania znajduje się poniżej. Przed wdrożeniem upewnij się, że zastąpisz wartości wyświetlane w pogrubieniu. Te wartości możesz pobrać z pliku .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>>

Uruchom powyższe polecenie z folderu głównego aplikacji. Możesz też zostać poproszony(-a) o włączenie interfejsów Google Cloud API i potwierdzenie różnych uprawnień.

Proces wdrażania może potrwać 5–7 minut, więc zachowaj cierpliwość.

3a6d86fd32e4a5e.png

Po udanym wdrożeniu w wyniku wdrożonego procesu znajdziesz adres URL usługi Cloud Run. Ma ono następującą formę:

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

Otwórz ten publiczny adres URL, aby sprawdzić, czy aplikacja internetowa została wdrożona i działa prawidłowo.

84e1cbf29cbaeedc.png

W konsoli Google Cloud możesz też otworzyć Cloud Run, aby wyświetlić listę usług w Cloud Run. Usługa yogaposes powinna być jedną z wymienionych usług (jeśli nie jedyną).

f2b34a8c9011be4c.png

Aby wyświetlić szczegóły usługi, takie jak adres URL, konfiguracje, dzienniki itp., kliknij nazwę konkretnej usługi (w naszym przypadku jest to yogaposes).

faaa5e0c02fe0423.png

To kończy rozwój i wdrażanie aplikacji internetowej polecającej pozycje jogi w Cloud Run.

10. Gratulacje

Gratulacje! Udało Ci się utworzyć aplikację, która przesyła zbiór danych do Firestore, generuje wektory dystrybucyjne i wykonuje wyszukiwanie według podobieństwa wektorów na podstawie zapytania użytkownika.

Dokumenty referencyjne