Browse Source

change embedding models to use google's

Dr-Swopt 5 hours ago
parent
commit
b1575fd3a0
5 changed files with 347 additions and 271 deletions
  1. 13 0
      gemini-embedding-service-key.json
  2. 13 0
      justfortesting-328703-80166a275d93.json
  3. 251 241
      package-lock.json
  4. 2 1
      package.json
  5. 68 29
      src/FFB/ffb-vector.service.ts

+ 13 - 0
gemini-embedding-service-key.json

@@ -0,0 +1,13 @@
+{
+  "type": "service_account",
+  "project_id": "gen-lang-client-0904742181",
+  "private_key_id": "cf587d7d95aeedee7f08d176317db7c614f9f6c9",
+  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCeaYc1n1Q/RCBQ\ndBuUb+67ltGBjdZJ2q8Y5AURZc+rnXZbZfmRa6rtKmkhqvd1LBFYd6Q1XDNKu6tC\nkMS2ucP+WF2dXOW7rCXYh7MXk4RMC6Fbv+YU/xkcwKHlkv1qRB/CBU7BcZcx+WAr\neCQPhJQmprKUgrKwr7vNe/2uP910EEvAdagwE//yPIJ5FPBFvyDqM/OF+XaIJQjx\nQt/KygBzjpwhdQg0/BGIFX6gvWQbB2OcxNuky5cyw9gvKWW05rQd+A0lREkRzCoW\nTzjASojjI7iKoKv1xkOxEu3TPKtV7LdOu9JCzfyHeKVbpSQYjJn1YqoYS0aS01jl\ne+It+QnHAgMBAAECggEAO4KRVMeiMpI5QYAczEqweLBTzEOoeN46YTNn4/1HYsUo\nrXVFqCe2cpo4HHnJtK71ZTKs8Y7NhgmycLNJ8wseYfJ4WKGJfFNTdGCmGvlD/f1w\nhLM2kDS1DmQfbcsmgEFPXOf5ihpOHmv8gNgFU/8OWObOt5PR5SZfevZVepxraSoO\ne6SsNQ09MYUW6kFYG0BbnXj2l9x7q8/r3ZGU4IpCJxm0t3g90oKN+QNlJl7EwlDz\nnKLWxhtpXdIfHo3jNyoJ1dyfjDkN6s7t/jCvR0w0Tt9n6GSGlIgTJ1iOj1Tv6TBo\npxzesgdckZarbFXKJ3JY1I+qxeq95r0v1TvExMzrgQKBgQDVb0UQ6gMH1Fs5KxbY\nfMClLr7XqQOgAcLmBEBNZ4TCEIta1iGlGv5tT9x6L/2/MFMbUL4Hi4ECSiir4Ghb\nu4Thp3avu7Yk467uOQuKGEs/AChpTjkeffFMeigrhWVlq4NELcoFzzi/0yhcUASN\n2PAIVgi6VWEJxBi0MFPnGAVlNwKBgQC+ASIzmdFkmiI/tCPq0VlqaS6cAAg6AqpC\nunXhrppa3QENdUhEwJBKYoyjA1yp8C02ZVSx+7z3OEKyWi5zG7lnp7mVOGMSNrcI\nkcnNRK4wG+yj4NBgKcb+dSRYKXP1NElnki8rgEntoLCk2YmkyvtaxhGIDjkD2jfP\nJeqx1gHH8QKBgAoAV80uGgxA4DFymnR0jBZxdVHnwpq52mcq5dR6uFbbrZwJErSI\n6kk3B87V9t8BpbNO+kiiOd5gmT1Mm7dItzZXwZEEi8l4vda955OGBeii2kHs/3I1\nVpxN0RoQ+ypjehg0yRWymycp/ucsfLok40KQvYH1xEFP5hRze0sF7iLLAoGABcrn\nDgeseJKyZJrLVYdYYIQgZaUimxIluq8QlPbLweVm+NAQifgM7hefDgE2PAAUgMoK\nEPsJwce3UNSrAdtghaZ5Y/E6I/4DCoHXUyi63sCbMEvUTno3lN5hY0awQFN9wWiV\ng4//sPzrJbt0FNATZasQMcOtPU7T0L7pLs7FcyECgYBdBdXi8WtefHi0Hs52X60C\niccUXlLySFiunER/8YN3IYvC/eerPXB4qTorcyRj4v1QqMDorAWkvfXaTd1qXKrB\nIsM/Coo65VWRDV8+mjtkpfCqql1X/OHrd2QNx6AF/aUupVFwUOIVtOANRX0ORPwF\nUgU2RrMoVKbOtbVApuAicA==\n-----END PRIVATE KEY-----\n",
+  "client_email": "gemini-embedding-service@gen-lang-client-0904742181.iam.gserviceaccount.com",
+  "client_id": "107546617782982018031",
+  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+  "token_uri": "https://oauth2.googleapis.com/token",
+  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/gemini-embedding-service%40gen-lang-client-0904742181.iam.gserviceaccount.com",
+  "universe_domain": "googleapis.com"
+}

