Skip to content
Back to Blog
algorithmic matchingdeterministic scoringrankingexplainabilitystreamingrecruitment platform

Algorithmic Matching Without an LLM

Daniel Anthony Romitelli Jr. · August 12, 2026

A recruiter opens a search result and sees a candidate at the top. The score says the match is strong. The card has to say why: the credential matched, the location fit, a production figure landed in a high band, availability read as urgent, and one mismatch got named instead of buried.

No language model wrote any of that. Nothing generative runs in the ranking path or the explanation path. The card text falls out of the same arithmetic that produced the order, computed in one pass, and it costs no extra call.

This describes work I did on a client system. The numbers and factor names here are generic. Take the design, not the numbers.

1. A score is not a reason

Two designs fail here, and they fail in opposite directions.

A bare ranked list makes the recruiter audit the system by hand. Candidate three sits above candidate seven and nothing on screen says whether the production number, the designation, or the commute drove it. The user either trusts the order without evidence or opens both profiles and reconstructs the comparison manually, which is the work the ranking was supposed to absorb.

The second design is the one most teams reach for now. Rank first, then hand the top results to a language model and ask it to write a sentence about each. The output reads well and carries no obligation to be true. Nothing binds the sentence to the arithmetic, so the model can credit a factor that contributed nothing while omitting the one that moved the candidate twenty places. The recruiter cannot tell the difference, because a fluent wrong answer and a fluent right answer look identical on a card.

That failure is worse than a missing explanation. A blank space invites a question. A confident sentence closes one.

There is a third option, and it is older than both. The scoring code already knows why. Make it say so.

2. Evidence as a return value

The scoring path holds every fact the card needs: parsed requirements, candidate fields, numeric attributes, credentials, availability text, and the mismatch checks. It knows them at the moment it assigns weight. Asking a later stage to recover that from a float is throwing the answer away and paying a model to guess it back.

So each feature returns two things. What it contributed, and what it saw.

FeatureResult {
  name
  weight
  score_delta
  evidence
  positive_reasons
  concerns
}

final_score = sum(r.weight * r.score_delta for r in feature_results)
card_payload = {
  highlights: collect(r.positive_reasons),
  concerns:   collect(r.concerns),
  evidence:   collect(r.evidence)
}

The number and the text come off one set of objects in a single pass. There is no second interpretation layer to drift, because there is no second layer.

The cost is writing freedom. Card copy now moves only when scoring moves, and a product request to phrase something differently becomes a change inside the feature that earned it. I take that trade, because the alternative buys flexible prose by giving up any guarantee the prose is about this candidate.

3. What each feature is allowed to claim

The design earns its value by being specific about what each component may assert. A vague contract produces vague cards.

featurecompareswhat it may put on the card
keyword overlaprequirement terms against location, role, employer, credentialsthe terms that actually matched
percentile rankingnumeric fields such as portfolio size and recent productionthat a value sits in a high band within the returned set
credential detectionrequired designations against held designationsexact hit, partial hit, or absent
availability parsingtiming language in candidate textan urgency level, not a date it cannot prove
concern flagsrequirements the candidate missesthe specific friction, next to the positives

Every row is set arithmetic, a comparison, or a rank. Nothing on that list needs a model, and nothing on it can hallucinate, because none of these components can produce a claim that was not computed.

Percentile framing carries most of the interpretive load, and it carries a caveat with it. A raw production figure means nothing to a reader without a reference set, and it means something different in every market. Reporting the band instead of the number keeps the claim inside what the data supports. But the band is computed against the returned set, not against the world, so it shifts as the query shifts. The card should say top of these results rather than top performer, and if the copy ever drops that qualifier the claim has quietly grown past its evidence.

Availability is the tightest constraint. A candidate writing that they are open to conversations supports an urgency level and nothing more. Turning that into a start date would invent a fact the platform never had, which is exactly the move a generative summarizer makes without noticing.

The weights behind the total are constants somebody chose, spread across a handful of factor families, and whatever they are they should be visible to the person using the tool. A recruiter who thinks credentials should not carry the share they carry for a given role needs something specific to argue with. Weights are a product decision wearing an engineering costume, and hiding them does not make them neutral.

4. Where a model runs, and where it does not

One learned component sits in this pipeline. Embeddings narrow the candidate set before any weight is applied, and the retrieval side is not naive about it. Records carry more than one vector, weighted by what the text is, so a query about experience searches the experience representation more heavily than a headline.

That placement is the whole argument. Retrieval produces a set. Scoring produces an order and the reasons for it. A retrieval model that returns a slightly different set is recoverable, because the next stage still evaluates every member on stated criteria and the recruiter still sees why each one placed where it did. A generative model writing the reason is not recoverable, because its output is the last word and nothing downstream checks it.

Retrieval, evaluation, and delivery stay separate stages. One service wraps the database search, semantic retrieval, duplicate suppression, and batching, and answers a single question: who is in the set. The matching engine weighs candidates across components and answers the second: how well does each one fit. A third assembles the typed payload the browser consumes and emits it with the candidate metadata.

That last split is deliberate. The engine can collapse component weights into a sentence for internal workflows where a sentence is enough, while the response path carries named fields for an interface that renders them individually and cannot parse a paragraph.

5. The payload exists before the card does

The client receives the candidate record, the score, location, designation, and the explanation fields in one typed event.

