Browse Source

ffb productino to use atlas vector DB

Dr-Swopt 3 weeks ago
parent
commit
21e1b4aaf3

+ 4 - 0
.gitignore

@@ -55,3 +55,7 @@ pids
 
 # Diagnostic reports (https://nodejs.org/api/report.html)
 report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+
+# Certs
+certs

File diff suppressed because it is too large
+ 659 - 37
package-lock.json


+ 2 - 0
package.json

@@ -29,12 +29,14 @@
     "@nestjs/platform-socket.io": "^11.1.3",
     "@nestjs/websockets": "^11.1.3",
     "@simplewebauthn/server": "^13.1.1",
+    "@xenova/transformers": "^2.17.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",
+    "mongodb": "^7.0.0",
     "mongoose": "^8.19.1",
     "passport": "^0.7.0",
     "passport-jwt": "^4.0.1",

+ 0 - 32
src/FFB/ffb-harvest.controller.ts

@@ -1,32 +0,0 @@
-import { Controller, Get, Post, Delete, Body, Param, Query } from '@nestjs/common';
-import { FFBHarvestService } from './ffb-harvest.service';
-import { FFBHarvest } from 'src/FFB/ffb-harvest.schema';
-
-@Controller('ffb-harvest')
-export class FFBHarvestController {
-  constructor(private readonly harvestService: FFBHarvestService) {}
-
-  @Post()
-  async create(@Body() body: FFBHarvest) {
-    console.log('POST /ffb-harvest');
-    return this.harvestService.create(body);
-  }
-
-  @Get()
-  async findAll(@Query() query: any) {
-    console.log('GET /ffb-harvest', query);
-    return this.harvestService.findAll(query);
-  }
-
-  @Get(':id')
-  async findById(@Param('id') id: string) {
-    console.log(`GET /ffb-harvest/${id}`);
-    return this.harvestService.findById(id);
-  }
-
-  @Delete(':id')
-  async delete(@Param('id') id: string) {
-    console.log(`DELETE /ffb-harvest/${id}`);
-    return this.harvestService.delete(id);
-  }
-}

+ 0 - 13
src/FFB/ffb-harvest.module.ts

@@ -1,13 +0,0 @@
-import { Module } from '@nestjs/common';
-import { FFBHarvestController } from './ffb-harvest.controller';
-import { FFBHarvestService } from './ffb-harvest.service';
-import { MongoModule } from 'src/mongo/mongo.module';
-
-@Module({
-  imports: [
-    MongoModule
-  ],
-  controllers: [FFBHarvestController],
-  providers: [FFBHarvestService],
-})
-export class FFBHarvestModule {}

+ 0 - 41
src/FFB/ffb-harvest.service.ts

@@ -1,41 +0,0 @@
-import { Injectable } from '@nestjs/common';
-import { MongoCoreService } from '../mongo/mongo-core.service';
-import { FFBHarvestRepository } from '../mongo/mongo-ffb-harvest.repository';
-import { FFBHarvest } from './ffb-harvest.schema';
-
-@Injectable()
-export class FFBHarvestService {
-  private repo: FFBHarvestRepository;
-
-  constructor(private readonly mongoCore: MongoCoreService) {}
-
-  /** Lazily get or create the repository */
-  private async getRepository(): Promise<FFBHarvestRepository> {
-    if (!this.repo) {
-      const db = await this.mongoCore.getDb();
-      this.repo = new FFBHarvestRepository(db);
-      await this.repo.init();
-    }
-    return this.repo;
-  }
-
-  async create(harvest: FFBHarvest) {
-    const repo = await this.getRepository();
-    return repo.create(harvest);
-  }
-
-  async findAll(filter: Record<string, any> = {}) {
-    const repo = await this.getRepository();
-    return repo.findAll(filter);
-  }
-
-  async findById(id: string) {
-    const repo = await this.getRepository();
-    return repo.findById(id);
-  }
-
-  async delete(id: string) {
-    const repo = await this.getRepository();
-    return repo.delete(id);
-  }
-}

+ 45 - 0
src/FFB/ffb-production.controller.ts

