the engine
How the engine works
Most filter engines answer your question by checking every record. Filter EngineZ answers it by doing as little as possible — stacking four classic data-structure tricks so the same results come back in a fraction of the work.
01 · intersection
Roaring bitmaps
Every active rule becomes one compressed Roaring bitmap — a bit per item, set where the item matches. Combining filters is then a single bitwise AND across those bitmaps rather than a walk over the dataset. Roaring stores long runs as compact containers, so the bitmaps stay small and the AND stays sub-millisecond even as the corpus grows into the millions. This is the floor the rest of the stack builds on: once intersection is a bitmap operation, everything downstream — simulating a filter, checking whether two rules can coexist — becomes cheap.
createFilterEngine({ acceleration: { bitmap: 'on' } })
// one compressed bitmap per active rule; the result
// set is a single bitwise AND across them
02 · short-circuit
Mutual-exclusion graph
Some filter combinations can only ever return nothing — a red wine that is also a white, a sub-$15 bottle that is also a reserve. The engine precomputes which pairs of rule-options share zero items and stores that as a graph. Before it does any real work, it consults the graph: if selecting an option would empty the result set, that option is disabled without a bitmap pass or a scan. It is the cheapest layer because its best case is doing nothing at all — the shortest computation is the one you prove you can skip.
createFilterEngine({ acceleration: { exclusionGraph: 'lazy' } })
// precomputed zero-overlap pairs let the engine skip
// combinations that can only return nothing
03 · memoization
Memoized simulation
A responsive filter panel asks a lot of what-if questions: for every option in every facet, "if I added this, how many results would remain?" Answering each one naively is a full pass. The engine routes those through simulate(), memoizing every answer in an LRU cache keyed by the hypothetical rule plus the current state. Ask the same what-if twice — which a re-render does constantly — and the second answer is a map lookup. The cache invalidates precisely when the data or the active rules change, so it is always correct and almost always warm.
const { resultCount } = engine.simulate({
withRule: ['price', range('priceUsd', { min: 60 })],
})
// identical what-ifs return from the LRU cache
See it projecting, on the books corpus →
04 · spatial
R-tree spatial index
Geographic filters — "listings within five miles" — are the one place a linear scan really hurts, because every candidate needs a haversine distance. The engine indexes coordinates in an R-tree: a hierarchy of bounding boxes. A radius query walks the tree and prunes whole branches whose box cannot possibly intersect the search circle, discarding most of the map before computing a single exact distance. Only the handful of points in surviving boxes get the haversine refinement. It is the same instinct as the exclusion graph — throw away what cannot match — applied to space instead of set logic.
createFilterEngine({ acceleration: { spatialIndex: 'auto' } })
// GeoProximityRule → R-tree bbox prune first,
// exact haversine only on the survivors
act two
Beyond filtering
The same bitmaps, signatures, and counts power more than intersection — suggestions, similarity, probabilistic membership, and text search.
05 · suggestions
Bayesian suggestions
Beyond filtering, the engine learns from recorded filter sessions which filter-options tend to be selected together and builds a Bayesian co-occurrence model. Ask it for suggestions given the current selection and it ranks the options most likely to come next — narrowing to Napa and Cabernet surfaces "90+ points" and "oak" ahead of the long tail. It is the same bitmap substrate reused for a different question: instead of "which items match?", it answers "which filter would a user reach for next?", in microseconds, from aggregate counts rather than a scan.
import { attachSuggestions } from '@filter-enginez/core/suggestions'
const picks = attachSuggestions(engine).suggestions.getSuggestions(activeRuleIds, 3)
// Bayesian co-occurrence ranks the filters most
// likely to pair with the current selection
See it suggesting, on the books corpus →
06 · similarity
MinHash + LSH similarity
Finding items similar to a given one is naively an all-pairs comparison. Instead the engine reduces each item to a compact MinHash signature — a fingerprint that preserves set similarity — and uses locality-sensitive hashing to bucket items whose signatures agree. "Find similar" becomes a lookup in the query item's bucket plus a cheap refinement, not a sweep over the corpus. The index builds lazily on the first call and invalidates in O(1) when the data version changes, so nearest-neighbors stays sub-millisecond even as the dataset grows.
import { attachSimilarity } from '@filter-enginez/core/similarity'
const nearest = attachSimilarity(engine, config).findSimilar(wine)
// MinHash signatures + LSH banding turn "find similar"
// into a bucket lookup, not an all-pairs scan
See it finding neighbors, on the books corpus →
07 · membership
Bloom membership
A Bloom filter answers set membership with a tiny bit-array and a few hash functions: it can say “definitely not” or “probably yes”, never producing a false negative. The engine ships it as a serializable BloomMembershipRule — a cheap pre-screen that rejects the vast majority of non-members in a handful of bit checks before any exact comparison runs. Pair it with an exact check where a false positive would be incorrect; use it alone where an approximate membership gate is all you need. It is the cheapest “is this even a candidate?” the engine can ask.
const picks = new BloomFilter(/* built from editor's-pick ids */)
engine.addRule('picks', bloomMembership('id', picks))
// k bit probes: definitely-not, or probably-yes
08 · text
Inverted text index
Whole-word and phrase search over text would otherwise scan every string. The engine keeps an inverted index — each word points to the posting list of documents that contain it — so an allWords query becomes an intersection of posting lists, resolved straight from the index. Character-trigram postings extend the same idea to fuzzy search, pruning the candidate set before the exact match confirms. On 10k reviews it turns a multi-word search from a few milliseconds of substring scanning into hundredths of a millisecond — the inverted index composing cleanly with the same Roaring bitmaps the rest of the stack uses.
new TextSearchRule('notes', { query: 'buttery oak', mode: 'allWords' })
// createFilterEngine({ acceleration: { textIndex: 'auto' } })
// whole-word AND resolved from posting lists
now stack all of it
Try the optimizations live
This runs the real predictive-disable workload — simulating every option across a facet — on 10,000 wines, in your browser, right now. Flip the layers off and watch the same answer take longer to compute.
With all three layers on, the sweep takes 0.087 ms, against 28.9 ms for a naive linear scan — 334× faster. reference bench · 10k wines · 2026-07-08