Most “resume vs job” tools give you a number like 73% and nothing else. You cannot tell why it is 73, what to fix, or whether the tool simply counted keywords. I built ResumeAI to do the opposite: an AI resume matcher where every requirement of the job gets a status, the evidence behind it and a concrete next step, and the overall score can be traced back to those parts.
It is also a complete SaaS: profiles, resume builder with templates and exports, job tracking, Free and Pro plans, Stripe billing and an admin dashboard. This article focuses on the parts that are hard to get right with AI: trustworthy scoring, privacy, and stopping the model from inventing things.
The project at a glance
| What it does | One master profile → tailored resumes → explainable match against a job → improvement, cover letters, interview prep, application tracking |
| Backend | Java 21, Spring Boot 4, Spring Security (JWT access and refresh tokens), JPA/Hibernate, Flyway, Apache Tika for CV import |
| Front end | Angular 17 with standalone components and signals; a Chrome extension that captures job postings |
| AI | Ollama running locally: a chat model for extraction and an embedding model (nomic-embed-text) for semantic matching |
| Data | PostgreSQL 16 with the pgvector extension as an embedding cache |
| Operations | Docker Compose, Nginx with a strict Content-Security-Policy, health checks with an AI “degraded” state, structured JSON logs, CI with unit and integration tests |

The problem with keyword scores and with pure AI scores
There are two common ways to match a resume to a job, and both fail users:
- Keyword counting says “Java” appears, so the Java requirement is met, even if Java only appears in a skills list and the job asks for five years of it.
- Asking a language model for a score gives a number that changes between runs and cannot be explained. Worse, models are confident about things that are not in the resume.
Recruiters reason differently: they look for evidence. A skill used in a dated role counts. A skill only listed under “Skills” is a claim. A missing must-have is a deal-breaker no matter how good the rest is. I designed the engine around that reasoning.
Architecture of the AI resume matcher
browser ──► Nginx (Angular app, /api proxy, CSP) ──► Spring Boot API ──► PostgreSQL 16 + pgvector
└──────────► Ollama (chat + embeddings)
Running the models through Ollama on the same infrastructure was a deliberate choice: a CV contains a person’s name, contact details and work history. With local models, that text never goes to a third-party AI provider. The trade-off is hardware cost and smaller models, which is why the engine does as much as possible without AI.
Step 1: Turn the job description into structured requirements
When a user pastes a job description, the chat model extracts a list of requirements, each with a type (skill, tool, experience, education, certification, language), a priority (must-have or nice-to-have) and details such as minimum years. The user can edit the list or add their own, and a re-analysis keeps the ones they added. From this point on, matching is done against structured data, not against free text.
Step 2: Exact matching first, with no AI at all
Layer 1 is deterministic. It is fast, free and gives the same answer every time, so every requirement it can resolve never reaches a model:
- Normalisation. Case, accents, punctuation and versions are ignored (“Java 17” → java), and an alias dictionary unifies names:
springbootandspring-boot→ spring boot,k8s→ kubernetes,postgres→ postgresql. - Token boundaries. “java” never matches “javascript”. Very short terms such as “go” or “r” are only trusted inside technology lists.
- Years from dates, not from claims. Years of experience with a skill come from the dates of the roles that mention it. Overlapping jobs are merged so they are not counted twice, and a finished role with no end date counts as zero rather than a guess.
- Education and languages are understood, not string-matched. Degree levels are ordered (including French terms such as licence, ingénieur, bac+5), related fields are grouped, and language levels follow the CEFR scale (“fluent” = C1, “business” = B2).
Step 3: Semantic matching only where it helps, with guardrails
Requirements that exact matching cannot resolve go to Layer 2. The requirement and the resume’s evidence sentences are embedded in one batch and compared by cosine similarity. Embeddings are good at “built REST APIs for a payments platform” vs “experience designing backend services”, and dangerous elsewhere, so the layer has hard rules:
- A named skill or tool is only compared with experience and project sentences, never with another listed skill. To an embedding model, Kubernetes and Docker are “similar”; to a recruiter, one is not proof of the other. Even then, the best result is
PARTIAL. - Education and certifications can only match education and certifications, and languages are never matched semantically.
- Thresholds are configuration, not constants (0.60 minimum and 0.80 strong by default, calibrated for
nomic-embed-text), because they depend on the model. - If the embedding model is down, matching continues with Layer 1 and the result says so.
Embeddings are cached in PostgreSQL with pgvector, so the same sentence is not embedded twice.
Step 4: Decide what counts as evidence
Layer 3 is the heart of the “explainable” part. Each requirement ends up with one of four statuses:
| Status | Meaning |
|---|---|
MATCHED |
Demonstrated in an experience, project, degree, certificate or language, and the minimum years or level is met |
PARTIAL |
Demonstrated but not fully: fewer years, lower level, related field, expired certificate, or related wording |
UNVERIFIED |
Claimed in the skills list or summary, but nothing on the resume shows it being used |
MISSING |
No evidence at all |

