From b39224073e47257b1629b6f71c423bf35f2da0e2 Mon Sep 17 00:00:00 2001 From: rektdeckard Date: Tue, 15 Sep 2020 01:07:40 -0400 Subject: [PATCH] selectors: add fuzzy-find search capabilities to filteredQueryResults Using the Fuse package, we now support fuzzy icon search. Results are weighted in favor of icon names and sorted by match score, improving search utility by surfacing best matches to the top of the list. There is still some fine-tuning to do, as threshold often matches unrelated strings, while missing more related but less-similar string queries. In future, we should play with the threshold, location, distance, and possibly the extendedSearch options. --- package.json | 1 + src/state/selectors.ts | 21 +++++++++------------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 19dbb3b..e1af5a5 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "dependencies": { "file-saver": "^2.0.2", "framer-motion": "^2.1.0", + "fuse.js": "^6.4.1", "phosphor-react": "^0.2.2", "react": "^17.0.0-rc.0", "react-dom": "^17.0.0-rc.0", diff --git a/src/state/selectors.ts b/src/state/selectors.ts index 81aa453..a666c7b 100644 --- a/src/state/selectors.ts +++ b/src/state/selectors.ts @@ -1,28 +1,25 @@ import { selector, selectorFamily } from "recoil"; import TinyColor from "tinycolor2"; +import Fuse from "fuse.js"; -import { searchQueryAtom, iconStyleAtom, iconColorAtom } from "./atoms"; +import { searchQueryAtom, iconColorAtom } from "./atoms"; import { IconEntry, IconCategory } from "../lib"; import { icons } from "../lib/icons"; -const isQueryMatch = (icon: IconEntry, query: string): boolean => { - return ( - icon.name.includes(query) || - icon.tags.some((tag) => tag.toLowerCase().includes(query)) || - icon.categories.some((category) => category.toLowerCase().includes(query)) - ); -}; +const fuse = new Fuse(icons, { + keys: [{ name: "name", weight: 2 }, "tags", "categories"], + threshold: 0.2, // Tweak this to what feels like the right number of results + // shouldSort: false, // We may want to sort if we find too many results? +}); export const filteredQueryResultsSelector = selector>({ key: "filteredQueryResultsSelector", get: ({ get }) => { const query = get(searchQueryAtom).trim().toLowerCase(); - const style = get(iconStyleAtom); - - if (!query && !style) return icons; + if (!query) return icons; return new Promise((resolve) => - resolve(icons.filter((icon) => isQueryMatch(icon, query))) + resolve(fuse.search(query).map((value) => value.item)) ); }, });