Module 8: Capstone RAG Project with ChromaDB

Capsule 08: Project - Production-Ready RAG

Capsule description

You've reached the guide's closure: deliver a complete RAG system, evaluated and deployable. This capsule consolidates everything built in the previous capsules and guides you step by step to integrate each piece into a portfolio-quality project.

Upon completing this capsule you'll have:

  • A RAG system with 1,000+ documents indexed in ChromaDB
  • A FastAPI API with /ingest, /search, /ask, /health, /metrics endpoints
  • A Docker deployment with docker-compose (rag-api + chromadb-server)
  • A test suite (unit, integration, performance, accuracy)
  • Observability (Prometheus, structured logging)
  • A README with architecture, API reference, and deployment guide

And most importantly: a solid base for Guide #8 (Advanced RAG Techniques), where you'll migrate to Pinecone, add re-ranking, and evaluate your RAG systematically.


Mandatory Deliverables

#DeliverableVerification
1System code (ingestion, retrieval, generation, API)docker-compose up works
2Docker configuration (Dockerfile, docker-compose.yml)One command brings everything up
3Test set and evaluation resultsAccuracy report in metrics-report.md
4Technical decisions documentDECISIONS.md or a section in the README
5Production readiness checklistTable in the README or a separate file

Final Project Structure

rag-project/
├── app/
│   ├── __init__.py
│   ├── main.py                 # FastAPI app, routers
│   ├── config.py               # Settings, env vars
│   ├── api/
│   │   ├── routes/
│   │   │   ├── ingest.py
│   │   │   ├── search.py
│   │   │   ├── ask.py
│   │   │   └── health.py
│   │   └── deps.py             # Dependencies (ChromaDB client, etc.)
│   ├── ingestion/
│   │   ├── chunking.py
│   │   ├── pipeline.py
│   │   └── loaders.py
│   ├── retrieval/
│   │   ├── retriever.py
│   │   └── filters.py
│   ├── generation/
│   │   └── generator.py
│   ├── cache/
│   │   ├── embedding_cache.py
│   │   └── result_cache.py
│   ├── metrics.py              # Prometheus
│   └── logging_config.py
├── tests/
│   ├── conftest.py
│   ├── unit/
│   ├── integration/
│   ├── performance/
│   └── evaluation/
│       └── golden_set.json
├── scripts/
│   ├── ingest_sample_docs.py    # Generate 1000+ test docs
│   └── run_evaluation.py
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
├── requirements-dev.txt
├── .env.example
├── README.md
├── DEPLOYMENT.md
├── CHANGELOG.md
└── metrics-report.md

Step-by-Step Integration Guide

Step 1: Initial setup (5 min)

mkdir -p rag-project/app/{api,ingestion,retrieval,generation,cache}
mkdir -p rag-project/tests/{unit,integration,performance,evaluation}
cd rag-project

Create requirements.txt:

fastapi>=0.104.0
uvicorn[standard]>=0.24.0
chromadb>=0.4.0
openai>=1.0.0
httpx>=0.24.0
pydantic-settings>=2.0.0
prometheus-client>=0.19.0
tenacity>=8.2.0
python-dotenv>=1.0.0

Step 2: Configuration (10 min)

# app/config.py
import os
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    chroma_host: str = os.getenv("CHROMA_HOST", "localhost")
    chroma_port: int = int(os.getenv("CHROMA_PORT", "8001"))
    chroma_collection: str = os.getenv("CHROMA_COLLECTION", "rag_docs")
    openai_api_key: str = os.getenv("OPENAI_API_KEY", "")
    chunk_size: int = 512
    chunk_overlap: int = 64
    top_k: int = 5

    class Config:
        env_file = ".env"

settings = Settings()

Step 3: Ingestion pipeline (15 min)

Implement app/ingestion/chunking.py, pipeline.py, and loaders.py following Capsule 03. The pipeline must:

  • Load documents (or generate 1000+ test ones)
  • Chunk with chunk_size=512, overlap=64
  • Generate embeddings with OpenAI
  • Insert into ChromaDB with stable IDs

Step 4: Retrieval and Generation (15 min)

