- Canonical evaluator — one implementation of
Filtersemantics, shared by every metadata-aware index so query results never diverge - Validate once, evaluate many —
FilterEvaluator::newchecks the filter (depth,Incardinality) a single time;evaluateis then infallible and runs per-row inside the search loop - Closed-world semantics — a leaf over an absent field is
false, type mismatches arefalse,NaNorderings arefalse, andNotof afalseleaf istrue(the "records without this field" idiom) - DoS-hardened — iterative validation that can't stack-overflow, with bounded depth and
Inwidth; the library never panics on adversarial input - Scan helpers —
prefilter/postfilterapply the evaluator as lazy, allocation-free iterator adapters over a stream of(key, metadata)pairs - Strategy selection — a structural selectivity estimate drives an automatic
PreFilter/PostFilterchoice, with a tunable threshold - First-party only — depends solely on
iqdb-types, so it is unblocked today
Installation
[]
= "0.3"
Quick start
Build an evaluator once, then test it against each record's metadata:
use FilterEvaluator;
use ;
// published == true AND year > 2000
let filter = and;
let evaluator = new.expect;
let meta: Metadata =
.into_iter
.collect;
assert!;
assert!; // no metadata -> every leaf is false
The Not / absent-field idiom selects records that lack a field, or carry it with a non-matching value:
use FilterEvaluator;
use ;
// "records that are not authored by ada" — including records with no author.
let evaluator =
new
.expect;
assert!;
Validation rejects pathological filters up front — bounded by the public caps:
use ;
use ;
// An `In` set wider than the cap is refused before it can slow a query.
let huge = vec!;
let err = new.unwrap_err;
assert_eq!;
Apply a strategy with the scan helpers, or let the selectivity estimate pick one:
use ;
use ;
let evaluator = new
.expect;
// `prefilter` keeps the keys of matching candidates, lazily, before scoring.
let rust: Metadata =
.into_iter
.collect;
let go: Metadata =
.into_iter
.collect;
let rows = ;
let kept: = evaluator.prefilter.collect;
assert_eq!;
// An equality predicate is narrow, so the selector recommends pre-filtering.
assert_eq!;
Errors
FilterEvaluator::new returns iqdb_types::Result; the only failure is
IqdbError::InvalidFilter, returned when a filter exceeds MAX_FILTER_DEPTH
nesting or carries an In node wider than MAX_IN_VALUES. After a filter is
validated, evaluate is infallible and never panics — including on records
with no metadata, type mismatches, and NaN values.
Status
v0.3.0 — the evaluator plus strategy selection. On top of the
canonical FilterEvaluator (validate-on-construction, infallible allocation-free
per-row evaluate), this release adds the prefilter / postfilter scan
helpers, a structural estimate_selectivity, and a tunable selector
(choose_strategy / StrategySelector) that resolves PreFilter vs
PostFilter. Semantics and the new surface are pinned by unit, integration, and
property tests, verified across the CI matrix (Linux, macOS, Windows) on stable
and the 1.87 MSRV. The optional inverted MetadataIndex, index-backed
selectivity, and InFilter pushdown remain deferred until the first
approximate-index consumer lands — see the ROADMAP
for the rationale. The full surface is documented in docs/API.md.
Where It Fits
iqdb-filter sits just above the types crate and is consumed by the index layer:
iqdb-types— theFilter,Metadata, andValuetypes it evaluatesiqdb-flat/iqdb-hnsw/iqdb-ivf— delegate here for metadata filteringiqdb— exposes filtered search to users
Its only first-party dependency is iqdb-types, so it is unblocked today.
Standards
Built to the iQDB Rust standard. See REPS.md (Rust Efficiency & Performance Standards) and dev/DIRECTIVES.md for the engineering law and the definition of done. Before a PR: cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean.