Add fast path for single-character patterns in FuzzyMatchV2

For single ASCII character, jump between occurrences with vectorized
search and score each directly from bonus matrix instead of running
full pipeline with prefilter and score arrays. 2x faster on 100K-line
benchmark corpus (5.4ms -> 2.5ms). Matters most on first keystroke
where candidate set is largest and result cache cannot help.
This commit is contained in:
Junegunn Choi
2026-07-23 20:48:20 +09:00
parent 224310d7a6
commit 9c401f9c9a
+44
View File
@@ -425,6 +425,44 @@ func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []in
}
}
// fuzzyMatchV2Single is a fast path for a single-character ASCII pattern on
// ASCII input. Same scoring and tiebreaks as Phase 2 of FuzzyMatchV2, but
// jumps between occurrences instead of scanning every character, and
// allocates no arrays.
func fuzzyMatchV2Single(caseSensitive bool, forward bool, input *util.Chars, b byte, withPos bool) (Result, *[]int) {
byteArray := input.Bytes()
maxScore, maxScorePos := int16(0), -1
for idx := 0; idx < len(byteArray); {
idx = trySkip(input, caseSensitive, b, idx)
if idx < 0 {
break
}
class := asciiCharClasses[byteArray[idx]]
prevClass := initialCharClass
if idx > 0 {
prevClass = asciiCharClasses[byteArray[idx-1]]
}
bonus := bonusMatrix[prevClass][class]
score := scoreMatch + bonus*bonusFirstCharMultiplier
if forward && score > maxScore || !forward && score >= maxScore {
maxScore, maxScorePos = score, idx
if forward && bonus >= bonusBoundary {
break
}
}
idx++
}
if maxScorePos < 0 {
return Result{-1, -1, 0}, nil
}
result := Result{maxScorePos, maxScorePos + 1, int(maxScore)}
if !withPos {
return result, nil
}
pos := []int{maxScorePos}
return result, &pos
}
func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
// Assume that pattern is given in lowercase if case-insensitive.
// First check if there's a match and calculate bonus for each position.
@@ -447,6 +485,12 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab)
}
// Single-character ASCII pattern needs neither the prefilter nor the
// score matrix
if M == 1 && input.IsBytes() && pattern[0] < utf8.RuneSelf {
return fuzzyMatchV2Single(caseSensitive, forward, input, byte(pattern[0]), withPos)
}
// Phase 1. Optimized search for ASCII string
minIdx, maxIdx := asciiFuzzyIndex(input, pattern, caseSensitive)
if minIdx < 0 {