← All work

AI & Backend · Production

rhconsole

AI CV/job matching console

A recruiting console that scores how well a CV fits a job offer, in real time and with an explanation: extraction, profiling, embeddings and a hybrid score behind a REST API, with no LLM in the loop.

Role
Solo end-to-end developer
Period
Sep 2025 – Jul 2026
Client
Bénin Digital
Status
In production
  • 700+Git commits
  • 40+REST endpoints
  • 13k+real CVs used to audit extraction
PythonFastAPIPostgreSQLpgvectorRedisSQLAlchemyAlembicspaCysentence-transformersTesseractDocker

Context

Recruiting means reading many CVs against many job offers. rhconsole automates the first pass: CVs and offers are dropped in watched folders or uploaded in the dashboard, and each pair gets a 0–100 score with the reasons behind it, so a recruiter knows what to check before calling a candidate.

What I built

  • A FastAPI REST API with 40+ endpoints, JWT authentication with three roles, rate limiting and Prometheus metrics, on PostgreSQL 16 with 25 Alembic migrations.
  • An event pipeline: a folder watcher and a standalone sync agent feed a Redis Streams queue, and a worker retries with exponential backoff before extraction, profiling, embeddings and scoring.
  • Text extraction that reads two-column CV layouts column by column, with a per-page Tesseract OCR fallback for scanned PDFs, tuned through audits on 13,000+ real CVs.
  • A four-layer skills taxonomy (2,162 hand-reviewed aliases, ROME 4.0, ESCO, O*NET and e-CF) with n-gram matching and a typo-tolerant fallback.
  • A hybrid 0–100 score: skills, experience, education, languages and contract, plus a multilingual e5 embedding signal and a French cross-encoder. Coverage caps and a penalty for missing key tools stop a generic semantic match from hiding a skills gap.
  • Every match comes with an explanation (why it matches, points of vigilance, evidence from the CV), three score levels and recruiter feedback stats, in a vanilla JavaScript dashboard.
  • Docker Compose for local and cloud, a hardened API container (read-only filesystem, no Linux capabilities), and GitHub Actions CI that blocks a merge when a model changes without its migration.

How it fits together

CV / job offerExtractionPyMuPDF + OCRProfilingspaCy + taxonomyEmbeddingse5 + pgvectorHybrid score+ cross-encoderWeb console(vanilla JS)REST APIFastAPI + JWTRedis Streamsqueue + cache

Simplified view of the matching pipeline.

Security

I audited the production app against OWASP practices and fixed real issues myself: a JWT secret left as an example value, a default administrator account exposed, and credentials present in the Git history.

Decisions & trade-offs

One weight set, not one per domain

The score used to switch its weights by detected domain. One ambiguous word, such as "patient" in a tech CV, could flip the whole profile with no visible sign. I removed it: the domain is still shown as a label, and recruiters can pick an explicit weighting profile per offer.

Delete dead code that looks alive

A learned-weights feature had been built but never connected to the real scoring. I dropped its table in a migration instead of leaving settings that suggested it was active.

A missing signal is left out, not guessed

When a component has nothing to compare (an offer with no years of experience, for example), it is left out of the average and its weight goes to the others, instead of defaulting to a neutral value that would drag the score.