Skip to content
This repository has been archived by the owner on Jan 15, 2025. It is now read-only.

optimization: only sort top N items when numResultsShown option is passed #9

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Node Attach",
"port": 9229,
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "pwa-node",
"continueOnAttach": true
},
]
}
133 changes: 94 additions & 39 deletions docs/bundle.js

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

2 changes: 1 addition & 1 deletion docs/bundle.js.map

Large diffs are not rendered by default.

15 changes: 3 additions & 12 deletions docs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ async function fetchArticles(): Promise<Article[]> {
class ArticleList {
private elRefs: Record<string, HTMLElement>;
private state: ArticleListState;
private searchFilterDebounceTimeout: number = -1;

constructor(articles: Article[], elRefs: Record<string, HTMLElement>) {
this.state = {
Expand All @@ -71,15 +70,7 @@ class ArticleList {
this.elRefs.results.scrollTop = 0;
const searchFilter = (ev.currentTarget as HTMLInputElement).value;
const numItemsToShow = NUM_INITIAL_ITEMS_TO_SHOW;

// debounce searchFilter for 100ms if input length is <= 3
// for large set of results (>10k) the native js .sort() overheard could be >100ms
window.clearTimeout(this.searchFilterDebounceTimeout);
if (searchFilter && searchFilter.length <= 3) {
this.searchFilterDebounceTimeout = window.setTimeout(() => this.update({searchFilter, numItemsToShow}), 100);
} else {
this.update({searchFilter, numItemsToShow});
}
this.update({searchFilter, numItemsToShow});
}

handleResultsScroll(ev: Event) {
Expand All @@ -96,8 +87,8 @@ class ArticleList {
}

fuzzyFilterArticles(): Array<FuzzyFilterResult<Article>> {
const {articles, searchFilter} = this.state;
return fuzzyFilter(articles, searchFilter, {fields: [`name`]});
const {articles, searchFilter, numItemsToShow} = this.state;
return fuzzyFilter(articles, searchFilter, {fields: [`name`], numResultsShown: numItemsToShow});
}

render() {
Expand Down
40 changes: 32 additions & 8 deletions fuzzbunny.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,45 @@ declare module "fuzzbunny" {
score: number;
highlights: { [K in keyof Item]?: string[] | undefined; };
};
export type FuzzyFilterOptions<Item> = {
/**
* - fields of the item object that will be searched
*/
fields: (keyof Item)[];
/**
* - maximum number of results that will be displayed in UI.
* Since sorting large arrays is expensive, only top N results are sorted by score
*/
numResultsShown?: number;
};
/**
* @template Item
* @typedef {{item: Item, score: number, highlights: {[K in keyof Item]?: string[]}}} FuzzyFilterResult
*/
/**
* @template Item
* @typedef {Object} FuzzyFilterOptions
* @prop {(keyof Item)[]} fields - fields of the item object that will be searched
* @prop {number} [numResultsShown] - maximum number of results that will be displayed in UI.
* Since sorting large arrays is expensive, only top N results are sorted by score
*/
/**
* Searches an array of items on props and returns filtered + sorted array with scores and highlights
* @template Item
* @param {Item[]} items
* @param {string} searchStr
* @param {{fields: (keyof Item)[]}} options
* @param {Item[]} items - list of objects to search on
* @param {string} searchStr - the search string
* @param {FuzzyFilterOptions<Item>} options - what fields to search on, and other options
* @returns {FuzzyFilterResult<Item>[]}
*
* @example
* fuzzyFilter([
* {frist: 'Hello', last: 'World'},
* {frist: 'Foo', last: 'Bar'},
* ], {
* fields: ['first', 'last'],
* })
*/
export function fuzzyFilter<Item>(items: Item[], searchStr: string, options: {
fields: (keyof Item)[];
}): {
export function fuzzyFilter<Item>(items: Item[], searchStr: string, options: FuzzyFilterOptions<Item>): {
item: Item;
score: number;
highlights: { [K in keyof Item]?: string[] | undefined; };
Expand All @@ -34,14 +58,14 @@ declare module "fuzzbunny" {
highlights: string[];
} | null;
/**
* fuzzyMatchSanitized is called by fuzzyMatch, it's a slightly lower level call
* fuzzyScoreItem is called by fuzzyMatch, it's a slightly lower level call
* If perf is of importance and you want to avoid lowercase + trim + highlighting on every item
* Use this and only call highlightsFromRanges for only the items that are displayed
* @param {string} targetStr - lowercased trimmed target string to search on
* @param {string} searchStr - lowercased trimmed search string
* @returns {{score: number, ranges: number[]} | null} - null if no match
*/
export function fuzzyMatchSanitized(targetStr: string, searchStr: string): {
export function fuzzyScoreItem(targetStr: string, searchStr: string): {
score: number;
ranges: number[];
} | null;
Expand Down
Loading