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,'&').replace(/</g,'<')
.replace(/>/g,'>').replace(/"/g,'"');
}
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 │
└─────────────────────────────┘