Back to All Projects
Algorithmic Scoring  ·  No AI  ·  Full Stack  ·  2026

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.

React + Vite Node.js + Express pdfjs-dist Tailwind Zero AI / Zero Cost
Status
Completed · Live
Year
2026
Type
Algorithm Engine · Full Stack
Cost
$0, No API Calls
01 Project Overview

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.

Every number is explainable. Because nothing routes through a black-box model, every point of the score traces back to a specific rule you can read in the source, which makes the project a genuine talking point for technical interviews, not just an API wrapper.

Stateless by design: nothing gets persisted, no database, no accounts. Upload, analyze, get the breakdown, done.

02 How the Algorithm Works
1. Tokenize
Text is lowercased, multi-word phrases like "machine learning" are protected so they survive splitting, then broken into words/phrases with stopwords removed.
2. Canonicalize
Each token is matched against a hand-built synonym dictionary, "Reactjs", "react.js", and "React" all collapse to one canonical term, so phrasing differences don't break the match.
3. Split by Importance
The job description is split at the first "nice to have"-style header. Everything before is a hard requirement; everything after is weighted lower in the final score.
4. Weighted Scoring
Required skill match, nice-to-have match, experience match, and education match are combined into one overall percentage, each skill weighted by how often it's mentioned in the listing.
5. Experience & Education Extraction
Regex detects phrases like "3+ years" or "minimum 2 tahun" in the job text, and date ranges like "2022 – Present" in the resume, merging overlapping ranges so concurrent roles don't inflate the total.
PDF Text Extraction
Both resume and job description PDFs are parsed server-side with pdfjs-dist, no manual copy-pasting required for either input.
03 Scoring Formula

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.

3b The Math Behind the Match

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:

Frequency-weighted coverage
\text{requiredSkillMatch} = \frac{\displaystyle\sum_{t \in M} w_t}{\displaystyle\sum_{t \in R} w_t}, \qquad w_t = \text{count}(t \mid \text{listing})

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:

Weighted Jaccard (term importance preserved)
J_w(R, P) = \frac{\displaystyle\sum_{t} \min(w_t^{R},\, w_t^{P})}{\displaystyle\sum_{t} \max(w_t^{R},\, w_t^{P})} \;\xrightarrow[w \equiv 1]{}\; \frac{|R \cap P|}{|R \cup P|}

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:

TF-IDF term weight
\text{tfidf}(t, d) = \underbrace{\frac{f_{t,d}}{\sum_{t'} f_{t',d}}}_{\text{term frequency}} \times \underbrace{\log\!\frac{N}{1 + |\{d : t \in d\}|}}_{\text{inverse document frequency}}

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]\}:

Sort by start, then merge any pair that overlaps or touches into a single interval:
[a_i, b_i] \cup [a_j, b_j] = [\min(a_i,a_j),\, \max(b_i,b_j)] \quad \text{if } a_j \le b_i
Total experience is the measure of the disjoint union of merged intervals \mathcal{U}:
Y_{\text{total}} = \sum_{[a,b]\in\mathcal{U}} (b - a)
The experience signal saturates at the requirement Y_{req}, exceeding it can't push the score past 1:
\text{experienceMatch} = \min\!\left(1,\ \frac{Y_{\text{total}}}{Y_{\text{req}}}\right)

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:

Convex blend of normalized signals
\text{score} = \sum_{k} \alpha_k\, s_k, \qquad \sum_k \alpha_k = 1,\quad \alpha_k \ge 0,\quad s_k \in [0,1]
Instantiated weights
\boldsymbol{\alpha} = (0.55,\ 0.15,\ 0.20,\ 0.10) \;\Rightarrow\; \textstyle\sum \alpha_k = 1.00
Why convexity is the safety net. Because each s_k \in [0,1] and the weights sum to one, the result is a weighted average, it can never exceed any individual signal's ceiling, so a perfect skills match can't paper over zero experience beyond the share its weight allows.
04 Why "No AI" Was the Point

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.

Zero ongoing cost, by construction. No API keys, no per-request billing, no rate limits to worry about. The entire engine runs on plain JavaScript logic that's auditable line by line.
05 Multi-Language Support

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.

App Modules
Tokenizer / Keyword Extractor100%
Synonym Canonicalization100%
Matching Engine / Scoring100%
PDF Extraction (pdfjs-dist)100%
React Frontend / Score Ring100%
Tech Stack
React Vite Tailwind Node.js Express Multer pdfjs-dist
No AI. Just algorithms that explain themselves.
Upload a resume and a job description, get a fit score you can actually trace back to a rule, not a black box.
Open Resume Match