Back to Tracker
Phase 5 — Capstone

PrepMind — Capstone Project Guide

Build a full-stack RAG system using Firebase Genkit + Firestore Vector Search. This is a real, deployable project that demonstrates every skill from this course in one portfolio-ready app.

Firebase GenkitFirestore Vector Search RAG PipelineEmbeddings Streaming UIRAGAS Evaluation
What You're Building

PrepMind is an AI-powered interview preparation assistant. Users paste a job description, and PrepMind answers their follow-up questions using a curated knowledge base of interview guides, system design patterns, and company-specific tips — all retrieved via semantic search, never hallucinated.

Tech Stack

BackendFirebase Genkit (Node.js flows)
Vector StoreFirestore Vector Search (native KNN)
Embedding ModelGemini text-embedding-004 (768-dim)
LLMGemini 1.5 Flash (generation)
FrontendVanilla JS + Firebase Hosting
AuthFirebase Auth (Google login)
EvaluationRAGAS framework (Python)

Why This Stack?

Firestore Vector Search is a native KNN index on Firestore. No separate vector DB needed. Data lives next to your app data. Supports COSINE and DOT_PRODUCT distance. Perfect for apps already on Firebase.

Firebase Genkit is Google's open-source framework for building AI flows — typed, observable, deployable as Firebase Functions. Use it like LangChain but Google-native.
1 Firestore Schema & Vector Index Setup Config + Firestore Index

Set up the Firestore collection to store knowledge chunks with their vector embeddings.

// Firestore document schema (collection: "knowledge_chunks") { id: "chunk_001", source: "system-design-guide.pdf", page: 3, text: "A rate limiter controls the rate of requests...", embedding: [0.023, -0.14, 0.87, ...], // 768-dim float array metadata: { topic: "system-design", difficulty: "intermediate", createdAt: Timestamp } }
// firestore.indexes.json — create the vector index { "indexes": [], "fieldOverrides": [ { "collectionGroup": "knowledge_chunks", "fieldPath": "embedding", "indexes": [ { "order": "ASCENDING", "queryScope": "COLLECTION" }, { "vectorConfig": { "dimension": 768, "flat": {} } } ] } ] }
Deploy: firebase deploy --only firestore:indexes
Index creation takes 5–20 minutes for large collections. Check status in Firebase Console → Firestore → Indexes.
2 Ingestion Pipeline — Chunk, Embed, Store Ingestion Script

Write a script that reads source documents, chunks them, generates embeddings, and stores to Firestore.

// ingestion/ingest.js (run as Node script, not a Cloud Function) import { initializeApp } from 'firebase-admin/app'; import { getFirestore } from 'firebase-admin/firestore'; import { GoogleGenerativeAI } from '@google/generative-ai'; import { readFileSync } from 'fs'; initializeApp(); const db = getFirestore(); const genai = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); const embModel = genai.getGenerativeModel({ model: 'text-embedding-004' }); async function chunkText(text, chunkSize = 400, overlap = 80) { const words = text.split(' '); const chunks = []; for (let i = 0; i < words.length; i += (chunkSize - overlap)) { chunks.push(words.slice(i, i + chunkSize).join(' ')); } return chunks; } async function embed(text) { const result = await embModel.embedContent(text); return result.embedding.values; // 768-dim array } async function ingestDocument(filePath, topic) { const text = readFileSync(filePath, 'utf8'); const chunks = await chunkText(text); for (let i = 0; i < chunks.length; i++) { const embedding = await embed(chunks[i]); await db.collection('knowledge_chunks').add({ source: filePath, text: chunks[i], embedding: FieldValue.vector(embedding), metadata: { topic, chunkIndex: i }, createdAt: FieldValue.serverTimestamp() }); console.log(`Stored chunk ${i+1}/${chunks.length}`); await new Promise(r => setTimeout(r, 100)); // rate limit } } await ingestDocument('./docs/system-design.md', 'system-design');
3 Retrieval Flow with Firebase Genkit Genkit Flow

Define a Genkit flow that takes a user query, embeds it, retrieves top-K chunks via Firestore vector search, and generates an answer.

// functions/src/flows/ragFlow.ts import { defineFlow, run } from '@genkit-ai/flow'; import { gemini15Flash, textEmbedding004 } from '@genkit-ai/googleai'; import { getFirestore, FieldValue } from 'firebase-admin/firestore'; import { embed } from '@genkit-ai/ai/embedder'; import { generate } from '@genkit-ai/ai'; import * as z from 'zod'; const db = getFirestore(); export const ragFlow = defineFlow( { name: 'ragFlow', inputSchema: z.string(), outputSchema: z.string() }, async (query) => { // 1. Embed the query const queryEmbedding = await run('embed-query', async () => { const result = await embed({ embedder: textEmbedding004, content: query }); return result.embedding; }); // 2. Firestore Vector Search (KNN) const chunks = await run('retrieve', async () => { const snapshot = await db.collection('knowledge_chunks') .findNearest('embedding', FieldValue.vector(queryEmbedding), { limit: 5, distanceMeasure: 'COSINE' }) .get(); return snapshot.docs.map(d => d.data().text); }); // 3. Generate answer with context const context = chunks.join('\n\n---\n\n'); const response = await run('generate', async () => { return generate({ model: gemini15Flash, prompt: `You are an expert interview coach. Answer based ONLY on the context below. If the context doesn't contain the answer, say "I don't have information on that." Context: ${context} Question: ${query} Answer:`, config: { temperature: 0.2, maxOutputTokens: 512 } }); }); return response.text(); } );
4 Frontend — Streaming Chat UI Chat Interface

