Skip to main content
All Projects
COMPLETED

VeritasRAG

Production-grade RAG document Q&A platform: upload PDFs, DOCX, and TXT files, then query them with grounded, cited answers powered by hybrid vector search and Gemini

VeritasRAG screenshot 1

Role

Full Stack Developer

Team

Solo

Stack
Frontend
Next.js 16ReactTypeScriptTailwind CSSshadcn/uiTanStack Query
Backend
Django 5FastAPIPythonCelery
Data
PostgreSQL (Neon)pgvectorRedis (Upstash)Cloudinary
AI & Retrieval
Gemini 2.0 Flashtext-embedding-004ms-marco-MiniLM-L-6-v2BM25 + RRF
Infra
Docker ComposeVercelRender
Challenges
  • Proxying SSE stream from FastAPI through Django and a Next.js Route Handler to the browser without buffering or timeout
  • Hybrid ANN + BM25 reciprocal rank fusion where RRF scores (~0.016) must not be compared against cosine similarity thresholds
  • Cross-encoder reranker (~80MB) loaded at FastAPI startup within Render free-tier 512MB RAM constraint
  • httpOnly JWT cookies set exclusively via Next.js Route Handlers: no token ever reaches browser JavaScript
  • Atomic document deletion cascading Celery ingestion status, pgvector chunks, embeddings, and Cloudinary asset in one transaction
  • Intent-aware retrieval routing: combined intent classification + HyDE in a single LLM call with standalone query rewriting for follow-up messages
Insights
  • pgvector HNSW cosine ANN combined with PostgreSQL BM25 full-text search via reciprocal rank fusion for higher recall on technical documents
  • Cross-encoder reranker (ms-marco-MiniLM-L-6-v2) scoring merged top-20 candidates down to top-5 before generation
  • FastAPI internal-only architecture: Django is the sole public-facing API; FastAPI enforces X-Internal-Key on every endpoint
  • Celery async ingestion pipeline decoupling upload from extract, chunk, embed, and pgvector write
  • Redis cache-aside at three layers: full RAG response (1h), retrieved chunks (30min), dashboard stats (5min)
  • Grounding score threshold gating: explicit low-confidence warning when top similarity scores fall below 0.75

Overview

VeritasRAG is a full-stack, production-grade Retrieval-Augmented Generation platform. Users upload PDF, DOCX, and TXT documents, which are ingested asynchronously through a multi-stage pipeline: text extraction, sliding-window chunking, embedding via Google text-embedding-004, and storage in pgvector. Once indexed, users query documents through a persistent chat interface and receive answers grounded strictly in retrieved source content, never hallucinated, with exact citations showing chunk text, source document, page number, and similarity score.

The system is a three-service architecture: a Next.js 16 (App Router) frontend, a Django 5 backend owning auth, document metadata, chat persistence, and Redis caching, and a FastAPI AI service owning the entire RAG pipeline. All services run in Docker Compose locally and deploy independently to Render and Vercel.

3

Independent services

512 / 50

Chunk tokens / overlap

768-d

HNSW vectors, pgvector

20 to 5

Reranked candidates


Key Features

Document management

Upload PDF, DOCX, TXT (max 50MB): the browser uploads directly to Cloudinary via a Django-signed URL, so credentials never reach the client. Celery tracks uploaded / processing / ready / failed status in PostgreSQL, polled by the frontend every 3 seconds. Deleting a document cascades chunks, embeddings, the Cloudinary asset, and related chat messages atomically.

RAG pipeline

Extraction via pdfplumber, python-docx, or plain read. Sliding-window chunking at 512 tokens with 50-token overlap preserving page numbers. text-embedding-004 produces 768-dimension vectors stored in pgvector with an HNSW index. Generation on gemini-2.0-flash with a strict grounding prompt, streamed token-by-token over SSE.

Hybrid retrieval + reranking

pgvector cosine ANN (top-20) merged with PostgreSQL BM25 full-text search (top-20) via Reciprocal Rank Fusion, then cross-encoder/ms-marco-MiniLM-L-6-v2 reranks the merged set down to the top-5 passed to generation.

Intent-aware retrieval

A single LLM call combines intent classification and HyDE to avoid a redundant round-trip on factual queries. Ten intent categories, standalone query rewriting for follow-up messages using conversation history, and top_k tuned per intent: 20 for comparison/factual, 15 for boolean/definition.

Citation grounding

Every answer ships citation cards with chunk text, source document, page number, and similarity score. A per-answer grounding_score renders a warning banner below 0.6, and if all top-5 similarities fall under 0.75 the response explicitly states the documents lack sufficient information. Low-confidence queries are logged for admin review.

Chat interface

Named chat sessions scoped to one or more documents, progressive streaming render as SSE tokens arrive, a collapsible citation panel per answer, full session history in the sidebar, and per-message metadata: latency, retrieval score, cache hit/miss badge.

