In this tutorial, we will create a context-aware to-do list application using Retrieval-Augmented Generation (RAG). We'll leverage Google's Gemini API for text embeddings, PgVector for efficient vector storage, and Prisma with NestJS for managing the PostgreSQL database. This setup will allow advanced features like cleaning up duplicate tasks and retrieving contextually similar tasks.
Prerequisites
- Basic understanding of NestJS and Prisma.
- Node.js and npm installed.
- A PostgreSQL database with PgVector extension enabled.
- Access to Google Cloud with the Gemini API key.
Step 1: Set Up NestJS Project
- Create a new NestJS project:
nest new todo-app
cd todo-app
- Remove unnecessary default files:
rm src/app.controller.* src/app.service.* src/app.module.ts
Step 2: Install Dependencies
Install the required dependencies:
npm install prisma @prisma/client @google/generative-ai dotenv
Step 3: Configure Prisma with PgVector
- Initialize Prisma:
npx prisma init
- Update the
.env
file with your PostgreSQL database credentials:
DATABASE_URL="postgresql://<username>:<password>@localhost:5432/<database>?schema=public"
- Enable PgVector in your
schema.prisma
file:
generator client {
provider = "prisma-client-js"
previewFeatures = ["postgresqlExtensions"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
extensions = [pgvector]
}
model Task {
id Int @id @default(autoincrement())
title String
content String
embedding Unsupported("vector(1536)")
}
- Apply the database migration:
npx prisma migrate dev --name init
Step 4: Configure Prisma in NestJS
Create a PrismaModule
for database access:
// src/prisma/prisma.module.ts
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
// src/prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
Import the PrismaModule
in your main module:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { PrismaModule } from './prisma/prisma.module';
import { TasksModule } from './tasks/tasks.module';
@Module({
imports: [PrismaModule, TasksModule],
})
export class AppModule {}
Step 5: Set Up the Tasks Module
- Generate the tasks module:
nest generate module tasks
nest generate service tasks
nest generate controller tasks
- Implement the
TasksService
:
// src/tasks/tasks.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Task } from '@prisma/client';
import { GeminiService } from '../gemini/gemini.service';
@Injectable()
export class TasksService {
constructor(private prisma: PrismaService, private geminiService: GeminiService) {}
async createTask(title: string, content: string): Promise<Task> {
const embedding = await this.geminiService.getEmbedding(`${title} ${content}`);
return this.prisma.task.create({
data: { title, content, embedding },
});
}
async getTasks(): Promise<Task[]> {
return this.prisma.task.findMany();
}
async findSimilarTasks(embedding: number[], limit = 5): Promise<any[]> {
const embeddingStr = `[${embedding.join(',')}]`;
return this.prisma.$queryRaw`
SELECT *, embedding <-> ${embeddingStr}::vector AS distance
FROM "Task"
ORDER BY embedding <-> ${embeddingStr}::vector
LIMIT ${limit};
`;
}
}
- Implement the
TasksController
:
// src/tasks/tasks.controller.ts
import { Controller, Post, Get, Body } from '@nestjs/common';
import { TasksService } from './tasks.service';
@Controller('tasks')
export class TasksController {
constructor(private tasksService: TasksService) {}
@Post()
async createTask(@Body('title') title: string, @Body('content') content: string) {
return this.tasksService.createTask(title, content);
}
@Get()
async getTasks() {
return this.tasksService.getTasks();
}
}
Step 6: Integrate Gemini API for Embedding Generation
- Create a GeminiService:
// src/gemini/gemini.service.ts
import { Injectable } from '@nestjs/common';
import * as genai from '@google/generative-ai';
@Injectable()
export class GeminiService {
private client: genai.GenerativeLanguageServiceClient;
constructor() {
this.client = new genai.GenerativeLanguageServiceClient({
apiKey: process.env.GEMINI_API_KEY,
});
}
async getEmbedding(text: string): Promise<number[]> {
const result = await this.client.embedText({
model: 'models/text-embedding-001',
content: text,
});
return result.embedding;
}
}
Final Thoughts
With this setup, you have a fully functional to-do list application that can:
- Generate embeddings for task content using Gemini.
- Store embeddings in a PostgreSQL database using PgVector.
- Retrieve similar tasks based on their embeddings.
This architecture enables advanced features like semantic search and contextual data cleaning. Expand it further to build intelligent task management systems!
Top comments (0)