Job Matching — AI Recruitment Platform
A French-language recruitment platform that turns CVs and assessment quizzes into structured profiles, ranks every candidate against a job description with an explainable score, and recommends the training that would close each candidate's gaps.

The problem
Keyword matching on CVs fails in both directions: it rejects strong candidates who describe their experience in different words, and it passes weak ones who happen to mirror the job ad. Recruiters end up reading every CV anyway, so the filter saves nothing. And a score on its own is not actionable — a recruiter needs to know why a candidate scored what they did, and what would fix it.
Architecture
Ingestion — CV and quiz PDFs upload per department and return immediately; extraction runs as a background task while the UI polls a processing_status field. Text comes out via PyPDF2, then an LLM turns it into a structured profile: education, experience, skills, languages, and per-category quiz scores.
Extraction fallback — when the model returns something unusable, a heuristic section parser and a direct regex quiz parser take over. A bad parse degrades the profile rather than losing the candidate.
Matching — the job description PDF is extracted into structured criteria, then every candidate in scope is evaluated by the model as a French recruiter would: a 0–100 compatibility score with strengths, gaps, matching skills, a written analysis and recommendations. A failed call on one candidate is caught and skipped rather than aborting the batch.
Training recommendations — TF-IDF and cosine similarity over a training catalogue, run against each candidate's identified gaps. Top three programmes above a 0.1 relevance threshold, returned with objectives, duration and cost.
Semantic search — profiles embedded into Qdrant (1536-dim OpenAI or 768-dim Ollama), searched by cosine similarity in plain French, with results re-hydrated from MongoDB and filtered to the caller.
Platform — FastAPI with MongoDB as the source of truth, JWT auth with bcrypt and account lockout, per-user data isolation, and a statically exported Next.js frontend served by nginx with no Node runtime in production.

Key decisions
TF-IDF for training recommendations
Asking the LLM to pick the courses
Matching a candidate's gaps to a fixed catalogue is similarity, not reasoning. TF-IDF is faster, free, deterministic and explainable — and a recruiter can see exactly why a course was suggested. The LLM is reserved for the part that genuinely needs judgement: evaluating the candidate.
MongoDB as the source of truth, Qdrant only for vectors
Serving search results from the vector store payload
Embeddings go stale, and the collection is rebuilt whenever the embedding model changes — switching between 1536-dim and 768-dim models drops every vector. Re-hydrating each hit from Mongo means search can be re-indexed at any time without the results going wrong in the meantime.
Per-candidate error isolation inside the batch
Letting a failed call fail the run
Matching runs across a whole department. One malformed CV or one timed-out call should cost you one candidate, not the other forty — and the recruiter should still get a ranked list they can act on.
Fire-and-forget ingestion with a polled status field
Processing inside the upload request
LLM extraction on a multi-page CV takes far longer than a request should hold open. Returning a candidate id immediately and surfacing processing → completed / error in the UI keeps uploads instant and makes failures visible instead of silent.
Runtime-swappable model provider
Compiling the provider choice in
Recruitment data is personal data. Being able to move from a hosted API to a self-hosted model without a redeploy is what makes an on-premise conversation possible at all.
The hard part
Making the score defensible. An opaque number is unusable in recruitment — a recruiter has to justify a rejection, and "the model said 43" is not a justification. The scoring prompt was constrained to a fixed rubric (70–100 strong fit, 40–69 partial, 0–39 weak) and required to return its reasoning as structured fields rather than prose: matching skills, gaps, strengths, and specific recommendations. Those gap fields then feed the TF-IDF training lookup, so the same evidence that produced the score produces the remedy. The output stopped being a verdict and became an argument the recruiter can read, check and overrule.
Results
Stack
Related field notes

Cutting Multi-Agent Latency from 14s to 4s
Stop using an LLM for tasks that are pattern matching. Measure every step before optimizing any of them.

From $52K to $4.8K/Month on a Customer Service AI
High API cost and data privacy look like two problems. They are usually the same problem with the same fix.

MAJID
“Innovation distinguishes between a leader and a follower.”
Need an AI system that holds up in production? Tell me what breaks today and I'll tell you what I'd build.