Implement app/retrieval/retriever.py and app/generation/generator.py following Capsule 04. The contract:

  • retrieve(question, collection, top_k=5) → documents + metadatas
  • generate_answer(question, docs) → answer + sources

Step 5: FastAPI API (15 min)

# app/main.py
from fastapi import FastAPI
from app.api.routes import ingest, search, ask, health
from app.metrics import setup_metrics

app = FastAPI(title="RAG API", version="1.0.0")
app.include_router(health.router, tags=["health"])
app.include_router(ingest.router, prefix="/ingest", tags=["ingest"])
app.include_router(search.router, prefix="/search", tags=["search"])
app.include_router(ask.router, prefix="/ask", tags=["ask"])
setup_metrics(app)

Step 6: Docker and docker-compose (10 min)

Copy the Dockerfile and docker-compose.yml from Capsule 06. Adjust paths if your structure differs.

Step 7: Tests (20 min)

Copy the test structure from Capsule 05. Make sure you have:

  • Unit: chunking, filters
  • Integration: /health, /search, /ask
  • Performance: p95 < 2s, throughput > 20 QPS
  • Accuracy: golden set > 90%

Step 8: Hardening (15 min)

Apply what's in Capsule 07:

  • Retries for ChromaDB and OpenAI
  • Fallbacks in /ask
  • Rate limiting
  • README and DEPLOYMENT.md

Step 9: Final evaluation (10 min)

Run:

docker-compose up -d
pytest tests/ -v --tb=short
python scripts/run_evaluation.py

Generate metrics-report.md with the results.


Complete Integration Code (Summary)

app/main.py (skeleton)

from fastapi import FastAPI
from app.config import settings
from app.api.routes import health, search, ask, ingest

app = FastAPI(title="RAG API", version="1.0.0", docs_url="/docs")
app.include_router(health.router)
app.include_router(search.router, prefix="/search")
app.include_router(ask.router, prefix="/ask")
app.include_router(ingest.router, prefix="/ingest")

app/api/routes/ask.py (complete example)

from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel, Field
from app.retrieval.retriever import retrieve
from app.generation.generator import generate_answer
from app.config import settings
from app.deps import get_collection

router = APIRouter()

class AskPayload(BaseModel):
    question: str = Field(..., min_length=1, max_length=500)

@router.post("")
async def ask_endpoint(payload: AskPayload, collection=Depends(get_collection)):
    docs = await retrieve(payload.question, collection, top_k=settings.top_k)
    if not docs or not docs.get("documents") or not docs["documents"][0]:
        return {"answer": "I didn't find enough information.", "sources": [], "confidence": 0}
    answer = await generate_answer(payload.question, docs["documents"][0])
    return {
        "answer": answer,
        "sources": docs.get("metadatas", [[]])[0],
        "confidence": 0.85,
    }

Suggested Approval Criteria

CriterionThreshold
Accuracy on the golden set≥ 85% (90% target)
p95 latency /ask< 2.5s (2s target)
Throughput /search≥ 20 QPS
Main endpoints/health, /search, /ask operational
Failure recoveryFallbacks manually validated
ObservabilityStructured logs, /metrics accessible

Recommended Closing Rubric

AreaCriterionStatus
FunctionalityIngestion, search, and ask operational
QualityAccuracy and errors within threshold
OperationsObservability and minimal runbooks
ResilienceFallbacks and documented rollback
SecurityAuth + rate limiting + validation
DocumentationREADME, API reference, deployment

Example Project Result

## RAG Project Result

- **Accuracy:** 0.87 (threshold 0.85) ✅
- **p95 /ask:** 1.9s (threshold 2.5s) ✅
- **Throughput:** 24 QPS (target 20) ✅
- **Error rate:** 0.6% (threshold 1%) ✅
- **Final status:** Approved for controlled pilot

### Open risks
- Dependence on the LLM provider for traffic spikes
- Missing 10x traffic load test

### Next iteration
- Semantic caching of results
- Per-tenant observability improvements

What You Learned Throughout the Guide

