Skip to content

Commit

Permalink
refactor(libs): split CustomSearch code into multiple files
Browse files Browse the repository at this point in the history
  • Loading branch information
ivangabriele committed Sep 28, 2023
1 parent 012f05c commit 75c9a78
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 207 deletions.
205 changes: 0 additions & 205 deletions src/libs/CustomSearch.ts

This file was deleted.

110 changes: 110 additions & 0 deletions src/libs/CustomSearch/index.ts
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 }
47 changes: 47 additions & 0 deletions src/libs/CustomSearch/types.ts
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
}>
Loading

0 comments on commit 75c9a78

Please sign in to comment.