Skip to content

Commit 25fa235

Browse files
JSKittyclaude
andcommitted
fix: emoji search ranking buried primary matches under secondary keywords
Searching 'fire' was returning ❤️‍🔥 (heart on fire) before 🔥 (fire) because both tied on the previous score model — exact word match scored 0 regardless of which keyword position matched, and source order in arrEmojis decided ties. Three new ranking signals: - Display-name match: search === display scores -1.0, prefix match scores -0.5 - Word-position penalty: matching at keyword index N adds 0.02*N, so primary-keyword matches outrank secondary ones - Total-name-length tiebreaker: shorter name is more specific Applies to both the emoji picker search and the :shortcut autocomplete, since both call searchEmojis(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0100929 commit 25fa235

1 file changed

Lines changed: 59 additions & 53 deletions

File tree

src/js/emoji.js

Lines changed: 59 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1952,128 +1952,134 @@ for (cEmoji of arrEmojis) {
19521952
*/
19531953
function searchEmojis(search) {
19541954
if (!search || search.length < 1) return [];
1955-
1955+
19561956
search = search.toLowerCase();
19571957
const results = [];
1958-
1958+
19591959
for (const emojiData of arrEmojis) {
19601960
const name = emojiData.name.toLowerCase();
1961+
const display = (emojiData.display || '').toLowerCase();
19611962
const words = name.split(' ');
1962-
1963+
19631964
let bestMatch = null;
1964-
1965-
for (const word of words) {
1966-
// Exact match - highest priority
1965+
const consider = (score, word, wordIndex) => {
1966+
if (!bestMatch || score < bestMatch.score) {
1967+
bestMatch = { score, word, wordIndex };
1968+
}
1969+
};
1970+
1971+
// Display-name tier — strongest signal (search "fire" → 🔥 "fire" wins)
1972+
if (display === search) {
1973+
consider(-1.0, display, 0);
1974+
} else if (display.startsWith(search + ' ')) {
1975+
consider(-0.5, display, 0);
1976+
}
1977+
1978+
for (let i = 0; i < words.length; i++) {
1979+
const word = words[i];
1980+
1981+
// Exact word match — first-word matches outrank later positions.
1982+
// Without this, "fire" matches both 🔥 (word 0) and ❤️‍🔥 (word 1)
1983+
// identically; the emoji at word position 0 is the more specific
1984+
// referent and should rank first.
19671985
if (word === search) {
1968-
bestMatch = { score: 0, word };
1969-
break;
1986+
consider(0 + i * 0.02, word, i);
1987+
continue;
19701988
}
1971-
1989+
19721990
// Word starts with search
19731991
if (word.startsWith(search) && search.length >= 2) {
1974-
const score = 0.1;
1975-
if (!bestMatch || score < bestMatch.score) {
1976-
bestMatch = { score, word };
1977-
}
1992+
consider(0.1 + i * 0.02, word, i);
19781993
}
1979-
1994+
19801995
// Fuzzy matching for 3+ character searches
19811996
if (search.length >= 3) {
19821997
const lengthDiff = Math.abs(word.length - search.length);
1983-
1998+
19841999
// Check words with similar length
19852000
if (lengthDiff <= 1) {
19862001
const distance = levenshteinDistance(word, search);
1987-
2002+
19882003
if (distance === 1) {
19892004
// Verify sufficient character overlap
19902005
const searchChars = new Set(search);
19912006
const wordChars = new Set(word);
19922007
const commonChars = [...searchChars].filter(c => wordChars.has(c)).length;
1993-
2008+
19942009
if (commonChars >= search.length - 1) {
19952010
let score = 0.5;
1996-
1997-
// Better score for missing letter at end (hart → heart)
19982011
if (word.length > search.length && word.startsWith(search)) {
1999-
score = 0.3;
2000-
}
2001-
// Better score for missing letter in middle
2002-
else if (word.length > search.length &&
2003-
word[0] === search[0] &&
2012+
score = 0.3; // missing letter at end (hart → heart)
2013+
} else if (word.length > search.length &&
2014+
word[0] === search[0] &&
20042015
word[word.length-1] === search[search.length-1]) {
2005-
score = 0.35;
2006-
}
2007-
2008-
if (!bestMatch || score < bestMatch.score) {
2009-
bestMatch = { score, word };
2016+
score = 0.35; // missing letter in middle
20102017
}
2018+
consider(score + i * 0.02, word, i);
20112019
}
20122020
}
20132021
}
2014-
2022+
20152023
// Check if search matches beginning of longer words
20162024
if (word.length > search.length && lengthDiff <= 3) {
20172025
const wordStart = word.substring(0, search.length);
20182026
const distance = levenshteinDistance(wordStart, search);
2019-
2027+
20202028
if (distance === 1) {
2021-
const score = 0.6;
2022-
if (!bestMatch || score < bestMatch.score) {
2023-
bestMatch = { score, word };
2024-
}
2029+
consider(0.6 + i * 0.02, word, i);
20252030
}
20262031
}
20272032
}
2028-
2033+
20292034
// For 5+ character searches, allow distance 2
20302035
if (search.length >= 5) {
20312036
const distance = levenshteinDistance(word, search);
20322037
if (distance === 2 && Math.abs(word.length - search.length) <= 1) {
2033-
const score = 0.8;
2034-
if (!bestMatch || score < bestMatch.score) {
2035-
bestMatch = { score, word };
2036-
}
2038+
consider(0.8 + i * 0.02, word, i);
20372039
}
20382040
}
20392041
}
2040-
2042+
20412043
if (bestMatch) {
20422044
results.push({
20432045
...emojiData,
20442046
score: bestMatch.score,
2045-
matchedWord: bestMatch.word
2047+
matchedWord: bestMatch.word,
2048+
matchedIndex: bestMatch.wordIndex
20462049
});
20472050
}
20482051
}
2049-
2052+
20502053
// If no results found, fall back to includes search
20512054
if (results.length === 0) {
20522055
for (const emojiData of arrEmojis) {
20532056
const name = emojiData.name.toLowerCase();
20542057
if (name.includes(search)) {
20552058
results.push({
20562059
...emojiData,
2057-
score: 1.0, // Lower priority than fuzzy matches
2058-
matchedWord: name
2060+
score: 1.0,
2061+
matchedWord: name,
2062+
matchedIndex: 99
20592063
});
20602064
}
20612065
}
20622066
}
2063-
2067+
20642068
// Sort by relevance
20652069
return results
20662070
.sort((a, b) => {
20672071
if (Math.abs(a.score - b.score) > 0.01) return a.score - b.score;
2068-
2069-
// For same scores, prefer shorter matched words
2070-
const aLen = a.matchedWord.length;
2071-
const bLen = b.matchedWord.length;
2072-
if (aLen !== bLen) return aLen - bLen;
2073-
2072+
// Earlier-position matches first (primary keyword > secondary)
2073+
if (a.matchedIndex !== b.matchedIndex) return a.matchedIndex - b.matchedIndex;
2074+
// Shorter matched word is more specific
2075+
if (a.matchedWord.length !== b.matchedWord.length) return a.matchedWord.length - b.matchedWord.length;
2076+
// Shorter total name is more specific (fire-engine beats fire-engine-with-bells)
2077+
const aNameLen = a.name ? a.name.length : 0;
2078+
const bNameLen = b.name ? b.name.length : 0;
2079+
if (aNameLen !== bNameLen) return aNameLen - bNameLen;
20742080
return a.matchedWord.localeCompare(b.matchedWord);
20752081
})
2076-
.map(({ score, matchedWord, ...emoji }) => emoji);
2082+
.map(({ score, matchedWord, matchedIndex, ...emoji }) => emoji);
20772083
}
20782084

20792085
/** Return our most used emojis */

0 commit comments

Comments
 (0)