More fuzzy finder (#860)

This commit is contained in:
Armin Ronacher 2026-01-19 22:22:51 +01:00 committed by GitHub
parent d276c9fbe0
commit d37b5a52d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 73 additions and 42 deletions

View file

@ -13,53 +13,79 @@ export function fuzzyMatch(query: string, text: string): FuzzyMatch {
const queryLower = query.toLowerCase();
const textLower = text.toLowerCase();
if (queryLower.length === 0) {
return { matches: true, score: 0 };
}
if (queryLower.length > textLower.length) {
return { matches: false, score: 0 };
}
let queryIndex = 0;
let score = 0;
let lastMatchIndex = -1;
let consecutiveMatches = 0;
for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {
if (textLower[i] === queryLower[queryIndex]) {
const isWordBoundary = i === 0 || /[\s\-_./:]/.test(textLower[i - 1]!);
// Reward consecutive matches
if (lastMatchIndex === i - 1) {
consecutiveMatches++;
score -= consecutiveMatches * 5;
} else {
consecutiveMatches = 0;
// Penalize gaps
if (lastMatchIndex >= 0) {
score += (i - lastMatchIndex - 1) * 2;
}
}
// Reward word boundary matches
if (isWordBoundary) {
score -= 10;
}
// Slight penalty for later matches
score += i * 0.1;
lastMatchIndex = i;
queryIndex++;
const matchQuery = (normalizedQuery: string): FuzzyMatch => {
if (normalizedQuery.length === 0) {
return { matches: true, score: 0 };
}
if (normalizedQuery.length > textLower.length) {
return { matches: false, score: 0 };
}
let queryIndex = 0;
let score = 0;
let lastMatchIndex = -1;
let consecutiveMatches = 0;
for (let i = 0; i < textLower.length && queryIndex < normalizedQuery.length; i++) {
if (textLower[i] === normalizedQuery[queryIndex]) {
const isWordBoundary = i === 0 || /[\s\-_./:]/.test(textLower[i - 1]!);
// Reward consecutive matches
if (lastMatchIndex === i - 1) {
consecutiveMatches++;
score -= consecutiveMatches * 5;
} else {
consecutiveMatches = 0;
// Penalize gaps
if (lastMatchIndex >= 0) {
score += (i - lastMatchIndex - 1) * 2;
}
}
// Reward word boundary matches
if (isWordBoundary) {
score -= 10;
}
// Slight penalty for later matches
score += i * 0.1;
lastMatchIndex = i;
queryIndex++;
}
}
if (queryIndex < normalizedQuery.length) {
return { matches: false, score: 0 };
}
return { matches: true, score };
};
const primaryMatch = matchQuery(queryLower);
if (primaryMatch.matches) {
return primaryMatch;
}
if (queryIndex < queryLower.length) {
return { matches: false, score: 0 };
const alphaNumericMatch = queryLower.match(/^(?<letters>[a-z]+)(?<digits>[0-9]+)$/);
const numericAlphaMatch = queryLower.match(/^(?<digits>[0-9]+)(?<letters>[a-z]+)$/);
const swappedQuery = alphaNumericMatch
? `${alphaNumericMatch.groups?.digits ?? ""}${alphaNumericMatch.groups?.letters ?? ""}`
: numericAlphaMatch
? `${numericAlphaMatch.groups?.letters ?? ""}${numericAlphaMatch.groups?.digits ?? ""}`
: "";
if (!swappedQuery) {
return primaryMatch;
}
return { matches: true, score };
const swappedMatch = matchQuery(swappedQuery);
if (!swappedMatch.matches) {
return primaryMatch;
}
return { matches: true, score: swappedMatch.score + 5 };
}
/**

View file

@ -53,6 +53,11 @@ describe("fuzzyMatch", () => {
assert.strictEqual(notAtBoundary.matches, true);
assert.ok(atBoundary.score < notAtBoundary.score);
});
it("matches swapped alpha numeric tokens", () => {
const result = fuzzyMatch("codex52", "gpt-5.2-codex");
assert.strictEqual(result.matches, true);
});
});
describe("fuzzyFilter", () => {