Sometimes the model is not wrong. The pipeline around it is, and the model gets the blame.
This chapter is one worked example of that, because the lesson generalises further than the example does: when a step in your pipeline is making a judgement, write the deterministic version and measure it, rather than reaching for a smarter prompt.
The Failure: Shipping Position Zero
Generated sites need photographs. The content step produces a search phrase per slot — "janitorial services celebration", "mopping office floor" — and a stock provider returns candidates in its own relevance order.
We shipped whatever sat at position zero.
That was the single largest source of off-topic imagery on provisioned sites, and the reason is in the queries. They are compound: a topic term plus a slot-specific term. A provider that cannot satisfy the whole phrase happily returns something matching only the weakest part of it. Ask for "janitorial services celebration" and you get balloons and confetti — an excellent match for one word and nothing to do with the business.
Notice that nothing here is the model's fault. The query was reasonable. The provider did what it does. The defect was that we accepted the first row without ever asking whether it matched.
Coverage, Not Cleverness
The fix is about as simple as an algorithm gets: count how many distinct query terms each candidate's description covers, and sort by that.
foreach ($candidates as $index => $candidate) {
$scored[] = [
'index' => $index,
'coverage' => self::coverage($queryTokens, $candidate['alt']),
'candidate' => $candidate,
];
}
usort($scored, function (array $a, array $b): int {
return $b['coverage'] <=> $a['coverage']
?: $a['index'] <=> $b['index'];
});
Tokenise, drop stopwords and very short tokens, match loosely so simple inflections still count — "clean" against "cleaning", "limpieza" against "limpiezas" — without pulling in a stemmer.
No model call. No embeddings. No vector store. A scoring function you can read in a minute, run a million times for nothing, and explain to somebody who is annoyed about a photograph.
The Experiment That Got Deleted
The interesting part is the version that did not survive, because it was the cleverer one and it was worse.
The theory was sound: in a compound query the tail is the slot-specific part, so weight terms by position and let the specific term win. It sounds right. It reads well in a commit message.
Measured against live results it lost. For "mopping office floor" it preferred a home-office photograph whose tags happened to include "office" and "floor" over one actually tagged "mopping, floor" — because a single higher-weighted term beat covering more of the query. On every other query measured, the two approaches agreed.
So the weighting was deleted. It added a concept, an explanation and a failure mode, and bought nothing.
A cleverer algorithm that measures the same is a worse algorithm. It has all the same behaviour and more surface to be wrong on.
This is Chapter 1's boring scales applied to the one part of the system where cleverness is most tempting, because it is the part that looks like machine learning.