manikanta.
← All workNext project →
Hybrid RAG · Aug 2026AI / ML

Scholarship Assistant

A question answering system for India's scholarship landscape. It tells a student what they qualify for and by when, and it is built around the fact that ordinary RAG gets this domain wrong in a way that is easy to miss and expensive for the student.

Role
Solo — retrieval, evaluation, deploy
Timeline
August 2026
Status
Live on Vercel and Render

The API sleeps on a free tier after 15 minutes, so the first request can take about 50 seconds. Live evaluation scores are published at /api/eval/latest, and query log trends at /api/health.

0.92
Threshold accuracy, from 0.83
1.00
Groundedness, from 0.93
44
Schemes in the corpus
0.85
Judge agreement with hand labels
The problem

Eligibility is numbers. "Minimum 80% marks" and "minimum 75% marks" produce nearly identical embeddings, so semantic search returns the wrong threshold and states it as fact. A student then wastes weeks on an application they were never eligible for, or misses a deadline that was never theirs to meet.

Measured on the corpus, plain semantic retrieval put 54 schemes in front of the model, and 44 of them, 81%, were ones the student could not access. Twelve schemes they actually qualified for never made the candidate list at all. Filtering after retrieval cannot fix that, because the right scheme was never retrieved.

Architecture
01Profile + questionmarks, income, category02 — FIRST, NOT LASTSQL filter6 columns, NULL = no rule03ChromaDBscoped to eligible ids04Grounding checkregex, then LLM judgeAnswer + citationsevery figure traceableOr a refusalsays what is missingMEASURED: WHAT HAPPENS WITHOUT STEP 02Plain semantic retrieval put 54 schemes in front of the model. 81% were ones the student could not access,and 12 schemes they did qualify for never reached the candidate list at all.
STAGE 01

Filter in SQL, first

Eligibility rules are extracted into typed columns at ingestion. Six nullable columns hold marks, income, age, course level, category and state, where NULL means no constraint. Comparisons happen in SQL before any retrieval, which is a relational query Chroma's metadata filter cannot express.

STAGE 02

Search only what survives

The surviving scheme ids are passed to ChromaDB as a metadata filter, so vector search with bge-small-en-v1.5 runs only inside schemes the student actually qualifies for. The model never sees the other 81%.

STAGE 03

Ground it, or refuse

Two independent checks. A regex verifies that every numeric claim in the answer appears in the retrieved text, then an LLM judge reviews the claims. If either fails, the system says what it does not know instead of guessing.

Evaluation
MeasureResultMethod
Overall correctness0.80 → 0.8840 hand-written questions
Threshold questions0.83 → 0.9212 profiles sitting at cutoffs
Answers with invented figures0.075 → 0.00Regex, then LLM judge
Correct refusals0.63 → 0.88Unanswerable trap questions
Stack
  • Python 3.12
  • FastAPI
  • PostgreSQL
  • ChromaDB
  • bge-small-en-v1.5
  • ONNX
  • Groq
  • Gemini
  • RAGAS
  • React 19
  • Vite
  • Neon
  • Vercel
  • Render
Deployment

React front end on Vercel, FastAPI on Render's 512 MB free tier, PostgreSQL on Neon. Postgres holds the durable copy of every embedding and Chroma is the index built from it, so a restart on ephemeral disk loses nothing. A cross-encoder reranker was built and then disabled in production: two evaluation runs scored 0.88 and 0.93, a five point swing that is two questions of run-to-run variance on a 40 question set, so the improvement could not be told apart from noise at that sample size. Every query is logged with its retrieval scores, abstention reason and latency, and the trends are public at /api/health.

What I'd do next

Want the walkthrough? I'll show you the repo.