Search
Thresholds and Confidence
How confidence is scored, clamped and checked against a threshold, and how to override an algorithm's default.
Every algorithm’s score method returns a confidence between 0 and 1, or
null for “not a match.” BaseAlgorithm.run clamps whatever score returns
into that range and checks it against the resolved threshold before counting
it as a match:
const confidence = clampConfidence(score.confidence);
if (confidence <= 0 || confidence < context.threshold) continue;
clampConfidence maps anything non-finite to 0, anything below 0 to 0,
and anything above 1 to 1. A confidence of exactly 0 never counts as a
match, even against a threshold of 0: score should return null instead
of 0 for “not a match,” and the base class enforces that regardless.
Where a threshold comes from
Each algorithm ships a default threshold, set in its constructor:
Levenshtein() // threshold 0.7
JaroWinkler() // threshold 0.8
NgramSearch() // threshold 0.4
FuzzySubsequence() // threshold 0.2
NaiveSearch() // threshold 0 — an exact match or nothing
Two things can override it, and the registration option wins:
Levenshtein({ threshold: 0.6 }); // the algorithm's own default
search.register(Levenshtein(), { threshold: 0.9 }); // wins over the factory's
.register(algorithm, { threshold }) is checked against algorithm.threshold
only as a fallback — registration.options.threshold ?? registration.algorithm.threshold — so the same Levenshtein instance could
in principle be registered into two different pipelines with two different
effective thresholds.
Reading confidence per algorithm
Confidence means something different for each algorithm, because each one measures a different kind of closeness:
naive/normalized-naive: binary in practice.1for an exact match, and fornormalized-naive,0.9when the match only appeared after folding ambiguous characters, so a true exact hit still outranks a folded one.substring/prefix/kmp/boyer-moore: how much of the candidate the query covers (needle.length / haystack.length), so a short query matched inside a long candidate scores low even though it did match.levenshtein:1 - distance / longestLength; close strings with a few edits score near1.jaro-winkler: Jaro similarity plus a bonus for a shared prefix.ngram: the Sørensen–Dice coefficient over two bags of n-grams.soundex:1for a whole-string phonetic match,0.75for a single matching word inside a longer candidate.fuzzy-subsequence: a blend of how tightly the matched characters cluster and how much of the candidate they cover.
See Bundled Algorithms for the full breakdown and
each algorithm’s tuning options: maxDistance on Levenshtein,
scalingFactor on JaroWinkler, size on NgramSearch, and so on.
caseSensitive
Every algorithm accepts { caseSensitive: boolean }, false by default.
BaseAlgorithm.prepare lower-cases both the candidate and the query before
score ever sees them unless caseSensitive is true.
NormalizedNaive is the one exception: it overrides prepare to fold
ambiguous characters instead of calling the base implementation, and its
folding always lower-cases along the way. caseSensitive is still accepted
on its options for consistency with every other algorithm, but it has no
effect there: case is always ignored.