From 9275193333b0d391a6644220f81c8609391d7275 Mon Sep 17 00:00:00 2001 From: Ushie Date: Sat, 27 Apr 2024 20:29:52 +0300 Subject: [PATCH] feat: Add fuzzy search to patches search (#229) * feat: Fuzzy Search Co-authored-by: Kendell R * slightly change the init logic * fix behavior * fix sort behavior i am so good at reading docs * update the search results on load * switch to fuse js * lower the threshold per @oSumAtrIX request --------- Co-authored-by: Kendell R Co-authored-by: afn --- package.json | 1 + pnpm-lock.yaml | 8 +++ src/lib/components/Search.svelte | 4 +- src/routes/patches/+page.svelte | 114 ++++++++++++++----------------- 4 files changed, 64 insertions(+), 63 deletions(-) diff --git a/package.json b/package.json index 7e4067f..58fb3fd 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.35.1", + "fuse.js": "^7.0.0", "imagetools-core": "^6.0.3", "prettier": "^3.1.1", "prettier-plugin-svelte": "^3.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1de767..f8d2c14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,6 +43,9 @@ devDependencies: eslint-plugin-svelte: specifier: ^2.35.1 version: 2.35.1(eslint@8.56.0)(svelte@4.2.8) + fuse.js: + specifier: ^7.0.0 + version: 7.0.0 imagetools-core: specifier: ^6.0.3 version: 6.0.3 @@ -1493,6 +1496,11 @@ packages: dev: true optional: true + /fuse.js@7.0.0: + resolution: {integrity: sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==} + engines: {node: '>=10'} + dev: true + /get-port@3.2.0: resolution: {integrity: sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==} engines: {node: '>=4'} diff --git a/src/lib/components/Search.svelte b/src/lib/components/Search.svelte index 765dc92..45aff05 100644 --- a/src/lib/components/Search.svelte +++ b/src/lib/components/Search.svelte @@ -6,11 +6,11 @@ export let title: string; export let searchTerm: string | null; - export let searchTermFiltered: string | undefined; + export let displayedTerm: string | undefined; function clear() { searchTerm = ''; - searchTermFiltered = ''; + displayedTerm = ''; const url = new URL($page.url); url.searchParams.delete('s'); diff --git a/src/routes/patches/+page.svelte b/src/routes/patches/+page.svelte index 0331dd6..d5b59b4 100644 --- a/src/routes/patches/+page.svelte +++ b/src/routes/patches/+page.svelte @@ -10,7 +10,6 @@ import { createQuery } from '@tanstack/svelte-query'; import { queries } from '$data/api'; - import { JsonLd } from 'svelte-meta-tags'; import Head from '$lib/components/Head.svelte'; import PackageMenu from './PackageMenu.svelte'; import Package from './Package.svelte'; @@ -20,9 +19,13 @@ import FilterChip from '$lib/components/FilterChip.svelte'; import Dialogue from '$lib/components/Dialogue.svelte'; import Query from '$lib/components/Query.svelte'; + import Fuse from 'fuse.js'; + import { onMount } from 'svelte'; const query = createQuery(['patches'], queries.patches); + let searcher: Fuse | undefined; + let searchParams: Readable; if (building) { searchParams = readable(new URLSearchParams()); @@ -31,15 +34,8 @@ } $: selectedPkg = $searchParams.get('pkg'); - let searchTerm = $searchParams.get('s'); - let searchTermFiltered = searchTerm - ?.replace(/\./g, '') - .replace(/\s/g, '') - .replace(/-/g, '') - .replace(/_/g, '') - .toLowerCase(); + let searchTerm = $searchParams.get('s') || ''; - let timeout: ReturnType; let mobilePackages = false; let showAllVersions = false; @@ -50,61 +46,58 @@ return !!patch.compatiblePackages?.find((compat) => compat.name === pkg); } - function searchString(str?: string, term: string, filter: RegExp) { - return str?.toLowerCase().replace(filter, '').includes(term); - } - function filter(patches: Patch[], pkg: string, search?: string): Patch[] { - if (search === undefined && pkg === '') { - return patches; + if (!search) { + if (pkg) return patches.filter((patch) => checkCompatibility(patch, pkg)); + else return patches; } - return patches.filter((patch) => { - // Don't show if the patch doesn't support the selected package - if (pkg && !checkCompatibility(patch, pkg)) { - return false; - } + if (!searcher) { + searcher = new Fuse(patches, { + keys: ['name', 'description', 'compatiblePackages.name', 'compatiblePackages.versions'], + shouldSort: true, + threshold: 0.3 + }); + } - // Filter based on the search term. - if (search !== undefined) { - return ( - searchString(patch.description, search, /\s/g) || - searchString(patch.name, search, /\s/g) || - patch.compatiblePackages?.find((x) => searchString(x.name, search, /\./g)) - ); - } - return true; - }); + const result = searcher + .search(search) + .map(({ item }) => item) + .filter((item) => { + // Don't show if the patch doesn't support the selected package + if (pkg && !checkCompatibility(item, pkg)) { + return false; + } + return true; + }); + return result; } // Make sure we don't have to filter the patches after every key press - const debounce = () => { - clearTimeout(timeout); - timeout = setTimeout(() => { - // Filter search term for better results (i.e. " Unl O-ck" and "unlock" gives the same results) - searchTermFiltered = searchTerm - ?.replace(/\./g, '') - .replace(/\s/g, '') - .replace(/-/g, '') - .replace(/_/g, '') - .toLowerCase(); - // Update search URL params - // must use history.pushState instead of goto(), as goto() unselects the search bar - const url = new URL(window.location.href); - url.pathname = '/patches'; - - const params = new URLSearchParams(); - if (selectedPkg) { - params.set('pkg', selectedPkg); - } - if (searchTerm) { - params.set('s', searchTerm); - } - - url.search = params.toString(); - window.history.pushState(null, '', url.toString()); - }, 500); + let displayedTerm = ''; + const debounce = (f: (...args: T) => void) => { + let timeout: number; + return (...args: T) => { + clearTimeout(timeout); + timeout = setTimeout(() => f(...args), 350); + }; }; + const update = () => { + displayedTerm = searchTerm; + + const url = new URL(window.location.href); + url.pathname = '/patches'; + + if (searchTerm) { + url.searchParams.set('s', searchTerm); + } else { + url.searchParams.delete('s'); + } + + window.history.pushState(null, '', url); + }; + + onMount(update);
-
@@ -192,9 +184,9 @@
- {#each filter(data.patches, selectedPkg || '', searchTermFiltered) as patch} + {#each filter(data.patches, selectedPkg || '', displayedTerm) as patch} - {#key selectedPkg || searchTermFiltered} + {#key selectedPkg || displayedTerm}