Matching questions by meaning cuts cost sharply, and starts answering questions nobody asked.
In a high-volume question-answering product, a large share of traffic re-asks something already answered, worded differently. Every one of those is a paid, slow model call producing an answer that already exists.
The product question: how much of that is reclaimable, and what is the failure mode of reclaiming it? An exact cache is safe and weak. A semantic cache is powerful and introduces a risk the exact cache never had.
The same handful of meanings arrives under many spellings, punctuation, capitalisation, "hey", "please". An exact cache pays full price for each, and here it topped out at a 0.691 hit rate.
Many spellings, one cached answer, lifting the hit rate to 0.724 while cutting model calls by the same fraction.
Matching on similarity means sometimes matching something that is merely similar, the false hit the whole project exists to measure.
Every request is a threshold decision. The threshold is the entire design, at 0.80 it bought a 3.5× speedup for a 1.35% false-hit rate.
The dump is one XML document of a few hundred megabytes (293 MB of Posts.xml); parsing it whole exhausts a 16GB machine. iterparse with elem.clear() keeps memory flat regardless of file size. That one line is the difference between a script that runs and one that swaps.
Links are pairwise, but A↔B and B↔C means all three are the same question. The groups are the connected components of that graph, union-find, a few lines. 1,945 duplicate pairs resolved into the ground-truth meaning-groups everything else is scored against.
Vectors (BGE) are L2-normalised into an inner-product index, so the threshold and its sweep read directly off [-1, 1].
Every metric here, hit rate, latency, calls avoided, false-hit rate, depends on which question matched and never on the answer's text. Paying for real generations would change what the notebook costs to run and none of its results. This project needs no API key at all.
The threshold sweep, a representative slice of the full 10-point sweep:
| Threshold | Hit rate | False-hit rate |
|---|---|---|
| 0.70 | 0.792 | 0.154 |
| 0.75 | 0.752 | 0.037 |
| 0.80 | 0.724 | 0.0135 |
| 0.85 | 0.706 | 0.0068 |
| 0.90 | 0.698 | 0.0009 |
| 0.92 | 0.695 | 0.000 |
Hit rate and false-hit rate move together. The loosest threshold has the best hit rate and a 15% false-hit rate, choosing a threshold is choosing how much wrongness a saving is worth.
This is the project's sharpest result, and it is in the data above: the loosest threshold (0.70) produces the best hit rate (0.792) and the lowest latency, and a 15.4% false-hit rate. Judged on every metric a normal dashboard shows, the worst configuration looks like the best one. The failure is silent: a fluent, confident, cached answer, with nothing logged. This is why the false-hit rate had to be built before the threshold could be chosen, and why the chosen point is 0.80, not 0.70.
Pick the operating point against an explicit false-hit budget. Where a wrong answer is cheap, a game-help bot, move down the curve and take the savings. Where it is expensive, move up and pay for the calls. The measured curve makes that a decision with numbers on it, not a guess.