All posts

How our match scoring actually works

A look under the hood at CareerAI's pipeline: embedding similarity, Claude re-ranking, and why your top match isn't always the one with the highest score.

CareerAI7 min read

Most job platforms are black boxes. You upload a resume, you get a list of “recommended” roles, and the only signal about whyis the ordering itself. If the top match is wrong, you have no idea whether the system is broken, whether your resume is wrong, or whether you’re being recommended filler.

This is the piece where we walk through what actually happens when CareerAI ranks your matches. It’s a five-step pipeline. None of the steps are secret.

Step 1: Parse your resume into structured data

The file you upload — PDF or DOCX — goes to Claude Sonnet 4.6, which pulls out a typed JSON shape: full name, headline, contact, experience entries, education, skills, projects, certifications. The system prompt is unusually strict about never inventing information. If your resume doesn’t say something, the parser omits it rather than guessing.

This structured shape is what powers everything downstream. Your cover letters reference real bullets from your experience. The coaching chat knows your actual employers. The match ranker reads concrete skills, not vibes.

Step 2: Embed the resume as a vector

We flatten your parsed resume into a condensed plaintext and send it to Cohere’s Embed Multilingual v3, which returns a 1024-dimensional float vector. Think of this as a coordinate in a high-dimensional “meaning space.” Resumes describing similar careers land near each other; resumes from completely different fields land far apart.

The input type matters. We use input_type=search_documentfor the resume, because Cohere optimizes each vector differently depending on whether it’ll be used as the thing being searched vs. the query doing the searching.

Step 3: Vector similarity against the job corpus

Every job we’ve cached in our database also has an embedding, computed the same way from the job title, company, and description. When we run your match recompute, we embed your resume as a search_query vector and ask Postgres (via pgvector with an HNSW index) for the top-20 job vectors by cosine similarity.

This is the cheap, fast, semantic-first pass. It catches that your “charge nurse, med-surg” resume is close to roles titled “clinical operations lead” and “unit manager” even though none of those exact words appear on your resume. It catches that your “senior brand manager, DTC” resume is close to “growth marketing lead” at a consumer startup. That’s what embeddings do well.

It’s also what embeddings do badly. Embedding similarity is directionally right but lossy: a senior role and an entry-level role in the same field can look similar to the vectors. A hybrid-in-office requirement looks similar to a fully-remote one. A role that requires five years of experience looks similar to a role that requires three months.

That’s why there’s step 4.

Step 4: Claude re-ranks the top 20

We take those 20 vector-similar candidates and hand them, along with your full structured resume, to Claude Sonnet 4.6 in a single structured-output call. Claude returns an integer 0–100 fit score for each one, plus a two-sentence rationale, up to three concrete strengths, and up to two concerns.

The system prompt for this call is deliberately calibrated. It assigns scores in specific bands:

  • 90–100— strong match on core responsibilities, seniority, and stack. Rare.
  • 75–89— solid match on most responsibilities, stretch on one axis.
  • 60–74— good thematic match with noticeable gaps.
  • 40–59— partial match with real transferable skills.
  • 0–39— not a reasonable fit.

The rationale has to quote specific evidence from your resume. “Matches the 22-bed med-surg experience on your 2023 line” passes review. “Relevant healthcare background” would not.

Step 5: Persist, re-sort, display

We sort by Claude’s score (tie-breaking on the original vector rank), persist the top 20 into the matchestable with your resume version stamped in, and the UI reads them from there. Any old match that fell out of the top 20 in this recompute gets deleted — we don’t keep stale rankings around.

Why the top match isn’t always obvious

One thing that trips users up: the highest vector-similarity job isn’t always the one Claude scores highest. That’s by design.

Suppose your resume is a marketing manager with five years running email and lifecycle at a DTC skincare brand. Vector similarity might put a “Senior Lifecycle Marketing Manager” at a big-box retailer at the top because the phrasing is nearly identical. But Claude reads both postings and notices the retailer role requires four days in-office in Minneapolis while your resume says “Remote” in every entry. Claude drops the score to 72 and surfaces the commute concern. Another role — a fully-remote lifecycle lead at a smaller consumer brand — vectors slightly lower but scores 82 because it aligns on location, seniority, and the DTC playbook you actually ran.

The vector pass is a filter. The rerank is the judgment. Both matter, and they’re not the same thing.

What we don’t do

  • No training on user data. Neither Anthropic nor Cohere trains on inference inputs. Your resume is a prompt, not a training sample.
  • No black-box score aggregation. The fit score you see is the literal number Claude returned, not a weighted blend of internal signals.
  • No paid placement. Companies cannot pay to rank higher. The corpus is real employer listings via Adzuna; the ordering is only the pipeline above.

If you want to see it work

The fastest way is to try it. Upload your resume, run a search, hit Recompute matches. You’ll get a ranked list with the rationale right under each score — which is the part nobody else shows you.