From 538e81db56a5ca9c8d7ddd288ae2dcd8066c13ab Mon Sep 17 00:00:00 2001 From: Chloe Kudryavtsev Date: Thu, 1 Sep 2022 08:21:43 -0400 Subject: [PATCH] client: optimize, simplify and smartify emoji picker search The query is split up on spaces, and we search for each of those terms, in order, anywhere in the emoji name or any aliases/keywords. This is done in a single filter pass against a compiled regex, making the process reasonably performant. Based on rough estimates, it should be between 2 and 5x faster than the old implementation, depending on several factors. There is a natural space left in to sort by relevancy (not done yet). It should also be easy to make the number of matches shown configurable. The number of matches is relevant, especially pre-sort. Another consideration is to delay the calculation by up to 300ms. --- .../client/src/components/emoji-picker.vue | 151 +++--------------- 1 file changed, 19 insertions(+), 132 deletions(-) diff --git a/packages/client/src/components/emoji-picker.vue b/packages/client/src/components/emoji-picker.vue index f32a65f62..03ba29184 100644 --- a/packages/client/src/components/emoji-picker.vue +++ b/packages/client/src/components/emoji-picker.vue @@ -126,145 +126,32 @@ const searchResultCustom = ref([]); const searchResultUnicode = ref([]); const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index'); +function emojiSearch(src: Type[], max: number, query: string): Type[] { + // discount fuzzy matching pattern + const re = new RegExp(query.split(' ').join('.*'), 'i'); + const match = (str: string): boolean => str && re.test(str); + const matches = src.filter(emoji => + match(emoji.name) + || emoji.aliases?.some(match) // custom emoji + || emoji.keywords?.some(match) // unicode emoji + ); + // TODO: sort matches by distance to query + if (max <= 0 || matches.length < max) return matches; + return matches.slice(0, max); +} + watch(q, () => { if (emojis.value) emojis.value.scrollTop = 0; - if (q.value == null || q.value === '') { + const query = q.value; + if (query == null || query === '') { searchResultCustom.value = []; searchResultUnicode.value = []; return; } - - const newQ = q.value.replace(/:/g, '').toLowerCase(); - - const searchCustom = () => { - const max = 8; - const emojis = customEmojis; - const matches = new Set(); - - const exactMatch = emojis.find(emoji => emoji.name === newQ); - if (exactMatch) matches.add(exactMatch); - - if (newQ.includes(' ')) { // AND検索 - const keywords = newQ.split(' '); - - // 名前にキーワードが含まれている - for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - // 名前またはエイリアスにキーワードが含まれている - for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - } else { - for (const emoji of emojis) { - if (emoji.name.startsWith(newQ)) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.aliases.some(alias => alias.startsWith(newQ))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.name.includes(newQ)) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.aliases.some(alias => alias.includes(newQ))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - } - - return matches; - }; - - const searchUnicode = () => { - const max = 8; - const emojis = emojilist; - const matches = new Set(); - - const exactMatch = emojis.find(emoji => emoji.name === newQ); - if (exactMatch) matches.add(exactMatch); - - if (newQ.includes(' ')) { // AND検索 - const keywords = newQ.split(' '); - - // 名前にキーワードが含まれている - for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - // 名前またはエイリアスにキーワードが含まれている - for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - } else { - for (const emoji of emojis) { - if (emoji.name.startsWith(newQ)) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.keywords.some(keyword => keyword.startsWith(newQ))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.name.includes(newQ)) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.keywords.some(keyword => keyword.includes(newQ))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - } - - return matches; - }; - - searchResultCustom.value = Array.from(searchCustom()); - searchResultUnicode.value = Array.from(searchUnicode()); + + searchResultCustom.value = emojiSearch(instance.emojis, 10, query); + searchResultUnicode.value = emojiSearch(emojilist, 10, query); }); function focus() {