@@ -0,0 +1,45 @@
+import { Controller, Get, Post, Delete, Body, Param, Query } from '@nestjs/common';
+import { FFBProduction } from './ffb-production.schema';
+import { FFBProductionService } from './ffb-production.service';
+
+@Controller('ffb-production')
+export class FFBProductionController {
+  constructor(private readonly ffbService: FFBProductionService) {}
+  
+  /** Vector search endpoint */
+  @Get('search')
+  async search(@Query('q') q: string, @Query('k') k?: string) {
+    console.log(`GET /ffb-production/search?q=${q}&k=${k}`);
+    const topK = k ? parseInt(k, 10) : 5;
+    return this.ffbService.search(q, topK);
+  }
+  
+  /** Create a new FFB production record (with embedding) */
+  @Post()
+  async create(@Body() body: FFBProduction) {
+    console.log('POST /ffb-production');
+    return this.ffbService.create(body);
+  }
+
+  /** Find all records */
+  @Get()
+  async findAll(@Query() query: any) {
+    console.log('GET /ffb-production', query);
+    return this.ffbService.findAll(query);
+  }
+
+  /** Find a record by ID */
+  @Get(':id')
+  async findById(@Param('id') id: string) {
+    console.log(`GET /ffb-production/${id}`);
+    return this.ffbService.findById(id);
+  }
+
+  /** Delete a record by ID */
+  @Delete(':id')
+  async delete(@Param('id') id: string) {
+    console.log(`DELETE /ffb-production/${id}`);
+    return this.ffbService.delete(id);
+  }
+
+}

+ 14 - 0
src/FFB/ffb-production.module.ts

@@ -0,0 +1,14 @@
+import { Module } from '@nestjs/common';
+import { FFBProductionController } from './ffb-production.controller';
+import { FFBProductionService } from './ffb-production.service';
+import { MongoModule } from 'src/mongo/mongo.module';
+import { FFBVectorService } from './ffb-vector.service';
+
+@Module({
+  imports: [
+    MongoModule
+  ],
+  controllers: [FFBProductionController],
+  providers: [FFBProductionService, FFBVectorService],
+})
+export class FFBProductionModule {}

+ 2 - 4
src/FFB/ffb-harvest.schema.ts → src/FFB/ffb-production.schema.ts

