Skip to content
scout logoScoutUtilities
Trigram-indexed fuzzy search with per-field weights, match highlighting, and an optional reactive layer.
Version
v2.2.0
Size
4.9 KB gzip
BrowserNode ≥22SSRDeno
createIndexcreateReactiveSearchcreateSearchScoutConfigurationErrorScoutDisposedError View all 13 exports

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);
FeatureArsenal fuzzy*Scout createIndexFuse.js
Bundle size~3 KB4.9 KB~23 KB
Zero dependencies @vielzeug/ripple runtime dependency
AlgorithmLevenshteinTrigram + overlap coefficientBitap
Query timeO(n·m)O(candidates)O(n·m)
Stateful index
Match highlighting
Reactive layerripple signals + debounce
Incremental updatesPartial

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/scout
sh
npm install @vielzeug/scout
sh
yarn add @vielzeug/scout

Quick 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 Lovelace

Features

  • createIndex() — Trigram inverted index; construction O(corpus × field_length), query O(candidates)
  • Per-field weights — Promote name matches over secondary fields; finite positive weights and custom stringify functions supported
  • createReactiveSearch() — Index + reactive SearchState in one call; .index for incremental mutations
  • createSearch() — Reactive search state backed by an existing ScoutIndex; share one index across many states
  • highlight() / highlightField() — Split field text into HighlightPart[] fragments for styled rendering
  • findMatchRanges() — Compute match ranges for custom display strings (truncated previews, formatted values)
  • toSearchMatcher() — Matcher adapter for sourcerer's LocalSource
  • toFilterPredicate() — Snapshot (item: T) => boolean predicate for Array.filter or vault queries
  • setItems() — 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; powers createSearch()'s reactivity and bulk reconciliation
  • segmentWords() — Split unsegmented-script text (CJK, Thai, ...) into words via native Intl.Segmenter
  • Debug logging via debugSearch() (@vielzeug/scout/devtools) — logs query/results transitions, tree-shaken from production bundles

Documentation

See Also

  • Arsenal — Use fuzzyFilter for ad-hoc filtering of small lists (< 200 items) without building an index
  • RipplecreateReactiveSearch() and createSearch() use Ripple signals for reactive query state and debounce
  • Sourcerer — use a ScoutIndex inside createLocalSource's explicit match callback
  • VaulttoFilterPredicate() wraps a one-time Scout query as a vault-compatible filter() predicate