Search

Quick Start

A complete, worked search pipeline from an empty Search instance to a SearchResult.

This walks through one realistic pipeline end to end: a small product catalog, an exact match tried first, a normalized fallback for garbled serial numbers, and edit distance as a last resort for typos.

import {
	KMP,
	Levenshtein,
	NaiveSearch,
	NormalizedNaive,
	Search,
} from "@michaelrwalker/search";

const allModels = [
	"iPhone 15 Pro",
	"iPhone 15 Pro Max",
	"MacBook Pro 16",
	"MacBook Air 13",
	"iPad Mini",
	"Apple Watch Ultra 2",
];

const catalog = new Search()
	.register(NaiveSearch())
	.register(NormalizedNaive())
	.register(KMP())
	.register(Levenshtein({ threshold: 0.75 }))
	.defineDataset(allModels)
	.runOrder(["naive", "normalized-naive", "kmp", "levenshtein"])
	.defineBehavior("stop-on-match");

catalog.search("iPad Mini");

What happens on an exact query

"iPad Mini" is an exact entry, so naive matches it on the first pass with confidence 1. Because the pipeline behavior is "stop-on-match", the pipeline stops there. normalized-naive, kmp and levenshtein never run:

{
	query: "iPad Mini",
	matched: true,
	best: {
		index: 4,
		confidence: 1,
		candidate: "iPad Mini",
		position: 0,
		algorithm: "naive",
	},
	matches: [ /* the one match above */ ],
	byAlgorithm: { naive: [ /* ... */ ] },
	ranAlgorithms: ["naive"],
}

What happens on a typo

"MacBook Pr 16" matches nothing exactly, so naive, normalized-naive and kmp all run and all come up empty. levenshtein is the one that saves it:

catalog.search("MacBook Pr 16");
{
	query: "MacBook Pr 16",
	matched: true,
	best: {
		index: 2,
		confidence: 0.9285714285714286,
		candidate: "MacBook Pro 16",
		algorithm: "levenshtein",
	},
	matches: [ /* the one match above */ ],
	byAlgorithm: {
		naive: [],
		"normalized-naive": [],
		kmp: [],
		levenshtein: [ /* ... */ ],
	},
	ranAlgorithms: ["naive", "normalized-naive", "kmp", "levenshtein"],
}

byAlgorithm lists every algorithm that ran, including the three that found nothing: an empty array means “ran, no hit,” not “did not run.” Compare that to ranAlgorithms, which is the actual list of algorithms the pipeline reached; it is shorter than the run order whenever stop-on-match ends things early.

Ranking instead of resolving

Not every use case wants one winner. Flip the behavior to "find-all" and every registered algorithm scores the whole dataset, which is what you want when you are ranking suggestions rather than resolving a single entry:

const suggestions = new Search()
	.register(KMP())
	.register(Levenshtein({ threshold: 0.4 }))
	.defineDataset(allModels)
	.defineBehavior("find-all");

const result = suggestions.search("MacBook");
result.best;              // the single highest-confidence match
result.byAlgorithm.kmp;   // just KMP's matches, if you want to render them separately

From here, Pipeline Stages walks through each method on Search in order, and Run Order and Type Safety covers the type-level guarantee that made runOrder(["naive", "normalized-naive", "kmp", "levenshtein"]) above safe to write.