-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(libs): split CustomSearch code into multiple files
- Loading branch information
1 parent
012f05c
commit 75c9a78
Showing
5 changed files
with
218 additions
and
207 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import diacritics from 'diacritics' | ||
import Fuse from 'fuse.js' | ||
|
||
import { cleanCollectionDiacritics } from './utils/cleanCollectionDiacritics' | ||
|
||
import type { CustomSearchKey, CustomSearchOptions } from './types' | ||
|
||
/** | ||
* We take advantage of the global JS scope to use this constant as a "singleton" cache | ||
* to avoid re-normalizing and re-search-indexing each time `CustomSearch` is instanciated. | ||
* | ||
* This cache will only be used if the `cacheKey` option is set while instanciating `CustomSearch`. | ||
*/ | ||
const FUSE_SEARCH_CACHE: Record< | ||
string, | ||
{ | ||
fuseSearchIndex: any | ||
normalizedCollection: any | ||
originalCollection: any | ||
} | ||
> = {} | ||
|
||
export class CustomSearch<T extends Record<string, any> = Record<string, any>> { | ||
#originalCollection: T[] | ||
#fuse: Fuse<T> | ||
/** See {@link CustomSearchOptions.isDiacriticSensitive}. */ | ||
#isDiacriticSensitive: boolean | ||
/** See {@link CustomSearchOptions.isStrict}. */ | ||
#isStrict: boolean | ||
|
||
constructor( | ||
collection: T[], | ||
keys: Array<CustomSearchKey<T>>, | ||
{ | ||
cacheKey, | ||
isCaseSensitive = false, | ||
isDiacriticSensitive = false, | ||
isStrict = false, | ||
shouldIgnoreLocation = true, | ||
threshold = 0.4 | ||
}: CustomSearchOptions = {} | ||
) { | ||
const maybeCache = cacheKey ? FUSE_SEARCH_CACHE[cacheKey] : undefined | ||
// eslint-disable-next-line no-nested-ternary | ||
const normalizedCollection: T[] = maybeCache | ||
? maybeCache.normalizedCollection | ||
: isDiacriticSensitive | ||
? collection | ||
: cleanCollectionDiacritics(collection, keys) | ||
|
||
this.#fuse = new Fuse( | ||
normalizedCollection, | ||
/* eslint-disable @typescript-eslint/naming-convention */ | ||
{ | ||
ignoreLocation: shouldIgnoreLocation, | ||
isCaseSensitive, | ||
keys, | ||
threshold, | ||
useExtendedSearch: isStrict | ||
}, | ||
/* eslint-enable @typescript-eslint/naming-convention */ | ||
maybeCache ? Fuse.parseIndex<T>(maybeCache.fuseSearchIndex) : undefined | ||
) | ||
this.#isDiacriticSensitive = isDiacriticSensitive | ||
this.#isStrict = isStrict | ||
this.#originalCollection = maybeCache ? maybeCache.originalCollection : collection | ||
|
||
if (cacheKey && !maybeCache) { | ||
FUSE_SEARCH_CACHE[cacheKey] = { | ||
fuseSearchIndex: this.#fuse.getIndex(), | ||
normalizedCollection, | ||
originalCollection: collection | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Searches the entire collection, and returns a list of items matching this query. | ||
* | ||
* @param query The keywords to look for | ||
* @param limit Denotes the max number of returned search results | ||
* @returns A list of matching items | ||
*/ | ||
public find(query: string, limit?: number): T[] { | ||
const normalizedQuery = (this.#isDiacriticSensitive ? query : diacritics.remove(query)).trim() | ||
|
||
// Here we use Fuse.js `useExtendedSearch` option to avoid fuzziness | ||
// when `CustomSearch` `isStrict` option is set to `true`. | ||
// In that case we want each space-separated `query` keywords to be strict | ||
// and only return results containing words exactly including each of these keywords. | ||
// I.e.: Looking for "ÉMOI" in ["mÉMOIre", "mÉMOrIsable"] will return only the first one instead of returning both. | ||
// https://fusejs.io/examples.html#extended-search | ||
const extendedQuery = this.#isStrict | ||
? normalizedQuery | ||
.split(/\s+/) | ||
.map(keyword => `'${keyword}`) | ||
.join(' ') | ||
: normalizedQuery | ||
|
||
return ( | ||
this.#fuse | ||
.search(extendedQuery, limit ? { limit } : undefined) | ||
// We remap to the original collection since the normalized collection can have some accents removed | ||
// (because of the internal diacritic-less normalization) | ||
.map(({ refIndex }) => this.#originalCollection[refIndex] as T) | ||
) | ||
} | ||
} | ||
|
||
export type { CustomSearchKey, CustomSearchOptions } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import type Fuse from 'fuse.js' | ||
|
||
export type CustomSearchKey<T> = string | Fuse.FuseOptionKeyObject<T> | ||
|
||
export type CustomSearchOptions = Partial<{ | ||
/** Cache search index to avoid Must be unique in the entire application. */ | ||
cacheKey: string | undefined | ||
/** | ||
* Indicates whether comparisons should be case sensitive. | ||
* | ||
* @default false | ||
* @see https://fusejs.io/api/options.html#iscasesensitive | ||
*/ | ||
isCaseSensitive: boolean | ||
/** Indicates whether comparisons should be diacritic (= accent) sensitive. */ | ||
isDiacriticSensitive: boolean | ||
/** | ||
* Use strict keywords matching, disabling fuzziness. | ||
* | ||
* @default false | ||
* @description | ||
* Looking for "emoi" in ["mÉMOIre", "mÉMOrIsable"] will return only the first one if `isStrict` is `true`, | ||
* instead of returning both by default (`false`). | ||
*/ | ||
isStrict: boolean | ||
/** | ||
* By default, location is set to 0 and distance to 100 | ||
* When isStrict is false, for something to be considered a match, it would have to be within | ||
* (threshold) 0.4 x (distance) 100 = 40 characters away from the expected location 0. (i.e. the first 40 characters) | ||
* When true, search will ignore location and distance, so it won't matter where in the string the pattern appears. | ||
* | ||
* @default true | ||
* @see https://www.fusejs.io/concepts/scoring-theory.html#scoring-theory | ||
*/ | ||
shouldIgnoreLocation: boolean | ||
/** | ||
* At what point does the match algorithm give up. | ||
* | ||
* @default 0.4 | ||
* @description | ||
* A threshold of `0.0` requires a perfect match (of both letters and location), | ||
* a threshold of `1.0` would match anything. | ||
* | ||
* @see https://fusejs.io/api/options.html#threshold | ||
*/ | ||
threshold: number | ||
}> |
Oops, something went wrong.