Skip to content
goutham.dechineni

Freelance project

Resume Screener

Problem
Reading a large stack of PDF resumes against one job description is slow, and keyword filters miss candidates who describe the same skills in other words.
Approach
Gemini parses each PDF into structured fields, and every resume gets a transparent score: 0.4 skills match + 0.3 experience + 0.3 semantic similarity from MiniLM embeddings. Batches run asynchronously with live progress over a WebSocket.
Result
A FastAPI service that screens a whole batch in one go, shows progress as it works, and paces its Gemini calls with a rate limiter.
My role
Freelance AI engineer (2024–2025). Built the system end to end for a client: backend, scoring, LLM integration and UI.
Stack
  • Python
  • FastAPI
  • PyMuPDF
  • Gemini
  • Pydantic
  • sentence-transformers (MiniLM)
  • asyncio
  • WebSockets
Links

Code private (client work)

01

Architecture

  1. Web UI: the recruiter enters a job description and uploads a set of resume PDFs.
  2. FastAPI: the backend is organised as layered services, with parsing, scoring and matching kept apart so each can be changed on its own.
  3. Batch matcher: resumes are processed asynchronously, 10 at a time, so a large batch neither runs one by one nor floods the LLM API.
  4. PyMuPDF extracts the text from each PDF.
  5. Gemini parser: Gemini turns the raw text into structured fields such as skills and experience.
  6. Rate limiter: every Gemini call passes through a limiter, so a big batch stays inside the API quota instead of failing halfway.
  7. Scorer: each resume gets 0.4 × skills match + 0.3 × experience match + 0.3 × embedding similarity. The parts are kept, so a recruiter can see why a candidate ranks where they do.
  8. MiniLM: the resume and the job description are split into chunks and embedded with a small MiniLM sentence-transformer model; their similarity is the semantic part of the score.
  9. WebSocket: progress for each resume is pushed to the browser as it finishes, so the recruiter sees results arrive instead of waiting on one long request.

02

Key decisions

  • A fixed, readable score instead of asking the LLM to rank

    The LLM only extracts fields; the ranking comes from a formula with fixed weights (0.4 / 0.3 / 0.3). The same resume always gets the same score, and the parts explain the result. The cost is that the weights are a judgement call, not learned from hiring outcomes.

  • Bounded concurrency with a rate limiter

    Running 10 resumes at a time keeps the batch moving without waiting on each LLM call in turn, and the limiter keeps Gemini calls under quota so a batch does not fail with rate-limit errors. The cost is that throughput has a hard ceiling set by the API quota, however many resumes are queued.

  • Chunked embeddings, not one vector per resume

    Embedding chunks lets a strong match in one section count, instead of being averaged away by the rest of the resume. The cost is more vectors to compute per resume, and a choice of how chunk similarities add up to one number.

03

Results

  • Screens a whole batch of PDF resumes against a job description in one run, with progress pushed live to the browser.
  • Every score breaks down into skills, experience and semantic parts that a recruiter can check.
  • Gemini calls are paced by a rate limiter and bounded concurrency, so large batches do not hit the API quota all at once.

04

What I'd do next

  • Collect recruiter decisions on screened batches and use them to check, then tune, the 0.4 / 0.3 / 0.3 weights.
  • Validate the Gemini output against a schema and retry or flag resumes whose parse is incomplete.
  • Cache parses by file hash, so a resume uploaded for a second job is not parsed again.