Emitting them together is the enforcement mechanism. The explanation cannot be assembled lazily on the client, cannot be filled in by a later request, and cannot quietly become optional, because the event schema is where the two halves meet. If a scoring change stops producing keyword hits or drops concern flags, the gap shows up at the boundary rather than three weeks later in a support thread.

6. Thresholds expire, and mine did

Scores feed tiers. Above one cutoff a match fires an immediate alert, below it the match waits for a daily roundup, below that a weekly one. Those cutoffs were not guesses. I sampled the real score distribution across the live candidate and job set, found where the population actually sat, and placed the boundaries where the data justified them.

Then I changed the scoring model. New factors went in, the distribution moved upward, and the old cutoffs stopped meaning what they had meant. I re-derived them by hand and moved on.

That worked. What it did not do is leave behind a mechanism.

The tell showed up in the source. The class docstring still described the original cutoffs long after the constants had changed, and nothing caught it, because a stale comment does not fail a test. Documentation and code disagreed about the most consequential numbers in the alerting path, and the only reason I know is that I went back and read both.

A hand-set threshold is a claim about a distribution. It expires the moment the distribution moves, and it expires silently, because a number that no longer means what it meant still returns a value.

The uncomfortable part is that the fix already existed in the same codebase. I had built a calibration service for a different problem: it computes a conformal threshold per selector from a rolling window of observed errors, records every threshold change with its delta and its provenance, and monitors drift. Cache reuse decisions got statistically grounded cutoffs. Alert tiers got constants in a constructor.

Difficulty was not the difference. The cache had an error signal. Reuse a stale entry and something downstream notices, so observations accumulate and calibration has something to calibrate against. Nobody was labeling whether a match just under the cutoff was a miss. Without that, there is no error distribution to fit, and conformal machinery has nothing to work with.

Calibration is usually blocked on ground truth, not on math. If you want calibrated thresholds in a ranking system, the work is not implementing the quantile logic. It is building the feedback path that tells you when a threshold was wrong.

7. What determinism costs

Adding a signal is no longer one commit.

A generated paragraph absorbs a new factor for free, since the prose just mentions it. A typed payload does not. A new feature touches the scorer that produces it, the schema that carries it, the renderer that displays it, and the fixtures that assert it. Four places, one idea, and they have to agree.

Schema changes split cleanly by direction. Adding a field is additive and safe. Renaming or removing one is not, and a client using optional chaining will render a card missing that row without raising anything.

The sharper cost is expressive. The card can only say what the vocabulary permits. When a recruiter looks at a result and knows the real reason is something the feature set never modeled, the interface has no slot for it, and there is no honest workaround. The fix is to model the signal, which is slower than writing a sentence about it. A language model would have covered that gap immediately, with prose about anything you asked it to discuss. Giving up that coverage is the actual price here, and it is not small.

The return is concrete. Explanation costs a few field lookups instead of a network round trip and a generation, which is the difference between explaining every result in a long list and explaining the top three. A given candidate against a given job produces an identical card every time, which means a recruiter disputing a result and an engineer reproducing it are looking at one artifact. Past rankings replay from stored evidence without re-running inference. There is no per-result token cost. And there is no surface on which a hallucinated reason can appear, because nothing in the path can produce a sentence that was not derived from a comparison.

8. An explanation you can actually falsify

Post-hoc explanation has a faithfulness problem that the field has not solved. Feature attribution methods build a second model that approximates the first, and the gap between the approximation and the real decision process is exactly what you cannot measure without already knowing the answer. Asking a language model for a rationale is the same problem with worse error bars. In both cases the explanation is a claim about the computation rather than a product of it, and no experiment cleanly separates a correct explanation from a plausible one.

Derive the explanation from the arithmetic and that stops being a philosophical problem. It becomes a test.

Take a scored candidate and change one input. Remove the required credential, hold everything else fixed, and rerun.

Three assertions follow. The credential row moves from exact to absent. The total drops by that factor's weighted contribution and by no more than that. Every other row of the card is byte identical to the previous run.

That is an ablation test for explanation fidelity, and it fails loudly in the cases that matter. A feature that moves the score without emitting evidence breaks assertion one. A feature that emits evidence without moving the score breaks assertion two. Coupling between features that should be independent breaks assertion three, and that third one catches real bugs, because a shared normalization step is the kind of thing that quietly makes one factor's change ripple into another factor's text.

Run the same experiment against a generated rationale and there is nothing to assert. The prose may change, may not, may change in an unrelated place. No outcome falsifies anything, which is another way of saying no outcome confirms anything either.

9. How it breaks, and how you find out

The dangerous failure is silent and partial.

Highlights arrive empty. The score is still there, the layout still renders, and the card degrades into the bare ranked list this design was built to replace. Optional chaining on the client turns a missing array into no row instead of an error, so nothing logs and nobody files a bug. Users see a slightly emptier card and assume that candidate simply had less to say.

That failure needs a metric, not a test. Count how many emitted events carry an empty highlights array. Zero is expected. A number that starts climbing after a deploy tells you which change severed the connection between scoring and evidence, and it tells you before a recruiter learns to stop reading the card.

This design does not make the ranking better. A candidate sitting at the top for a bad reason still sits at the top, and binding the copy to the arithmetic means that mistake gets displayed with the same confidence as a correct call.

What changes is how long that survives contact with a user. A recruiter who can see that a match ranked high on a credential the role does not require has been handed the bug. Wire the explanation to the score and the ranking stops being something you defend in a meeting. It becomes something the product argues about with you, one card at a time.