+ 13 - 0
justfortesting-328703-80166a275d93.json

@@ -0,0 +1,13 @@
+{
+  "type": "service_account",
+  "project_id": "justfortesting-328703",
+  "private_key_id": "80166a275d93f660e9d3b2e9c12ccdfb4238148f",
+  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0VLqbL8G4NbqK\nBy8jORkLQhU+4sJbjlMpVXrc6UIrspWUiauWWICAVEiVRzzh2WUVuOy33v0/fr1R\nTi63pPaoR6h73xPjM9NTODuSl52OI0G4JsWQeFWyJsMPVZukewOqbfkKS6iy8Esc\nSUY/30HJvkmIvMQUTn7aYKeRkm3vDlTi4zgkooCH3HDsRdP1kaQrbwctmH6enlrA\n17/hv7a7EpSNx9pNPJ5PQg7MMlNqQa1KE7vmYiAdv7dPjPWxnUN9lmT3/d0yPrRl\nF13ioiv1RlHW5/xmhZ2n0VDj6tKttrpVtLsLqQLU3QZ4Ybp5Z8RB1sfGB+Ak+wQP\nTKaygFvLAgMBAAECggEABNo40T82O8I6XEd4Nb7uFRb4omIQr80DINwbe8FNFgbu\nrnoL43ZLMujRsS6jDUK/zRdWS1ZVQzWiW4M6j5eBSaLv/jFRBC6bU6RyOe4Zf78k\nIBPvAgWy7KHNYua/Uuw6e4YNCwFhLzt3EfJI0SQtTYPwz5ygvBkmobKzH6AslB+n\nbrqcDi3PsjB8ibxJKSOs89Kb6uEuqwXcEuLOqiuGsNnOeEtcB+8DAfvzLjfJyQ6n\nrSy/MYbkydbLvY77FCB5bdp/ixFGhnYTZTlfHOe7ueUpdYhXfiZsUzD8+4zCE4ma\nfnZTAOYs95SzCrDwQj4b7I3NuCCZ76sMpubxuoaxiQKBgQD2AvkIHH8Db0MBzyXA\nX5irZ6kmq7MD8yiR2N66rW877qDNK4uH0LOzLoUmLmV7kAMK5PkLQZ3eEdOLTNBV\n1nQ68GpGcc/U1O7nTcGH/kgFb1jbCLSptp5E3EVQTkHtnNkZ60n1cicJZ6IHlvbG\n7rXtt6LDlfZ8NiMB1smgkivWowKBgQC7pxOExEr97kHLHeBvtFYCfbi0YO3kUhtC\nhPykYcvRewG8oHbG3e9XbBXMgKBZb3vcRKu61ou+IS1XADykDN81XDB9PD5+26n4\nTprQHiaGYvV4KAO0dR+swcZ6l0tRK+m+T3FPLzhlB6mqs0IFlrCnDAVg4t2hJ2NS\nSWjgBkjAuQKBgQCIuk4+O9g7yHKtZrvMl1T6rrpMS9FKuLIrnSTtC8duv8mPPkxm\nR5AYXhqShebRLdEDFQ91OhrLeYzhNufzTSV8PHJUhJzF5TKCS6zsMF9G5gO0eLow\nONbt3p6Ha3co4KuoxCRuzer0Ryy+myC8n59tZ0qG+ansjwoV05JsM9E5kQKBgDg8\nQaUwDik7FYyBT5kqOfxVIN0tjx01XeX5ZJz+kc4dRs/4ZqACMo/IXGAEzAkBV+US\nz0QWt5oq5yODdqjTErEzB3UAcNojijRXmh10a6cqUNXJaBLUZsGm8Iwcev3AYzQd\nKp/ITuY3/aiiP87c8eOdvp7iXfjFjQZD6aH5QCRBAoGAa25JuAQJImZwcVEc1HNp\nKSZ516plBNsbKZv6MW8fHwpCBe09DxFGJszfTe4KVM2RiydeA+k+7SK6xbHq89ws\n+T8qFfltDpTVNNqB5iJOFMF0Yb5CvDpDfkm0TqogtySsIgtmlQBlkIRF9xnaPH3V\nFfcQFFCeGbu/c4TAKvVRk3s=\n-----END PRIVATE KEY-----\n",
+  "client_email": "gemini-embedding-service@justfortesting-328703.iam.gserviceaccount.com",
+  "client_id": "117446588306721668549",
+  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+  "token_uri": "https://oauth2.googleapis.com/token",
+  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/gemini-embedding-service%40justfortesting-328703.iam.gserviceaccount.com",
+  "universe_domain": "googleapis.com"
+}

