Resume Match
An algorithmic resume-to-job scoring engine, upload a resume PDF and a job description, get a fit score with a full breakdown of matched skills, missing skills, and extra value. No AI model, no API key, no database, pure tokenization, synonym canonicalization, and weighted scoring.
Resume Match answers a question every job applicant has: "how well does my resume actually fit this listing?", without sending a single byte to an AI API. Upload a resume PDF and a job description (pasted or PDF), and the engine returns an overall fit score plus a category breakdown.
The deliberate design choice was no AI, no LLM calls: every score comes from deterministic logic: tokenization, a hand-built synonym dictionary, section-aware weighting, and regex-based experience/education extraction. The same category of system real ATS tools used before deep learning got cheap.
Stateless by design: nothing gets persisted, no database, no accounts. Upload, analyze, get the breakdown, done.
The overall score is a weighted composite of four independently-calculated signals:
overallScore = requiredSkillMatch × 0.55
+ niceToHaveSkillMatch × 0.15
+ experienceMatch × 0.20
+ educationMatch × 0.10
Skill match percentages aren't a flat count, a skill mentioned three times in the listing pulls more weight toward the score than one mentioned once, since the algorithm treats repetition as a proxy for importance.
Each of the four signals in overallScore is itself a non-trivial computation. This section formalises the engine as it actually runs: weighted set similarity over a canonicalised term space, not a keyword count.
1. Weighted skill coverage. Let R be the set of required skills extracted from the job listing and M \subseteq R the subset matched in the resume. Each skill t carries a weight w_t equal to its mention frequency in the listing, repetition is treated as a proxy for importance. The required-skill match is the weighted coverage ratio:
This is why one skill mentioned three times moves the needle more than one mentioned once: the denominator and numerator are both mass over weights, not cardinalities of sets.
2. Why not plain Jaccard? A flat set-overlap score would treat every term as equally important. The engine instead uses a weighted generalization of Jaccard similarity, which collapses to the classic form only when all weights are equal:
Here P is the candidate (resume) term profile. The canonicalization step matters mathematically: by mapping react.js, Reactjs, and React to one term, it prevents the denominator from being inflated by synonym duplicates, without it, J_w systematically under-reports true overlap.
3. TF-IDF intuition for importance weighting. Mention-frequency weighting is a deliberate simplification of full TF-IDF. The general term weight that motivates the design is:
Because a single job listing is one document, IDF degenerates to a constant per run, so the engine keeps only the TF component, formally justifying the "count = weight" rule rather than treating it as an arbitrary heuristic.
4. Experience matching, interval union, not naive sum. Date ranges in a resume overlap (concurrent roles), so summing durations double-counts time. The engine merges intervals first, then measures total covered span. Given raw ranges \{[a_i, b_i]\}:
5. The composite as a convex combination. The four signals are blended with weights that form a partition of unity, which guarantees the output is always a valid percentage in [0, 1] regardless of the inputs:
It would have been faster to wrap an LLM call and prompt it to "compare this resume to this job." That's not what this project demonstrates. The goal was to build the kind of deterministic, rule-based system that shows actual algorithm design, the weights, the edge cases, the synonym dictionary, are things I designed and can defend, not a prompt I wrote.
The stopword and synonym dictionaries are structured so a new language needs no change to the matching logic — add a language key with its stopword list and localised variants. Tokenisation is language-agnostic; dictionary coverage is strongest for English, Indonesian, and Malay.