Version v2.2.0 Size 4.9 KB gzip
Why Scout?
Arsenal's fuzzy / fuzzyFilter helpers perform pairwise Levenshtein distance — O(n·m) per item per query. For ≤200 items they are fine. For 500–100k items with real-time keystrokes, you need an index.
Scout builds a trigram inverted index at construction time. Query time scores only items sharing a trigram with the query; broad queries can still approach O(n), while selective queries avoid scoring the whole corpus.
ts
// Before
const matches = users.filter((user) => user.name.toLowerCase().includes(query.toLowerCase()));
// After
import { createIndex } from '@vielzeug/scout';
const index = createIndex(users, { fields: ['name', 'email'] });
const matches = index.search(query);| Feature | Arsenal fuzzy* | Scout createIndex | Fuse.js |
|---|---|---|---|
| Bundle size | ~3 KB | 4.9 KB | ~23 KB |
| Zero dependencies | @vielzeug/ripple runtime dependency | ||
| Algorithm | Levenshtein | Trigram + overlap coefficient | Bitap |
| Query time | O(n·m) | O(candidates) | O(n·m) |
| Stateful index | |||
| Match highlighting | |||
| Reactive layer | ripple signals + debounce | ||
| Incremental updates | Partial |
Use Scout when you need search over 500+ items, real-time UI search boxes (combobox, command palette), or reactive query state with ripple signals.
Consider arsenal.fuzzyFilter when you have fewer than 200 items and don't need a persistent index.
Installation
sh
pnpm add @vielzeug/scoutsh
npm install @vielzeug/scoutsh
yarn add @vielzeug/scoutQuick Start
ts
import { createIndex } from '@vielzeug/scout';
const users = [
{ email: 'ada@example.com', name: 'Ada Lovelace' },
{ email: 'grace@example.com', name: 'Grace Hopper' },
];
const index = createIndex(users, {
fields: [
{ field: 'name', weight: 2 },
{ field: 'email' },
],
});
const results = index.search('ada');
console.log(results[0]?.item.name); // Ada LovelaceFeatures
createIndex()— Trigram inverted index; construction O(corpus × field_length), query O(candidates)- Per-field weights — Promote
namematches over secondary fields; finite positive weights and customstringifyfunctions supported createReactiveSearch()— Index + reactiveSearchStatein one call;.indexfor incremental mutationscreateSearch()— Reactive search state backed by an existingScoutIndex; share one index across many stateshighlight()/highlightField()— Split field text intoHighlightPart[]fragments for styled renderingfindMatchRanges()— Compute match ranges for custom display strings (truncated previews, formatted values)toSearchMatcher()— Matcher adapter for sourcerer'sLocalSourcetoFilterPredicate()— Snapshot(item: T) => booleanpredicate forArray.filteror vault queriessetItems()— Reconcile a refreshed corpus by reference, preserve incoming order, and notify once- Incremental updates —
add()/remove()/reindex()patch individual items in O(field_length) onMutate()— Subscribe to index mutations; powerscreateSearch()'s reactivity and bulk reconciliationsegmentWords()— Split unsegmented-script text (CJK, Thai, ...) into words via nativeIntl.Segmenter- Debug logging via
debugSearch()(@vielzeug/scout/devtools) — logs query/results transitions, tree-shaken from production bundles
Documentation
See Also
- Arsenal — Use
fuzzyFilterfor ad-hoc filtering of small lists (< 200 items) without building an index - Ripple —
createReactiveSearch()andcreateSearch()use Ripple signals for reactive query state and debounce - Sourcerer — use a
ScoutIndexinsidecreateLocalSource's explicitmatchcallback - Vault —
toFilterPredicate()wraps a one-time Scout query as a vault-compatiblefilter()predicate