File diff suppressed because it is too large
+ 251 - 241
package-lock.json


+ 2 - 1
package.json

@@ -32,13 +32,14 @@
     "@nestjs/platform-socket.io": "^11.1.3",
     "@nestjs/websockets": "^11.1.3",
     "@simplewebauthn/server": "^13.1.1",
-    "@xenova/transformers": "^2.17.2",
+    "axios": "^1.13.2",
     "bcrypt": "^6.0.0",
     "class-transformer": "^0.5.1",
     "class-validator": "^0.14.2",
     "dotenv": "^17.2.3",
     "express-list-endpoints": "^7.1.1",
     "express-session": "^1.18.1",
+    "google-auth-library": "^10.5.0",
     "mongodb": "^7.0.0",
     "mongoose": "^8.19.1",
     "passport": "^0.7.0",

+ 68 - 29
src/FFB/ffb-vector.service.ts

@@ -1,44 +1,89 @@
 import { Injectable, OnModuleInit } from '@nestjs/common';
-import { pipeline, FeatureExtractionPipeline } from '@xenova/transformers';
+import axios from 'axios';
+import { GoogleAuth } from 'google-auth-library';
 import { MongoCoreService } from 'src/mongo/mongo-core.service';
 import { FFBProductionRepository } from 'src/mongo/mongo-ffb-production.repository';
 import { FFBProduction } from './ffb-production.schema';
 
 @Injectable()
 export class FFBVectorService implements OnModuleInit {
-  private embedder: FeatureExtractionPipeline;
   private repo: FFBProductionRepository;
-  private readonly VECTOR_DIM = 384; // must match your index
+  private readonly VECTOR_DIM = parseInt(process.env.VECTOR_DIM || '3072'); // Gemini default
+  private accessToken: string; // OAuth token
 
-  constructor(private readonly mongoCore: MongoCoreService) { }
+  constructor(private readonly mongoCore: MongoCoreService) {}
 
-  /** Initialize model and repository at module startup */
   async onModuleInit() {
-    const modelName = process.env.EMBEDDING_MODEL || 'Xenova/all-MiniLM-L6-v2';
-    console.log(`🔹 Loading embedding model: ${modelName}...`);
-
-    this.embedder = (await pipeline('feature-extraction', modelName)) as unknown as FeatureExtractionPipeline;
-
+    // Initialize Mongo repository
     const db = await this.mongoCore.getDb();
     this.repo = new FFBProductionRepository(db);
     await this.repo.init();
 
-    console.log(`✅ Embedding model loaded and repository ready.`);
+    // Authenticate with Google service account
+    const auth = new GoogleAuth({
+      keyFile: process.env.GOOGLE_APPLICATION_CREDENTIALS,
+      scopes: ['https://www.googleapis.com/auth/cloud-platform'],
+    });
+
+    const client = await auth.getClient();
+    const tokenResponse = await client.getAccessToken();
+    if (!tokenResponse.token) {
+      throw new Error('Failed to obtain OAuth 2 access token for Gemini embeddings');
+    }
+    this.accessToken = tokenResponse.token;
+
+    console.log('✅ Gemini embedding service ready. Repository initialized.');
   }
 
-  /** Convert an FFBProduction record into text for embedding */
+  /** Convert a record to a string suitable for embedding */
   private recordToText(record: FFBProduction): string {
     return `Production on ${new Date(record.productionDate).toISOString()} at ${record.site} in ${record.phase} ${record.block} produced ${record.quantity} ${record.quantityUom} with a total weight of ${record.weight} ${record.weightUom}.`;
   }
 
-  /** Generate embedding vector from text */
+  /** Generate embedding via Google Gemini */
   private async embedText(text: string): Promise<number[]> {
-    const output = await this.embedder(text, { pooling: 'mean', normalize: true });
-    const vector = Array.from(output.data);
-    if (vector.length !== this.VECTOR_DIM) {
-      throw new Error(`Embedding dimension mismatch. Expected ${this.VECTOR_DIM}, got ${vector.length}`);
+    const project = process.env.GOOGLE_PROJECT_ID!;
+    const model = process.env.EMBEDDING_MODEL || 'gemini-embedding-001';
+    const location = 'us-central1';
+
+    const url = `https://${location}-aiplatform.googleapis.com/v1/projects/${project}/locations/${location}/publishers/google/models/${model}:predict`;
+
+    try {
+      const response = await axios.post(
+        url,
+        {
+          instances: [{ content: text }],
+          parameters: { autoTruncate: true, outputDimensionality: this.VECTOR_DIM },
+        },
+        {
+          headers: {
+            Authorization: `Bearer ${this.accessToken}`,
+            'Content-Type': 'application/json',
+          },
+        },
+      );
+
+      const predictions = response.data?.predictions;
+      if (!predictions || predictions.length === 0) {
+        throw new Error(`No predictions returned from Gemini API: ${JSON.stringify(response.data)}`);
+      }
+
+      const embedding = predictions[0]?.embeddings?.values;
+      if (!embedding || !Array.isArray(embedding)) {
+        throw new Error(`Invalid embedding format returned: ${JSON.stringify(predictions[0])}`);
+      }
+
+      if (embedding.length !== this.VECTOR_DIM) {
+        console.warn(
+          `⚠️ Warning: embedding dimension mismatch. Expected ${this.VECTOR_DIM}, got ${embedding.length}`,
+        );
+      }
+
+      return embedding;
+    } catch (err: any) {
+      console.error('Failed to generate embedding:', err.response?.data || err.message);
+      throw err;
     }
-    return vector;
   }
 
   /** Insert a single record with embedding vector */
@@ -46,7 +91,6 @@ export class FFBVectorService implements OnModuleInit {
     const text = this.recordToText(record);
     const vector = await this.embedText(text);
 
-    // Explicitly tell TypeScript this object matches the repository type
     const data: FFBProduction & { vector: number[] } = { ...record, vector };
     return this.repo.create(data);
   }
@@ -55,17 +99,12 @@ export class FFBVectorService implements OnModuleInit {
   async vectorSearch(query: string, k = 5) {
     if (!query) throw new Error('Query string cannot be empty');
 
-    // Step 1: Embed the query text
     const vector = await this.embedText(query);
-
-    // Step 2: Use repository aggregation for vector search
-    const results = await this.repo.vectorSearch(vector, k, 50); // numCandidates = 50
-    // Step 3: Return results directly (they now include the full document + score)
-    return results.map(r => ({
-      ...r,      // all FFBProduction fields
-      _id: r._id.toString(), // convert ObjectId to string if needed
-      score: r.score           // similarity score
+    const results = await this.repo.vectorSearch(vector, k, 50);
+    return results.map((r) => ({
+      ...r,
+      _id: r._id.toString(),
+      score: r.score,
     }));
   }
-
 }

Some files were not shown because too many files changed in this diff