Build a chat interface that streams the LLM response token-by-token and shows source citations.

// public/app.js — call the Genkit flow via Firebase Functions import { getFunctions, httpsCallable } from 'firebase/functions'; const functions = getFunctions(); const askPrepMind = httpsCallable(functions, 'ragFlow'); async function sendMessage(query) { const chatDiv = document.getElementById('chat'); // Optimistic UI: show user message chatDiv.innerHTML += `<div class="msg user">${escHtml(query)}</div>`; const aiDiv = document.createElement('div'); aiDiv.className = 'msg ai'; aiDiv.textContent = 'Thinking...'; chatDiv.appendChild(aiDiv); try { const result = await askPrepMind({ query }); aiDiv.textContent = result.data.answer; // Show citations if (result.data.sources?.length) { const sources = result.data.sources.map(s => `<span class="source">${escHtml(s)}</span>` ).join(''); aiDiv.innerHTML += `<div class="sources">Sources: ${sources}</div>`; } } catch (e) { aiDiv.textContent = 'Error: ' + e.message; } } function escHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;') .replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
Streaming: For real token streaming, use the Genkit streaming API and Server-Sent Events (SSE) on the client. The callable function pattern above returns the full response — good enough for V1.
5 RAGAS Evaluation — Measure Quality Eval Report

Build a golden eval set and score your RAG pipeline using RAGAS metrics. This is what separates you from candidates who just vibe-check their AI features.

# eval/ragas_eval.py from ragas import evaluate from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall from datasets import Dataset # Golden test set — human curated (question, ground_truth, contexts, answer) test_data = { "question": [ "What is a rate limiter and when would you use one?", "Explain the CAP theorem.", ], "ground_truth": [ "A rate limiter controls the number of requests...", "CAP theorem states that a distributed system can only guarantee...", ], "contexts": [ ["A rate limiter controls the rate of requests a client can make..."], ["The CAP theorem states that any distributed data store can only..."], ], "answer": [ # Run your RAG system on each question and store actual answers here retrieve_and_generate("What is a rate limiter..."), retrieve_and_generate("Explain the CAP theorem."), ] } dataset = Dataset.from_dict(test_data) results = evaluate( dataset, metrics=[faithfulness, answer_relevancy, context_precision, context_recall] ) print(results) # Example output: # faithfulness: 0.91 answer_relevancy: 0.88 # context_precision: 0.85 context_recall: 0.79
Target scores: Faithfulness > 0.85 (answer is grounded), Context Precision > 0.75 (retrieved relevant chunks), Answer Relevancy > 0.80 (answered the question). Below these = tune chunking, retrieval, or prompt.
6 Deploy & Portfolio Presentation Live URL + GitHub

Deploy to Firebase Hosting + Cloud Functions. Write a README that explains the architecture like an engineering blog post.

# Deploy everything firebase deploy # Your app is live at: # https://prepflix-lms.web.app

README Must Include

  • Architecture diagram (draw.io or Excalidraw)
  • RAG pipeline explanation
  • Firestore vector index setup steps
  • RAGAS eval results (screenshot or table)
  • Cost analysis (tokens used, approx. price)
  • What you'd do differently at 10x scale

Interview Talking Points

  • "I chose Firestore Vector Search because it collocates vectors with document metadata, eliminating a cross-DB join."
  • "I measured faithfulness at 0.91 using RAGAS on a 50-question golden set."
  • "My chunking uses 400-word windows with 80-word overlap — I tuned this by testing context recall."
  • "At 10K QPS I'd add semantic caching (Redis) and shard the ingestion pipeline."
This project is your proof of work. Every AI interview you walk into, you can say: "I built a RAG system using Firestore Vector Search, measured it with RAGAS, and deployed it." That alone puts you in the top 10% of SDE candidates interviewing for AI roles.
Full Architecture at a Glance
┌─────────────────────────────┐ │ INGESTION PIPELINE │ │ (run once / on new docs) │ │ │ Source Docs ──→ │ Chunk (400w, 80 overlap) │ │ ↓ │ │ Embed (Gemini emb-004) │ │ ↓ │ │ Store → Firestore │ │ knowledge_chunks │ └─────────────────────────────┘ ┌─────────────────────────────┐ │ QUERY PIPELINE (live) │ │ │ User Query ──→ │ Embed query (emb-004) │ │ ↓ │ │ Firestore findNearest() │ ← COSINE KNN │ top-5 chunks │ │ ↓ │ │ Build prompt + context │ │ ↓ │ │ Gemini 1.5 Flash │ ← LLM │ ↓ │ │ Stream answer to UI │ └─────────────────────────────┘