Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Bit-Parallel Pre-Filter

The core search is thorough but pays a per-position cost. When you search large inputs that are mostly non-matching text, with_prefilter() adds an opt-in fast lane: a bit-parallel (Bitap / Wu–Manber) approximate scan runs first, at hundreds of MB/s, to locate candidate regions, and the full weighted engine then re-searches only those regions.

Results are identical to a plain search with the same SearchOptions — the filter is a conservative over-approximation (a necessary condition), so it never drops a real match; it only spares the engine from scanning text that cannot contain one.

use fuzzy_aho_corasick::{FuzzyAhoCorasickBuilder, FuzzyLimits, SearchOptions};

let engine = FuzzyAhoCorasickBuilder::new()
    .fuzzy(FuzzyLimits::new().edits(1))
    .build(["vestibulum", "consectetur"]);

let pf = engine.with_prefilter(); // build once, reuse across searches
let hits = pf.search("… lorem vestibulm ipsum …", &SearchOptions::new().threshold(0.85)).unwrap();
// Same matches as engine.search(…).unwrap(), just faster on large, sparse inputs.

Why it’s sound

A match the engine accepts has a bounded number of edits: the threshold caps the total penalty a kept match may carry (P_max = N·(1 − θ/weight)), and each edit costs at least some minimum penalty. That bound becomes the bit-parallel scan’s Levenshtein budget k (a transposition counts as two unit edits), so every match the full engine could accept is guaranteed to survive the filter.

Graceful fallback

When the configuration can’t be reduced to the bit model, the wrapper transparently runs the full search instead — always correct, merely without the speedup. That happens when:

  • multi-character mappings are configured (block edits don’t map to unit Levenshtein),
  • a pattern is longer than 63 graphemes,
  • a penalty is so low that an edit is effectively free (the budget becomes unbounded), or
  • the derived budget is too large to stay selective.

pf.is_active() reports whether a usable filter was built.

When it helps

The win scales inversely with match density: on sparse inputs the engine sees only a small fraction of the text (an ~13× end-to-end speedup on a 16 MiB sample after transcode optimizations), while match-saturated inputs gain little — a wasted scan, then roughly baseline. It degrades gracefully rather than ever going wrong.

See examples/bitap_prototype.rs for the standalone algorithm, a brute-force correctness verifier, and a throughput comparison.