@@ -1,11 +1,9 @@
-export interface FFBHarvest {
+export interface FFBProduction {
   _id?: string;
-  harvestDate: Date;
+  productionDate: Date;
   site: string;
   phase: string;
   block: string;
-  harvester: string;
-  daysOfWork: number;
   weight: number;
   weightUom: string;
   quantity: number;

+ 54 - 0
src/FFB/ffb-production.service.ts

@@ -0,0 +1,54 @@
+import { Injectable } from '@nestjs/common';
+import { MongoCoreService } from '../mongo/mongo-core.service';
+import { FFBProduction } from './ffb-production.schema';
+import { FFBProductionRepository } from 'src/mongo/mongo-ffb-production.repository';
+import { FFBVectorService } from './ffb-vector.service';
+
+@Injectable()
+export class FFBProductionService {
+  private repo: FFBProductionRepository;
+
+  constructor(
+    private readonly mongoCore: MongoCoreService,
+    private readonly vectorService: FFBVectorService, // Inject vector service
+  ) {}
+
+  /** Lazily get or create the repository */
+  private async getRepository(): Promise<FFBProductionRepository> {
+    if (!this.repo) {
+      const db = await this.mongoCore.getDb();
+      this.repo = new FFBProductionRepository(db);
+      await this.repo.init();
+    }
+    return this.repo;
+  }
+
+  /** Create a new record with embedding */
+  async create(record: FFBProduction) {
+    // Use vector service to insert with embedding
+    return this.vectorService.insertWithVector(record);
+  }
+
+  /** Find all records (no embedding required) */
+  async findAll(filter: Record<string, any> = {}) {
+    const repo = await this.getRepository();
+    return repo.findAll(filter);
+  }
+
+  /** Find a record by ID (no embedding required) */
+  async findById(id: string) {
+    const repo = await this.getRepository();
+    return repo.findById(id);
+  }
+
+  /** Delete a record by ID (no embedding required) */
+  async delete(id: string) {
+    const repo = await this.getRepository();
+    return repo.delete(id);
+  }
+
+  /** Search for top-k similar records using a text query */
+  async search(query: string, k = 5) {
+    return this.vectorService.vectorSearch(query, k);
+  }
+}

+ 72 - 0
src/FFB/ffb-vector.service.ts

@@ -0,0 +1,72 @@
+import { Injectable, OnModuleInit } from '@nestjs/common';
+import { pipeline, FeatureExtractionPipeline } from '@xenova/transformers';
+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
+
+  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;
+
+    const db = await this.mongoCore.getDb();
+    this.repo = new FFBProductionRepository(db);
+    await this.repo.init();
+
+    console.log(`✅ Embedding model loaded and repository ready.`);
+  }
+
+  /** Convert an FFBProduction record into text 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 */
+  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}`);
+    }
+    return vector;
+  }
+
+  /** Insert a single record with embedding vector */
+  async insertWithVector(record: FFBProduction) {
+    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);
+  }
+
+  /** Search for top-k similar records using a text query */
+  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
+    }));
+  }
+
+}

+ 2 - 6
src/app.module.ts

@@ -1,8 +1,6 @@
 import { Module } from '@nestjs/common';
 import { AppController } from './app.controller';
 import { AuthModule } from './auth/auth.module';
-import { AttendanceModule } from './attendance/attendance.module';
-import { PaymentModule } from './payment/payment.module';
 import { PlantationTreeModule } from './plantation/plantation-tree.module';
 import { AppService } from './app.service';
 import { MongooseModule } from '@nestjs/mongoose';
@@ -10,18 +8,16 @@ import { mongooseConfig } from './config/mongoose.config';
 import { ServiceModule } from './services/service.module';
 import { ActivityModule } from './activity/activity.module';
 import { MongoModule } from './mongo/mongo.module';
-import { FFBHarvestModule } from './FFB/ffb-harvest.module';
+import { FFBProductionModule } from './FFB/ffb-production.module';
 
 @Module({
   imports: [
     MongooseModule.forRootAsync({
       useFactory: () => mongooseConfig,
     }),
-    FFBHarvestModule,
+    FFBProductionModule,
     MongoModule,
     AuthModule,
-    AttendanceModule,
-    PaymentModule,
     PlantationTreeModule,
     ServiceModule,
     ActivityModule

+ 0 - 40
src/attendance/attendance.controller.ts

@@ -1,40 +0,0 @@
-import {
-    Controller,
-    Post,
-    Body,
-    UseGuards,
-    Request,
-    Logger,
-} from '@nestjs/common';
-import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard';
-import { AttendancePayload } from 'src/interface/interface';
-import { AttendanceService } from 'src/attendance/attendance.service';
-
-@Controller('attendance')
-export class AttendanceController {
-    private logger: Logger = new Logger(`Attendance Controller`)
-    private service: AttendanceService
-
-    constructor(attendanceService: AttendanceService) {
-        this.service = attendanceService
-    }
-
-    @UseGuards(JwtAuthGuard)
-    @Post()
-    submitAttendance(
-        @Request() req,
-        @Body() body: AttendancePayload
-    ) {
-        const user = req.user; // ← comes from the token
-        const { date } = body;
-        this.service.emit({ name: user.name, date: body.date })
-        return {
-            message: `Attendance received for ${user.name} on ${new Date(date).toDateString()}`,
-            user: {
-                id: user.sub,
-                name: user.name,
-                email: user.email,
-            },
-        };
-    }
-}

+ 0 - 13
src/attendance/attendance.module.ts

@@ -1,13 +0,0 @@
-import { Module } from '@nestjs/common';
-import { AttendanceController } from './attendance.controller';
-import { AttendanceService } from 'src/attendance/attendance.service';
-import { SocketGateway } from 'src/gateway/socket.gateway';
-import { SocketModule } from 'src/gateway/socket.module';
-
-@Module({
-  controllers: [AttendanceController],
-  imports: [SocketModule],
-  exports: [], // if you have services to share with other modules
-  providers: [AttendanceService]
-})
-export class AttendanceModule { }

+ 0 - 17
src/attendance/attendance.service.ts

@@ -1,17 +0,0 @@
-import { Injectable } from '@nestjs/common';
-import { SocketGateway } from 'src/gateway/socket.gateway';
-import { AttendancePayload } from 'src/interface/interface';
-
-@Injectable()
-export class AttendanceService {
-    attendanceRecord: AttendancePayload[] = []
-
-    constructor(private socket: SocketGateway) {
-    }
-
-    public emit(attendance: AttendancePayload) {
-        console.log(`Processing attendance for ${attendance.name} at ${attendance.date}`)
-        this.attendanceRecord.push(attendance)
-        this.socket.emit({ action: `Attendance`, name: attendance.name, time: attendance.date })
-    }
-}

+ 0 - 51
src/mongo/mongo-ffb-harvest.repository.ts

@@ -1,51 +0,0 @@
-import { Db, ObjectId, WithId } from 'mongodb';
-import { FFBHarvest } from 'src/FFB/ffb-harvest.schema';
-export class FFBHarvestRepository {
-  private readonly collectionName = 'FFB Harvest';
-
-  constructor(private readonly db: Db) {}
-
-  private get collection() {
-    return this.db.collection<FFBHarvest>(this.collectionName);
-  }
-
-  async init() {
-    const collections = await this.db.listCollections({ name: this.collectionName }).toArray();
-    if (collections.length === 0) {
-      await this.db.createCollection(this.collectionName, {
-        timeseries: {
-          timeField: 'harvestDate',
-          metaField: 'site',
-          granularity: 'days',
-        },
-      });
-      console.log(`✅ Created time series collection: ${this.collectionName}`);
-    }
-  }
-
-  async create(harvest: FFBHarvest): Promise<FFBHarvest> {
-    const result = await this.collection.insertOne({
-      ...harvest,
-      harvestDate: new Date(harvest.harvestDate),
-    });
-    return { ...harvest, _id: result.insertedId.toString() };
-  }
-
-  async findAll(filter: Record<string, any> = {}): Promise<FFBHarvest[]> {
-    const results = await this.collection.find(filter).toArray();
-    // Convert ObjectId to string for frontend-friendliness
-    return results.map((r: WithId<FFBHarvest>) => ({
-      ...r,
-      _id: r._id?.toString(),
-    }));
-  }
-
-  async findById(id: string): Promise<FFBHarvest | null> {
-    const result = await this.collection.findOne({ _id: new ObjectId(id) as any });
-    return result ? { ...result, _id: result._id.toString() } : null;
-  }
-
-  async delete(id: string) {
-    return this.collection.deleteOne({ _id: new ObjectId(id) as any });
-  }
-}

+ 88 - 0
src/mongo/mongo-ffb-production.repository.ts

@@ -0,0 +1,88 @@
+import { Db, ObjectId, WithId } from 'mongodb';
+import { FFBProduction } from 'src/FFB/ffb-production.schema';
+
+export class FFBProductionRepository {
+  private readonly collectionName = 'FFB Production';
+
+  constructor(private readonly db: Db) { }
+
+  private get collection() {
+    return this.db.collection<FFBProduction & { vector?: number[] }>(this.collectionName);
+  }
+
+  async init() {
+    const collections = await this.db.listCollections({ name: this.collectionName }).toArray();
+    if (collections.length === 0) {
+      await this.db.createCollection(this.collectionName);
+      console.log(`✅ Created collection: ${this.collectionName}`);
+    }
+  }
+  async create(ffb: FFBProduction & { vector?: number[] }): Promise<FFBProduction & { vector?: number[] }> {
+    const result = await this.collection.insertOne({
+      ...ffb,
+      productionDate: new Date(ffb.productionDate),
+      vector: ffb.vector, // optional vector
+    });
+    return { ...ffb, _id: result.insertedId.toString() };
+  }
+
+  async findAll(filter: Record<string, any> = {}): Promise<(FFBProduction & { vector?: number[] })[]> {
+    const results = await this.collection.find(filter).toArray();
+    return results.map((r: WithId<FFBProduction & { vector?: number[] }>) => ({
+      ...r,
+      _id: r._id?.toString(),
+    }));
+  }
+
+  async findById(id: string): Promise<(FFBProduction & { vector?: number[] }) | null> {
+    const result = await this.collection.findOne({ _id: new ObjectId(id) as any });
+    return result ? { ...result, _id: result._id.toString() } : null;
+  }
+
+  async delete(id: string) {
+    return this.collection.deleteOne({ _id: new ObjectId(id) as any });
+  }
+
+  /** Optional: helper for vector search via aggregation */
+  async vectorSearch(vector: number[], k = 5, numCandidates = 50) {
+    return this.collection
+      .aggregate([
+        {
+          $vectorSearch: {
+            index: 'vector_index',
+            queryVector: vector,
+            path: 'vector',
+            numCandidates,
+            limit: k
+          }
+        },
+        {
+          $lookup: {
+            from: this.collectionName,
+            localField: "_id",
+            foreignField: "_id",
+            as: "doc"
+          }
+        },
+        { $unwind: "$doc" },
+        {
+          $project: {
+            _id: "$doc._id",
+            productionDate: "$doc.productionDate",
+            site: "$doc.site",
+            phase: "$doc.phase",
+            block: "$doc.block",
+            quantity: "$doc.quantity",
+            quantityUom: "$doc.quantityUom",
+            weight: "$doc.weight",
+            weightUom: "$doc.weightUom",
+            // vector: "$doc.vector",
+            score: "$_score"
+          }
+        }
+      ])
+      .toArray();
+  }
+
+
+}

+ 0 - 40
src/payment/payment.controller.ts

@@ -1,40 +0,0 @@
-import {
-    Controller,
-    Post,
-    Body,
-    UseGuards,
-    Request,
-    Logger,
-} from '@nestjs/common';
-import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard';
-import { PaymentPayload } from 'src/interface/interface';
-import { PaymentService } from 'src/payment/payment.service';
-
-@Controller('payment')
-export class PaymentController {
-    private logger: Logger = new Logger(`Payment Controller`)
-    private service: PaymentService
-
-    constructor(attendanceService: PaymentService) {
-        this.service = attendanceService
-    }
-
-    @UseGuards(JwtAuthGuard)
-    @Post()
-    submitPayment(
-        @Request() req,
-        @Body() body: PaymentPayload
-    ) {
-        const user = req.user; // ← comes from the token
-        const { date } = body;
-        this.service.emit({ name: user.name, date: body.date, verified: body.verified })
-        return {
-            message: `Payment received for ${user.name} on ${new Date(date).toDateString()}`,
-            user: {
-                id: user.sub,
-                name: user.name,
-                email: user.email,
-            },
-        };
-    }
-}

+ 0 - 12
src/payment/payment.module.ts

@@ -1,12 +0,0 @@
-import { Module } from '@nestjs/common';
-import { PaymentController } from './payment.controller';
-import { PaymentService } from 'src/payment/payment.service';
-import { SocketModule } from 'src/gateway/socket.module';
-
-@Module({
-  controllers: [PaymentController],
-  imports: [SocketModule],
-  exports: [], // if you have services to share with other modules
-  providers: [PaymentService]
-})
-export class PaymentModule {}

+ 0 - 16
src/payment/payment.service.ts

@@ -1,16 +0,0 @@
-import { Injectable } from '@nestjs/common';
-import { SocketGateway } from 'src/gateway/socket.gateway';
-import { AttendancePayload, PaymentPayload } from 'src/interface/interface';
-
-@Injectable()
-export class PaymentService {
-    paymentRecord: PaymentPayload[] = []
-    constructor(private socket: SocketGateway) {
-    }
-
-    public emit(payment: PaymentPayload) {
-        console.log(`Processing payment for ${payment.name} at ${payment.date}`)
-        this.paymentRecord.push(payment)
-        this.socket.emit({ action: `Payment`, name: payment.name, time: payment.date })
-    }
-}

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