← ALL POSTS · /BLOG

BENCHMARKS · DECISIONS · SMS SPAM + BANKING77 · RUNS 2026-09-21

A BM25 VOTE TIES JEV ON SPAM
AND BEATS IT ON INTENT

We gave TypeSafe's Jev five hundred support messages to classify, then answered the same set from a XERJ index with no model at all. On spam the two tied. On fine-grained intent, looking the answer up in our own labelled history won by nine points.

01 · THE SETUP

What we compared

Jev is a decision model. You send it a question and a payload, and it returns a typed answer instead of prose, which is a genuinely useful shape for anything that has to branch on the result.

It also raises an uncomfortable question about our own product. If you already hold a pile of labelled history, do you need a model to classify the next item, or can you simply look it up?

So we built the dumbest competitor we could think of: retrieve the ten nearest labelled examples from an index, let them vote, and return the winning label. No training, no fine-tuning, no inference. We then ran it against Jev on two public datasets with five hundred held-out items each, scoring every arm with exact McNemar tests and bootstrap confidence intervals so that a difference of a point or two could not be mistaken for a result.

DatasetQuestion typeClassesIndexedTested
SMS Spam Collection (UCI)binary noul24,000500
Banking77 (PolyAI)many-way choice7710,003500

One thing to be precise about, because our own rules require it: rows labelled semantic or hybrid ran --embed-mode neural with all-MiniLM-L6-v2 in-process on CPU. XERJ's default embedder is lexical feature hashing, and the BM25 rows used no model whatsoever. Everything local still costs zero API tokens either way.

02 · THE TIE

On spam, nobody wins

Both systems get essentially every message right, and the difference between them is noise.

SMS Spam, n=500Accuracy95% CIp50$/1k
majority floor0.8440.812–.8760
BM25 vote, no model0.9800.968–.9927.8 ms0
hybrid vote0.9840.972–.99426.5 ms0
Jev, rich criteria0.9880.978–.996922 ms$0.0145

Paired on the same items, hybrid and Jev disagree on ten of them and split those six to four, which an exact McNemar test scores at p = 0.75. There is no detectable difference in accuracy between a hosted frontier decision model and simply counting which labels turn up near your query. The gap that does exist sits in the other two columns, where the local arm is roughly thirty-five times faster and the bill is zero.

03 · THE WIN

On 77 classes, retrieval wins outright

We expected the fine-grained task to be where a hosted model pulled ahead. It went the other way, and not by a little.

Jev, hosted XERJ, local, no tokens
0.8240
0.8300
0.9140
0.9180
Jevchoice, 77 options BM25 voteas shipped hybridMiniLM, local semantic + RRbest arm
Banking77 accuracy, 500 held-out utterances — run 2026-09-21

Paired, the two disagree on eighty-five items and Jev loses sixty-five of them, which is decisive at p < 0.0001. The best local arm also calibrates better by a factor of three on expected calibration error, so unlike the hosted arm its confidence is worth thresholding on rather than merely sorting by.

Why would a vote beat a model here? Because seventy-seven intents in a banking app are not a general reasoning problem, they are a vocabulary problem, and that vocabulary is already sitting in the index. A model that has never seen this bank's phrasing has to infer the whole taxonomy from a list of option strings, while the index holds ten thousand examples of how real customers actually word each one.

04 · THE CROSSOVER

How much history you need before this works

This is the number we would most want if we were reading someone else's post, so here it is.

Banking77 history indexedPer classAccuracyvs Jev 0.8240
38550.5740far behind
770100.7420behind
1,925250.8360passes Jev
3,850500.9120clear
10,0031300.9140flat after here

Twenty-five labelled examples per class. Below that line it is worth paying for the model, and above it you are paying a vendor to do worse than your own archive. Most support teams crossed that threshold years ago without noticing, which is the part of this result that seems genuinely interesting.

05 · THE LOSS

Where Jev beat us, and why

On DBpedia, Jev wins 0.9800 to our 0.9125. Fourteen ontology classes, encyclopaedic text, and a taxonomy whose boundaries are semantic rather than lexical. Our embedder puts Artist and Athlete near each other because the sentences look alike. Jev knows they are different kinds of thing.

We nearly published a much better-looking number here. Our first DBpedia run dumped the class labels as bare integers, so Jev was scoring at chance and the result looked like a rout — until we read our own harness and found the bug. With real class names the comparison flipped completely, and the honest version is the one above.

There is a second loss worth naming, and it is structural rather than incidental. Ask a question whose answer is not in the label vocabulary at all and retrieval scores zero, because a vote can only ever return a label it has already seen; Jev answers the same question zero-shot with the best calibration we measured anywhere. A vote is not a reasoner, and we should stop pretending that the two substitute cleanly for one another.

06 · THE BUGS

The comparison found two defects in our own code

Running a competitor properly is the cheapest code review available, and two of ours failed under it. The first is an instruction bug, which showed up when we tested a four-step ladder running from "Is this spam?" up to a multi-clause policy and the two systems moved in opposite directions.

InstructionJevOurs
L1 · "Is this spam?"0.96330.9900
L2 · one clarifying clause0.98330.9667
L3 · explicit criteria0.98670.8100
L4 · multi-clause policy0.99000.6400

The cause is that our implementation concatenates the instruction into the retrieval query. A longer instruction is better guidance for a model and steadily worse query text for an index, because the instruction is a question rather than something to search for, which is filed as #1000.

The second defect is quieter and worse: a structured state payload was being silently discarded, so the endpoint returned confident answers computed from an empty context and nothing ever errored. That one is #1001.

07 · LIMITS

What we are not claiming

Both datasets are public and may sit in Jev's training data. That biases the comparison in Jev's favour, which makes the Banking77 result conservative rather than inflated, but it is still a real limit on what two datasets can tell you.

The runs used one vendor model version over a single week, sampled once per item, on a shared machine whose load average wandered between eight and twenty-two. Local latencies are therefore lower bounds, and Jev's include a network path nobody outside TypeSafe can see.

Every item was a short English text with a single label, in one language. Nothing here shows that the result transfers to long documents or multilingual corpora, and that claim stays unmade until somebody measures it.

The full write-up, every arm and the per-item predictions live in the study. The endpoints are /v1/systemone and /_decide, both documented in the docs, and the retrieval side of the story continues in our earlier post on reranking and on the benchmarks page. If you would rather check the claim than read about it, the shortest path is the quickstart, an index recipe and your own labelled archive.