Redis caching (cache-aside)

Full RAG response cached by hash(question + doc_ids) for 1 hour, retrieved chunks for 30 minutes, dashboard stats for 5 minutes, JWT session data for 24 hours. Cache hits return in under 5ms, with the hit badge visible in the UI and hit rate on the dashboard.

Auth, security, observability

JWT access (15 min) and refresh (7 day) tokens set as httpOnly; Secure; SameSite=Strict cookies exclusively by Next.js Route Handlers. Every Django view enforces row-level ownership. Every query is logged with latency, similarity, grounding score, cache hit, and model. Health checks and a keep-alive ping warm Render cold starts.


Architecture

Three independently deployable services behind a Next.js BFF layer:

Browser
  └── Next.js 16 (Vercel)
        ├── Route Handlers (BFF): cookie management, auth proxy, SSE proxy, upload signature
        └── App Router pages: dashboard, documents, chat, citations

Next.js Route Handlers
  └── Django 5 (Render Web Service)
        ├── Auth: SimpleJWT signup/login/logout/refresh
        ├── Documents: CRUD, Cloudinary signature, status polling
        ├── Chat: session + message persistence, Redis cache-aside, SSE proxy
        ├── Stats: dashboard aggregates (Redis cached)
        └── Celery dispatcher → Redis broker

Celery Worker (Render Background Worker, same Django image)
  └── POST /ingest → FastAPI

FastAPI (Render Web Service, internal-only)
  ├── POST /ingest: extract → chunk → embed → pgvector write
  ├── POST /query: embed → hybrid search → rerank → generate → SSE stream
  └── GET /health

PostgreSQL + pgvector (Neon)  |  Redis (Upstash)  |  Files (Cloudinary)
Internal-only AI service

Django is the sole public-facing API. FastAPI enforces an X-Internal-Key header on every endpoint and only Django calls it, so the entire RAG surface is unreachable from the internet and auth lives in exactly one place.

512MB RAM budget

The ~80MB cross-encoder reranker loads once at FastAPI startup and has to fit alongside the app inside Render's free-tier 512MB limit, which shaped model choice (ms-marco-MiniLM-L-6-v2) and ruled out larger rerankers.

Ingestion Flow

POST /api/documents/ (Django)
  → DOCUMENT record (status = uploaded)
  → Celery task dispatched

Celery: process_document(document_id)
  → FastAPI POST /ingest {document_id, storage_key, file_type}
  → fetch file from Cloudinary
  → extract text → chunk (512t, 50 overlap) → embed (text-embedding-004)
  → write CHUNKS + EMBEDDINGS to pgvector
  → return {chunk_count}
  → DOCUMENT.status = ready

Query Flow

user sends message
  → Django: Redis cache check hash(question + doc_ids)
  → cache hit: return in <5ms
  → cache miss: FastAPI POST /query (httpx streaming)
      → embed question (text-embedding-004)
      → pgvector HNSW ANN top-20 + BM25 FTS top-20
      → Reciprocal Rank Fusion merge
      → cross-encoder rerank top-20 → top-5
      → grounding check: all scores < 0.75 → grounding_score = 0.0
      → grounding prompt + gemini-2.0-flash SSE stream
  → Django proxies SSE → Next.js Route Handler → browser
  → on complete: persist MESSAGE + CITATIONS + QUERY_LOG; cache response
RRF scores are not similarities

Reciprocal Rank Fusion produces scores around 0.016 that live on a completely different scale from cosine similarity, so grounding thresholds (0.75) are checked against the raw ANN similarities, never the fused rank scores.


Data Model

Eight PostgreSQL tables plus pgvector:

  • users: UUID PK, email, hashed password, full name.
  • documents: user-owned; filename, storage_key, file_type, status, chunk_count.
  • chunks: document-scoped; chunk_index, content, token_count, page_number.
  • embeddings: one-to-one with chunks; vector(768), model_name. Decoupled so re-embedding a new model doesn't touch chunk data.
  • chat_sessions: user-owned; title, document_ids (jsonb array).
  • messages: session-scoped; role, content, retrieval_score, grounding_score, latency_ms, cache_hit.
  • citations: message-scoped; chunk_id FK, similarity_score, citation_order.
  • query_logs: full observability row per query: question, latency, grounding score, cache hit, model used, low_confidence flag.

Outcome

VeritasRAG demonstrates a production-grade RAG system: hybrid vector + keyword retrieval, cross-encoder reranking, intent-aware query routing, streaming SSE generation, Redis multi-layer caching, and strict citation grounding across a three-service Docker-native architecture. Every answer is traceable to an exact source chunk with page-level provenance, and every architectural boundary is enforced at both the API and database layers.