Search
Overview
What Search is, why it is composable, and why running the wrong algorithm name is a compile error instead of a silent no-op.
@michaelrwalker/search is a composable, type-safe search pipeline for arrays
of strings. You register the algorithms you want, hand it a dataset, decide
what order they run in, and decide what should happen the moment one of them
hits.
There is no single “search” function to configure with a pile of flags.
Instead there is a set of interchangeable algorithms — eleven that ship with the
package and any you write yourself — that all implement the same run
contract and return matches in the same shape. Because the shape is shared,
you can register NaiveSearch next to a fuzzy phonetic matcher next to your
own domain-specific scorer, and the pipeline treats them identically.
import { KMP, Levenshtein, NaiveSearch, Search } from "@michaelrwalker/search";
const results = new Search()
.register(NaiveSearch())
.register(KMP(), { stopOnMatch: true })
.register(Levenshtein())
.defineDataset(allModels)
.runOrder(["naive", "kmp", "levenshtein"])
.defineBehavior("find-all")
.search("MacBook Pr 16");
Why a pipeline, not a function
Real search over a small, known dataset is rarely one algorithm. A product
catalog wants an exact match to win outright, a normalized match to survive
a mis-typed serial number, and a fuzzy match to catch a typo, but you do not
want to pay for edit-distance scoring on every query when a plain string
comparison would have answered it. Search models that directly: register
each algorithm, put the cheap ones first, and tell the pipeline to stop as
soon as one of them is confident enough.
The type-safety story
register is generic over the algorithm’s name, and each call widens the
Search instance’s type parameter with that literal name:
class Search<TAlgorithms extends string = never> {
register<TName extends string>(
algorithm: BaseAlgorithm<TName>,
options?: RegistrationOptions,
): Search<TAlgorithms | TName>;
runOrder(order: readonly TAlgorithms[]): this;
}
After .register(NaiveSearch()).register(KMP()), the instance’s type
parameter is "naive" | "kmp". runOrder only accepts that union, so your
editor autocompletes the algorithms you actually registered, and a typo such
as "levenstein" fails to compile: it never reaches production as a run
order entry that silently matches nothing. The same names are also checked at
runtime with a SearchConfigError, which is what protects plain JavaScript
callers who do not have the compiler watching.
This is the library’s headline feature: the pipeline you configure and the
type the compiler checks it against are the same set of names, kept in sync
automatically as you add or remove .register() calls.
What ships
- Eleven bundled algorithms covering exact, normalized, substring, prefix, positional, edit-distance, phonetic and subsequence matching. See Bundled Algorithms.
- A
BaseAlgorithmbase class so a custom algorithm only has to implement one method,score. See Writing a Custom Algorithm. - Zero runtime dependencies. The package ships as TypeScript source: there is
no build step, no compiled
dist, so your bundler or Node’s own type stripping compiles it alongside the rest of your code.