“Java” in the skills list does not mean “five years of Java”. That single rule is what makes the score feel fair to recruiters and honest to candidates.
Step 5: A score you can trace
Layer 4 turns statuses into numbers in a way a user can follow:
- Requirement score: MATCHED 100, PARTIAL 40 to 90 depending on coverage (for example, years shown ÷ years required), UNVERIFIED 50, MISSING 0.
- Category score: a weighted average where must-haves count three times as much as nice-to-haves, because job ads list many nice-to-haves that should not dominate.
- Overall score: categories weighted (skills 35, experience 30, education 15, certifications 10, languages 10), renormalised to the categories the job actually has, so a job that lists only skills is scored on skills.
- Must-have gate: if any must-have is missing, the score is capped at 69. A resume can never read “Strong match” while a hard requirement is absent.
Every stored result records a scoringVersion, so results calculated under different rules are never compared as if they were the same. The engine runs on an immutable snapshot of the resume, which makes it pure and easy to unit-test with “golden” scenarios, and no database transaction is held open while a model is thinking.
Step 6: Guard against invented claims
The app also writes: tailored resume sections, translations between French and English, cover letters and interview answers. Language models are tempted to “improve” a resume by adding a technology or a number that is not true. ResumeAI checks generated text against the user’s own resume:
- A rewrite or translation that adds a technology or a number that is not in the source is rejected.
- Cover letters and interview answers that do so are flagged sentence by sentence for the user to review.
- Advice is generated from the requirement statuses, not by the model, and never suggests claiming something the candidate has not done (“If you have real experience with Kubernetes, add it where you used it; if not, treat it as a gap”).
Step 7: Make it a product, not a demo
- CV import from PDF, DOCX, DOC or TXT with Apache Tika. The import only fills empty fields and never overwrites what the user typed.
- Six resume templates with live preview, PDF and Word export, version history and public share links with view counts.
- A Chrome extension sends the job posting you are reading straight into the app.
- Plans and billing. Free and Pro plans with monthly AI quotas (HTTP 402 when used up). Stripe subscriptions switch on only when the keys and webhook secret are configured.
- Onboarding and analytics. A guided checklist for new users and an admin dashboard for product metrics.
Step 8: Production hardening
- Short-lived tokens. Access tokens last 15 minutes; logout, password change or reset and account deletion revoke all tokens at once. Accounts lock after five failed logins.
- Ownership checks everywhere. User data lives under
/api/v1/users/{userId}/…and the security layer rejects any ID other than the caller’s. An integration test runs a full cross-user (IDOR) matrix. - Rate limits per IP on authentication and per user on AI endpoints.
- Graceful AI failure. When Ollama is unreachable, the health check reports the AI component as
DEGRADEDbut the app stays up: profiles, resumes and exact matching keep working. - Observability. Structured JSON logs, a request ID on every request and line, and a separate security audit log for logins, lockouts and resets.
- Tests in CI. Unit tests for the matching engine and parsers, integration tests on a real PostgreSQL with pgvector through Testcontainers, front-end tests in headless Chrome, then Docker image builds.
Lessons for anyone adding AI to a product
- Use AI for what rules cannot do, and rules for everything else. Deterministic layers are cheaper, faster and explainable; the model fills the gaps.
- Put guardrails in code, not in the prompt. “Do not invent facts” in a prompt is a wish. Comparing output with the source is a check.
- Design for the model being down. An AI feature that takes the whole app with it is a liability.
- Keep sensitive data on your infrastructure when the domain demands it. Local models are often good enough when the rest of the system does the heavy lifting.
Want AI in your product, done this way?
The same approach works for any “match, score or extract” problem: candidates and jobs, leads and offers, documents and checklists, support tickets and answers. I can design the scoring so your users trust it, run the models on your own infrastructure when privacy matters, and build the product around it.
Official resources for AI resume matcher
Documentation and references for AI resume matcher: