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 za pomocą wyszukiwania wektorowego będzie rekomendować pozycje jogi.

W ramach ćwiczeń z programowania będziesz wykonywać kolejne czynności:

  1. Użyj istniejącego zbioru danych Hugging Face z pozami jogi (w formacie JSON).
  2. Wzbogać zbiór danych o dodatkowy opis pola, który wykorzystuje Gemini do generowania opisów każdej z pozycji.
  3. Załaduj dane dotyczące pozycji jogi jako kolekcję dokumentów w kolekcji Firestore z wygenerowanymi osadzaniami.
  4. Utwórz indeks złożony w Firestore, aby umożliwić wyszukiwanie wektorowe.
  5. Użyj wyszukiwania wektorowego w aplikacji Node.js, która łączy wszystkie elementy w sposób pokazany poniżej:

84e1cbf29cbaeedc.png

Co musisz zrobić

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

Czego się nauczysz

  • Jak używać Gemini do generowania treści tekstowych, a w kontekście tego laboratorium – opisów pozycji jogi
  • Jak wczytywać rekordy z ulepszonego zbioru danych z Hugging Face do Firestore wraz z wektorami dystrybucyjnymi
  • Jak używać wyszukiwania wektorowego w Firestore do wyszukiwania danych na podstawie zapytania w języku naturalnym
  • Jak używać interfejsu Google Cloud Text-to-Speech API do generowania treści audio

Czego potrzebujesz

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

W tym samouczku, przeznaczonym dla deweloperów na wszystkich poziomach zaawansowania (w tym dla początkujących), w przykładowej aplikacji używane są JavaScript i Node.js. Znajomość języka JavaScript i środowiska Node.js nie jest jednak wymagana do zrozumienia przedstawionych koncepcji.

2. Zanim zaczniesz

Utwórz projekt

  1. W konsoli Google Cloud na stronie wyboru 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. Będziesz używać Cloud Shell, czyli środowiska wiersza poleceń działającego w Google Cloud, które jest wstępnie załadowane narzędziem bq. U góry konsoli Google Cloud kliknij Aktywuj Cloud Shell.

Obraz przycisku aktywowania Cloud Shell

  1. Po połączeniu z Cloud Shell sprawdź, czy uwierzytelnianie zostało już przeprowadzone, a projekt jest już ustawiony na Twój identyfikator projektu, używając tego polecenia:
gcloud auth list
  1. Aby potwierdzić, że 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, aby go ustawić:
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 poniżej:

Operation "operations/..." finished successfully.

Alternatywą dla polecenia gcloud jest wyszukanie poszczególnych usług w konsoli lub skorzystanie z tego linku.

Jeśli pominiesz jakiś interfejs API, możesz go włączyć w trakcie wdrażania.

Informacje o poleceniach gcloud i ich użyciu 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 przewodnika. Zakładając, że jesteś w Cloud Shell, wpisz to polecenie w katalogu głównym:

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. Kliknij pasek menu w lewym górnym rogu i wybierz Plik → Otwórz folder, jak pokazano poniżej:

66221fd0d0e5202f.png

Wybierz folder yoga-poses-recommender-nodejs. Powinien się otworzyć i zawierać te pliki:

7dbe126ee112266d.png

Teraz musimy skonfigurować zmienne środowiskowe, których będziemy używać. Kliknij plik env-template. Powinna wyświetlić się jego zawartość, jak pokazano 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 tym, co zostało wybrane podczas tworzenia projektu Google Cloud i regionu bazy danych Firestore. Najlepiej, aby wartości LOCATION były takie same w przypadku projektu Google Cloud i bazy danych Firestore, np. us-central1.

Na potrzeby tego ćwiczenia użyjemy tych wartości (z wyjątkiem PROJECT_IDLOCATION, które musisz ustawić zgodnie z 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 menu głównym w lewym górnym rogu środowiska IDE Cloud Shell 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! Możemy teraz przejść do konfigurowania 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 do danych aplikacji. Dane w Cloud Firestore są uporządkowane w kolekcjach dokumentów.

Inicjowanie bazy danych Firestore

Otwórz stronę Firestore w konsoli Cloud.

Jeśli w projekcie nie masz jeszcze 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.
  • Wybierz Typ lokalizacji – Region i wybierz lokalizację us-central1 dla regionu.
  • W przypadku reguł zabezpieczeń wybierz Test rules.
  • Utwórz bazę danych.

61d0277510803c8d.png

W następnej sekcji przygotujemy podstawy do utworzenia kolekcji o nazwie poses w domyślnej bazie danych Firestore. Ta kolekcja będzie zawierać przykładowe dane (dokumenty) lub informacje o pozycjach jogi, które następnie wykorzystamy w naszej aplikacji.

To już koniec sekcji dotyczącej konfigurowania bazy danych Firestore.

4. Przygotowywanie zbioru danych z pozami jogi

Naszym pierwszym zadaniem jest przygotowanie zbioru danych Yoga Poses, którego będziemy używać w aplikacji. Zaczniemy od istniejącego zbioru danych Hugging Face, a następnie uzupełnimy go o dodatkowe informacje.

Zapoznaj się z zbiorem danych Hugging Face dotyczącym pozycji jogi. Pamiętaj, że w tym samouczku używamy jednego ze zbiorów danych, ale możesz użyć dowolnego innego zbioru danych i zastosować te same techniki, aby go ulepszyć.

298cfae7f23e4bef.png

W sekcji Files and versions możemy pobrać plik danych JSON ze wszystkimi pozami.

3fe6e55abdc032ec.png

Pobraliśmy plik yoga_poses.json i udostępniliśmy go Tobie. Ten plik ma nazwę yoga_poses_alldata.json i znajduje się w folderze /data.

Otwórz plik data/yoga_poses.json w edytorze Cloud Shell i zapoznaj się z listą obiektów JSON, z których każdy reprezentuje pozycję jogi. Mamy łącznie 3 rekordy. Przykładowy rekord jest pokazany poniżej:

{
   "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 możemy użyć domyślnego modelu do wygenerowania pola description.

W edytorze Cloud Shell otwórz plik generate-descriptions.js. Zawartość tego pliku jest pokazana poniżej:

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 z pozycją jogi. Opis zostanie uzyskany w wyniku wywołania modelu Gemini, któremu przekażemy odpowiedni 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. W funkcji main() zobaczysz, że wywołuje ona funkcję add_descriptions_to_json i podaje oczekiwany plik wejściowy i wyjściowy.
  2. Funkcja add_descriptions_to_json wykonuje te czynności w przypadku każdego rekordu JSON, czyli informacji o poście dotyczącym jogi:
  3. Wyodrębnia pose_name, sanskrit_name, expertise_levelpose_types.
  4. Wywołuje funkcję callGemini, która tworzy prompt, a następnie wywołuje klasę modelu LangchainVertexAI, aby uzyskać tekst odpowiedzi.
  5. Ten tekst odpowiedzi jest następnie dodawany do obiektu JSON.
  6. Zaktualizowana lista obiektów JSON zostanie zapisana w pliku docelowym.

Uruchommy tę aplikację. Otwórz nowe okno terminala (Ctrl+Shift+C) i wpisz to polecenie:

npm run generate-descriptions

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

Aplikacja zacznie działać. Dodaliśmy 30-sekundowe opóźnienie między rekordami, aby uniknąć limitów szybkości, które mogą występować na nowych kontach Google Cloud. Prosimy o cierpliwość.

Poniżej widać przykładowe uruchomienie w trakcie:

469ede91ba007c1f.png

Gdy wszystkie 3 rekordy zostaną wzbogacone o informacje z wywołania Gemini, zostanie wygenerowany plik data/yoga_poses_with_description.json. Możesz to sprawdzić.

Mamy już plik danych. Teraz musimy dowiedzieć się, jak wypełnić nim bazę danych Firestore i wygenerować osadzanie.

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

Mamy już plik data/yoga_poses_with_description.json. Musimy teraz wypełnić nim bazę danych Firestore i wygenerować osadzenia wektorowe dla każdego rekordu. Wektory dystrybucyjne przydadzą się później, gdy będziemy musieli przeprowadzić na nich wyszukiwanie podobieństw za pomocą zapytania użytkownika podanego w języku naturalnym.

Aby to zrobić:

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

Kod dla import-data.js jest podany poniżej (fragmenty kodu zostały skrócone):

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

Uruchommy tę aplikację. Otwórz nowe okno terminala (Ctrl+Shift+C) i wpisz to polecenie:

npm run import-data

Jeśli wszystko przebiegnie prawidłowo, zobaczysz komunikat podobny do tego poniżej:

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 wygenerowano osadzanie, otwórz stronę Firestore w konsoli Cloud.

504cabdb99a222a5.png

Kliknij (domyślną) bazę danych. Powinna się wyświetlić kolekcja test-poses i wiele dokumentów w tej kolekcji. 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 text-embedding-004 Vertex AI.

f0ed92124519beaf.png

Po przesłaniu rekordów do bazy danych Firestore z osadzonymi wektorami możemy przejść do następnego kroku i sprawdzić, jak przeprowadzić wyszukiwanie podobieństwa wektorowego w Firestore.

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

Teraz utworzymy kolekcję poses, która zawiera pełną listę 160 pozycji jogi. Wygenerowaliśmy dla niej plik importu bazy danych, który możesz bezpośrednio zaimportować. Dzięki temu zaoszczędzisz czas w laboratorium. Proces generowania bazy danych zawierającej opis i wektory osadzenia jest taki sam jak w poprzedniej sekcji.

Zaimportuj bazę danych, wykonując czynności opisane poniżej:

  1. Utwórz zasobnik w projekcie za pomocą podanego poniżej polecenia gsutil. W poniższym poleceniu zastąp zmienną <PROJECT_ID> identyfikatorem projektu Google Cloud.
gsutil mb -l us-central1 gs://<PROJECT_ID>-my-bucket
  1. Po utworzeniu zasobnika musimy skopiować do niego przygotowany eksport bazy danych, zanim będziemy mogli go zaimportować do bazy danych Firebase. Użyj polecenia podanego poniżej:
gsutil cp -r gs://yoga-database-firestore-export-bucket/2025-01-27T05:11:02_62615  gs://<PROJECT_ID>-my-bucket

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

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

Importowanie potrwa kilka sekund. Po zakończeniu możesz sprawdzić bazę danych Firestore i kolekcję, otwierając stronę https://console.cloud.google.com/firestore/databases, wybierając bazę danych default i kolekcję poses, jak pokazano poniżej:

561f3cb840de23d8.png

W ten sposób utworzysz kolekcję Firestore, której będziemy używać w naszej aplikacji.

7. Wykonywanie wyszukiwania podobieństwa wektorów w Firestore

Aby przeprowadzić wyszukiwanie podobieństwa wektorowego, użyjemy zapytania od użytkownika. Przykładowe zapytanie to "Suggest me some exercises to relieve back pain".

Zapoznaj się z plikiem search-data.js. Najważniejsza funkcja to search, która jest pokazana poniżej. Na najwyższym poziomie tworzy klasę osadzania, która będzie używana do generowania osadzania dla zapytania użytkownika. Następnie nawiązuje połączenie z bazą danych i kolekcją Firestore. Następnie w kolekcji wywołuje metodę findNearest, która przeprowadza wyszukiwanie podobieństwa wektorów.

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 zapytanie z kilkoma przykładami, musisz najpierw wygenerować złożony indeks Firestore, który jest potrzebny do prawidłowego działania zapytań. Jeśli uruchomisz aplikację bez utworzenia indeksu, pojawi się błąd wskazujący, że musisz najpierw utworzyć indeks, wraz z poleceniem, które to umożliwi.

Poniżej znajdziesz 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

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

gcloud firestore indexes composite list

Na liście powinien pojawić się utworzony właśnie indeks.

Wypróbuj teraz to polecenie:

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

Powinno pojawić się kilka rekomendacji. Przykładowe uruchomienie pokazano poniżej:

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

Gdy to zrobisz, będziesz wiedzieć, jak korzystać z bazy danych wektorów Firestore, aby przesyłać rekordy, generować wektory dystrybucyjne i przeprowadzać wyszukiwanie podobieństwa wektorów. Możemy teraz utworzyć aplikację internetową, która zintegruje wyszukiwanie wektorowe z interfejsem internetowym.

8. Aplikacja internetowa

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

Zalecamy zapoznanie się z obydwoma plikami. Zacznij od pliku app.js, który zawiera moduł obsługi /search. Moduł ten pobiera prompt przekazany z pliku HTML index.html. Następnie wywołuje to metodę wyszukiwania, która przeprowadza wyszukiwanie podobieństwa wektorowego omówione w poprzedniej sekcji.

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

Lokalne uruchamianie aplikacji

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

npm run start

Przykładowe wykonanie jest pokazane poniżej:

...
Server listening on port 8080

Po uruchomieniu aplikacji otwórz jej adres URL, klikając przycisk Podgląd w przeglądarce widoczny poniżej:

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. Powinno to spowodować pobranie z bazy danych niektórych rekomendacji. Zobaczysz też przycisk Play Audio, który wygeneruje strumień audio na podstawie opisu. Możesz go od razu odsłuchać.

789b4277dc40e2be.png

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

Ostatnim krokiem będzie wdrożenie tej aplikacji w Google Cloud Run. Poniżej znajdziesz polecenie wdrażania. Zanim je wdrożysz, zastąp różne wartości w nawiasach (<<>>) podane poniżej. Są to wartości, które można 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 w folderze głównym aplikacji. Może też pojawić się prośba o włączenie interfejsów Cloud API Google i potwierdzenie różnych uprawnień.

Proces wdrażania potrwa około 5–7 minut, więc zachowaj cierpliwość.

3a6d86fd32e4a5e.png

Po pomyślnym wdrożeniu w danych wyjściowych wdrożenia pojawi się adres URL usługi Cloud Run. Będzie on miał postać:

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

Otwórz ten publiczny adres URL. Powinna się wyświetlić ta sama aplikacja internetowa, która została wdrożona i działa prawidłowo.

84e1cbf29cbaeedc.png

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

f2b34a8c9011be4c.png

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

faaa5e0c02fe0423.png

W ten sposób zakończyliśmy tworzenie i wdrażanie aplikacji internetowej do rekomendowania pozycji 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 przeprowadza wyszukiwanie podobieństwa wektorowego na podstawie zapytania użytkownika.

Dokumentacja