Upon completing the 8 modules of the Vector Databases Fundamentals guide, you can now:

  1. Explain why RAG needs vector databases — SQL/NoSQL don't scale for similarity search; numpy doesn't manage millions of vectors in memory.

  2. Understand the internal architecture — indexing layer (HNSW, IVF), query engine, storage. You know what M and efConstruction do, and when they matter.

  3. Master ChromaDB — local setup, vector CRUD, metadata filtering, batch ingestion, persistence. You built a system that indexes 1,000+ documents.

  4. Compare the landscape — Pinecone, Weaviate, Qdrant, Milvus. You know managed vs self-hosted, pricing, and when to migrate.

  5. Decide which DB to use — You applied a decision framework (cost, scale, features) to choose technology.

  6. Prepare RAG for production — scaling, backups, monitoring, migrations, cost optimization.

  7. Build a complete RAG system — ingestion → ChromaDB → retrieval → generation → REST API, with Docker, tests, observability, and hardening.

What's missing (and comes in Guide #8):

  • ChromaDB → Pinecone migration (managed cloud)
  • Re-ranking with cross-encoders
  • Query optimization (expansion, rewriting)
  • Advanced chunking (recursive, semantic)
  • Systematic RAG evaluation (retrieval + generation metrics)

Completeness Checklist

Check each item as you complete it:

Functionality

  • Ingestion pipeline processes 1,000+ documents
  • ChromaDB persists data in a volume
  • /search returns results with scores and metadata
  • /ask generates answers with sources
  • /ingest accepts documents via API or script

Quality

  • Accuracy ≥ 85% on the golden set
  • p95 latency /ask < 2.5s
  • Error rate < 1%

Operations

  • Docker Compose brings everything up with one command
  • /health verifies ChromaDB
  • /metrics exposes Prometheus
  • Structured logs (JSON) with a trace_id

Resilience

  • Retries on ChromaDB and OpenAI
  • Fallbacks when retrieval or generation fail
  • Documented rollback plan

Security

  • Optional API key in headers
  • Rate limiting on /ask
  • Input validation (Pydantic)

Documentation

  • README with architecture, setup, API reference
  • DEPLOYMENT.md with instructions
  • CHANGELOG with relevant decisions
  • metrics-report.md with evaluation results

Connection with Guide #8 (Advanced RAG Techniques)

This project is the direct base for Guide #8. It's not throwaway code: it's the foundation that evolves.

In Guide #8 you'll do:

TransformationDescription
ChromaDB → PineconeMigration to a managed vector DB in the cloud
Retrieval + Re-rankingCross-encoders to improve top-k precision
Query optimizationExpansion, rewriting, decomposition
Advanced chunkingRecursive, semantic chunking
RAG evaluationRetrieval metrics (MRR, recall) + generation (faithfulness, relevance)

Prerequisites for Guide #8:

  • Stable, versioned project base (Git)
  • Minimal pilot metrics recorded
  • Improvements list prioritized by impact
  • Preliminary decision on migration (whether Pinecone fits your case)

Project Exercises with Detailed Solutions

Exercise 1: Ingestion script for 1,000 synthetic documents

Goal: Create scripts/ingest_sample_docs.py that generates 1,000 simulated Wikipedia documents and indexes them in ChromaDB.

Solution:

# scripts/ingest_sample_docs.py
import chromadb
from chromadb.utils import embedding_functions
import os

categories = ["science", "history", "technology", "arts", "sports"]
ef = embedding_functions.OpenAIEmbeddingFunction(api_key=os.getenv("OPENAI_API_KEY"))

client = chromadb.PersistentClient(path="./chroma_data")
collection = client.get_or_create_collection("rag_docs", embedding_function=ef)

docs, metadatas, ids = [], [], []
for i in range(1000):
    cat = categories[i % len(categories)]
    text = f"Article about {cat} topic {i}. This document contains information relevant to {cat}."
    docs.append(text)
    metadatas.append({"category": cat, "doc_id": i})
    ids.append(f"doc_{i}")

batch_size = 100
for i in range(0, len(docs), batch_size):
    collection.add(documents=docs[i:i+batch_size], metadatas=metadatas[i:i+batch_size], ids=ids[i:i+batch_size])
    print(f"Ingested {min(i+batch_size, len(docs))}/{len(docs)}")

print(f"Done. Total: {collection.count()} documents")

Exercise 2: Generate metrics-report.md automatically

Goal: A script that runs the evaluation and writes metrics-report.md with a results table.

Solution:

