Skip to content

Commit

Permalink
feat: Add fuzzy search to patches search (#229)
Browse files Browse the repository at this point in the history
* feat: Fuzzy Search

Co-authored-by: Kendell R <[email protected]>

* 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 <[email protected]>
Co-authored-by: afn <[email protected]>
  • Loading branch information
3 people committed Apr 27, 2024
1 parent f86456c commit 9275193
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 65 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/lib/components/Search.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
118 changes: 55 additions & 63 deletions src/routes/patches/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<Patch> | undefined;
let searchParams: Readable<URLSearchParams>;
if (building) {
searchParams = readable(new URLSearchParams());
Expand All @@ -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 timeout: ReturnType<typeof setTimeout>;
let searchTerm = $searchParams.get('s') || '';
let mobilePackages = false;
let showAllVersions = false;
Expand All @@ -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;
}
// 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;
});
if (!searcher) {
searcher = new Fuse(patches, {
keys: ['name', 'description', 'compatiblePackages.name', 'compatiblePackages.versions'],
shouldSort: true,
threshold: 0.3
});
}
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 = <T extends any[]>(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);
</script>

<Head
Expand Down Expand Up @@ -135,12 +128,11 @@
<div class="search">
<div class="search-contain">
<!-- Must bind both variables: we get searchTerm from the text input, -->
<!-- and searchTermFiltered gets cleared with the clear button -->
<Search
bind:searchTerm
bind:searchTermFiltered
bind:displayedTerm
title="Search for patches"
on:keyup={debounce}
on:keyup={debounce(update)}
/>
</div>
</div>
Expand Down Expand Up @@ -192,9 +184,9 @@
</aside>

<div class="patches-container">
{#each filter(data.patches, selectedPkg || '', searchTermFiltered) as patch}
{#each filter(data.patches, selectedPkg || '', displayedTerm) as patch}
<!-- Trigger new animations when package or search changes (I love Svelte) -->
{#key selectedPkg || searchTermFiltered}
{#key selectedPkg || displayedTerm}
<div in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<PatchItem {patch} bind:showAllVersions />
</div>
Expand Down

0 comments on commit 9275193

Please sign in to comment.