# scripts/run_evaluation.py
import httpx
import time
import json

def run_eval():
    base = "http://localhost:8000"
    latencies = []
    for _ in range(50):
        s = time.perf_counter()
        r = httpx.post(f"{base}/ask", json={"question": "What is RAG?"}, timeout=10)
        latencies.append(time.perf_counter() - s)
    latencies.sort()
    p95 = latencies[int(0.95 * len(latencies))]
    # Accuracy: assume the golden set in tests; simplified here
    report = f"""# Metrics Report

| Metric | Value | Threshold | Status |
|---------|-------|--------|--------|
| p95 /ask | {p95:.2f}s | <2.5s | {'✅' if p95 < 2.5 else '❌'} |
"""
    with open("metrics-report.md", "w") as f:
        f.write(report)
    print(report)

if __name__ == "__main__":
    run_eval()

Exercise 3: README with an architecture diagram

Goal: A complete README with an ASCII or Mermaid diagram, setup in 5 steps, and an API table.

Solution:

# RAG API with ChromaDB

Production-ready RAG system: 1,000+ docs, FastAPI, Docker, tests.

## Architecture

\`\`\`
┌─────────┐   ┌──────────┐   ┌─────────┐   ┌───────────┐   ┌────────┐
│Documents│──▶│ Chunking │──▶│Embeddings│──▶│ ChromaDB  │──▶│Retrieval│
└─────────┘   └──────────┘   └─────────┘   └───────────┘   └────┬────┘
                                                                 │
┌─────────┐   ┌──────────┐   ┌─────────┐                        │
│ Response │◀──│   LLM    │◀──│ Context │◀───────────────────────┘
└─────────┘   └──────────┘   └─────────┘
\`\`\`

## Setup (5 steps)

1. Clone: \`git clone <repo>\`
2. Copy env: \`cp .env.example .env\` and add \`OPENAI_API_KEY\`
3. Build: \`docker-compose build\`
4. Up: \`docker-compose up -d\`
5. Test: \`curl http://localhost:8000/health\`

## API Reference

| Endpoint | Method | Description |
|----------|--------|-------------|
| /health | GET | Health + ChromaDB status |
| /search?q=&top_k=5 | GET | Semantic search |
| /ask | POST | {\"question\": \"...\"} |
| /ingest | POST | Batch ingestion |
| /metrics | GET | Prometheus |

Project Troubleshooting Guide

SymptomProbable causeAction
/health returns 503ChromaDB unreachableVerify chromadb is up in docker-compose; check CHROMA_HOST
/ask takes >10sLLM timeout or slow embeddingsCheck OPENAI_API_KEY; reduce top_k
Accuracy < 85%Inadequate chunking or poor corpusAdjust chunk_size/overlap; expand the golden set
Docker build failsDependencies or memorydocker system prune; increase memory for Docker
Integration tests failAPI not runningdocker-compose up -d before pytest
ChromaDB empty after restartNon-persistent volumeVerify the volume is in docker-compose and docker-compose down -v is not used

Technical Decisions to Document (DECISIONS.md)

Include in your project a DECISIONS.md file with:

  1. Why ChromaDB: Free, local, sufficient for 1K-10K docs; migration to Pinecone planned for scale.
  2. Chunk size 512: Balance between context and granularity; validated with recall on the golden set.
  3. top_k = 5: Sufficient for most questions; acceptable latency.
  4. Embedding model: text-embedding-3-small for cost/quality for the MVP.
  5. LLM: GPT-3.5-turbo for generation; GPT-4 optional for complex cases.
  6. No Redis initially: In-memory cache for development; Redis in the next iteration.

Example CHANGELOG.md

# Changelog

## [1.0.0] - 2026-03-13

### Added
- Ingestion pipeline for 1,000+ documents
- Endpoints /health, /search, /ask, /ingest
- Docker deployment with docker-compose
- Test suite (unit, integration, performance, accuracy)
- Prometheus metrics and structured logging
- Retry logic and fallbacks for ChromaDB and OpenAI
- Rate limiting and input validation

### Decisions
- ChromaDB as the initial vector store (migration to Pinecone in Guide #8)
- chunk_size=512, overlap=64
- Thresholds: accuracy ≥85%, p95 <2.5s

Portfolio Quality Criteria

Your project is ready for a portfolio if:

  • A recruiter can clone and run it in < 5 minutes
  • The README clearly explains what it does and how to use it
  • There is evidence of tests (badge or mention in the README)
  • The code is organized with minimal types/documentation
  • There are documented technical decisions
  • You explicitly connect to "next step: Guide #8"

Pre-Delivery Verification Script

Run before marking the project as completed:

#!/bin/bash
# scripts/pre_submit_check.sh
set -e
echo "1. Docker Compose..."
docker-compose up -d
sleep 10

echo "2. Health check..."
curl -sf http://localhost:8000/health | jq .

echo "3. Search..."
curl -sf "http://localhost:8000/search?q=vector&top_k=3" | jq '.documents | length'

echo "4. Ask..."
curl -sf -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"question":"What is RAG?"}' | jq '.answer, .sources'

echo "5. Metrics..."
curl -sf http://localhost:8000/metrics | head -20

echo "6. Tests..."
pytest tests/ -v --tb=short -x

echo "✅ Pre-submit check passed"
docker-compose down

Integration with Previous Capsules

CapsuleWhat it contributes to this project
01-02Architecture and flow diagram
03Ingestion pipeline, chunking, batch
04Retrieval, generation, API contract
05Fixtures, unit/integration/performance/accuracy tests
06Dockerfile, docker-compose, Prometheus, logs
07Retries, fallbacks, rate limit, README
08Full integration and closing checklist

Recommended Delivery Format

Minimal structure to share (GitHub, portfolio):

project/
├── app/
├── tests/
├── scripts/
├── docker-compose.yml
├── Dockerfile
├── README.md           # Architecture, setup, API, troubleshooting
├── DEPLOYMENT.md       # Staging, production, rollback
├── CHANGELOG.md        # Decisions and versions
├── metrics-report.md   # Final metrics table
└── .env.example

Closing Troubleshooting

"We met functionality, failed on operations"

Don't close the project until you cover the minimums of observability and resilience. A system without health checks or fallbacks is not production-ready.

"Correct metrics on the test set, not on real traffic"

Incorporate real feedback (logs of failed questions) and recalibrate thresholds. The golden set is an approximation; real traffic can reveal edge cases.

"The team wants to move to the next guide already"

Close the critical hardening debt first. Migrating to Pinecone with a fragile system will drag problems along, multiplied.

"We don't have 1,000 real documents"

Use the synthetic documents script from Exercise 1. The goal is to validate that the pipeline scales; the content can be simulated.

"Docker build fails due to memory"

Reduce batch_size in ingestion or use a machine with more RAM for the build. In CI, consider building on a remote server.


Suggested Next Steps

  1. Define an improvements backlog — Top 5 prioritized by impact (e.g. caching, re-ranking, more documents).
  2. Prioritize retrieval evaluation — Recall@k, MRR with human or LLM-as-judge judgments.
  3. Prepare the transition to Guide #8 — Review the prerequisites, have a Pinecone API key if applicable.

Summary

  • You completed the capstone project of the Vector Databases Fundamentals guide.
  • You have a RAG system with ChromaDB, FastAPI, Docker, tests, observability, and hardening.
  • You know what you learned throughout the guide and what comes in Guide #8 (Advanced RAG).
  • You have a solid base to evolve toward advanced RAG techniques: migration to Pinecone, re-ranking, query optimization, systematic evaluation.

Congratulations! You built a system an AI Engineer would show in an interview. Continue with Guide #8 to take it to the next level.


Extended golden_set.json Template

For complete coverage, use at least 40 questions in your golden set:

[
  {"question": "What is a vector database?", "expected_keywords": ["vector", "embedding", "search"], "category": "frequent"},
  {"question": "How does HNSW work?", "expected_keywords": ["graph", "approximate", "neighbor"], "category": "frequent"},
  {"question": "When to use ChromaDB?", "expected_keywords": ["local", "development", "RAG"], "category": "frequent"},
  {"question": "What is RAG?", "expected_keywords": ["retrieval", "generation", "documents"], "category": "frequent"},
  {"question": "What is the difference between HNSW and IVF?", "expected_keywords": ["graph", "clustering"], "category": "difficult"},
  {"question": "What happened on March 15, 2030 on Mars?", "expected_keywords": ["i don't have", "evidence", "unknown"], "category": "out_of_coverage"}
]

Recommended categories: 20 frequent, 10 difficult, 10 out_of_coverage.


Comparison: Before vs After the Guide

AspectBefore the guideAfter Module 8
Vector DBYou didn't know when or whyYou understand the architecture, you use ChromaDB
RAGTheoretical conceptComplete system with ingestion, retrieval, generation
Production"Works on my machine"Docker, tests, metrics, hardening
Technology decisionIntuitionDocumented decision framework
Next stepUncertainGuide #8: Pinecone, re-ranking, evaluation

Additional Resources


Final Project FAQ

Can I use another LLM instead of OpenAI?
Yes. Replace app/generation/generator.py with the integration for Anthropic, a local LLM (Ollama), etc. The contract (question + docs → answer) stays the same.

Is ChromaDB enough for production?
For 1K-100K docs and moderate traffic, yes. For millions of vectors or multi-tenant at scale, Guide #8 with Pinecone is the way.

Do I need Redis for the cache?
It's not mandatory. You can start with an in-memory cache. Redis adds persistence and sharing the cache between replicas.

How do I add more documents after deploy?
Use POST /ingest with the new batch. The IDs must be unique. If you use deterministic IDs (e.g. content hash), re-ingestion is idempotent.

What do I do if accuracy doesn't reach 90%?
Review: (1) chunk_size and overlap, (2) top_k, (3) corpus quality, (4) the generation prompt. Add failed cases to the golden set and iterate.


Suggested Implementation Timeline

If you have 3-4 hours for the complete project:

BlockDurationActivity
145 minSetup, config, ingestion pipeline, 1000-docs script
245 minRetrieval, generation, API endpoints
345 minDocker, docker-compose, health, metrics
445 minTests (unit, integration), golden set, accuracy
530 minHardening (retries, fallbacks, rate limit)
630 minREADME, DEPLOYMENT.md, CHANGELOG, final evaluation

Adjust to your priorities. The critical thing: that it works end-to-end and is in Docker before refining details.


Transition Checklist to Guide #8

Before starting Guide #8 (Advanced RAG Techniques), verify:

  • Project in Git with coherent commits
  • README updated with the current status
  • Pilot metrics (accuracy, latency) recorded
  • Improvements list prioritized (top 5)
  • Pinecone account created (if you're going to migrate)
  • You understand what problems re-ranking and query optimization will solve

Estimated time: 60-90 minutes (complete integration)
Next guide: Advanced RAG Techniques — Migration to a more advanced stack and retrieval optimizations.


Final Message

You completed the 8 modules of the Vector Databases Fundamentals guide. You went from "what is a vector?" to "I have a RAG system deployed with ChromaDB, Docker, tests, and monitoring". That's a real achievement.

The project you built is not an academic exercise. It's a reusable base for Guide #8, where you'll migrate to Pinecone, add re-ranking, and evaluate your RAG systematically. Every decision you documented (chunk_size, top_k, ChromaDB vs managed) will help you justify changes in interviews and in production.

Continue with Guide #8 when you're ready. The AI Engineering path is iterative: what's "production-ready" today will be "an MVP we evolve" tomorrow. What matters is that you now know how to build that base.


Summary of Deliverables per Capsule

CapsuleKey deliverable
01-02Architecture diagram, contracts between components
03Ingestion pipeline for 1,000+ docs, validations
04/search, /ask endpoints with a stable contract
05Fixtures, unit/integration/performance/accuracy tests
06Dockerfile, docker-compose, Prometheus, logging
07Retries, fallbacks, rate limit, README
08Full integration, checklist, Guide #8 connection

Each deliverable is verifiable: you can mark the capsule as completed when the corresponding item works in your project.


Quick Verification Commands

Before considering the project closed, run:

docker-compose up -d && sleep 15
curl -s http://localhost:8000/health | jq
curl -s -X POST http://localhost:8000/ask -H "Content-Type: application/json" -d '{"question":"What is RAG?"}' | jq '.answer'
pytest tests/ -v --tb=line -q

If the three commands pass, your system is ready for delivery.