Rust · Filtered Search

Filtered Search

use usearch::*; 3.0.0-rc.1 · 4c3e5023

Cross-referencing an external database during traversal beats paging through results and filtering afterwards. Non-matching entries still route the traversal, so recall does not collapse at low selectivity.

let mut arena = Arena::new_in(&executor)?;
let is_odd = |key: Key| u64::try_from(key).is_ok_and(|value| value % 2 == 1);
let matches = index.on(&executor).search(&rows(&query, 768)?, DenseSearchConfig {
    predicate: Some(&is_odd),
    ..DenseSearchConfig::from(10)
}, &mut arena)?;

A predicate is Fn and Sync, since the executor’s threads call it together, and a panic inside it rejects the key. A VRAM handle refuses a host predicate with Status::Device.

Every long verb’s config takes a progress handler, called with the units finished so far and the units the call finishes if it runs to the end. Answering ControlFlow::Break(()) stops the call at the next increment boundary.

use std::ops::ControlFlow;
use std::time::{Duration, Instant};

let deadline = Instant::now() + Duration::from_secs(30);
let mut before_the_deadline = move |_finished: usize, _total: usize| match Instant::now() < deadline {
    true => ControlFlow::Continue(()),
    false => ControlFlow::Break(()),
};
let refined = index.on(&executor).refine(DenseRefineConfig {
    expansion_factor: 128,
    progress: Some(&mut before_the_deadline),
    ..DenseRefineConfig::default()
});
if let Err(failure) = refined {
    assert_eq!(failure.status, Status::Cancelled, "only the deadline stops it");
}

The handler is FnMut and Send where a predicate is Fn and Sync: it arrives on whichever thread advanced the work and never on two at once, which is what lets it carry a counter or a deadline without a lock of its own. A panic inside it is read as a stop. Reports are denominated in whatever the verb consumes: entries for an insertion, queries for a search, re-selected nodes for a refine, an absorb or a split, draws for a sample, shards for a checkpoint, and bytes for a buffer. How often they arrive is the increment’s size rather than a guarantee, so read the counts and never the number of calls.

What a stop leaves behind is each verb’s own contract. An insert cannot be stopped: it claims every entry before its first report, ignores a Break, and publishes the whole batch at once. A stopped search reports Status::Cancelled and leaves no matches. A stopped refine is the time budget a whole-graph sweep is meant to run under: re-selection claims no identity, and the call still re-links its islands, so the graph stays correct and merely less improved, and it reports Status::Cancelled. A stopped load creates no index, and a stopped save_buffer leaves no monolith.

API

Types

ProgressCallbackA periodic report of finished and total units; Break stops the call at the next increment wherever the verb can stop, which an insertion cannot.
lib.rs:1536 ↗
pub type ProgressCallback<'a> = dyn FnMut(usize, usize) -> ControlFlow<()> + Send + 'a;

It runs on whichever thread advanced the work, never on two at once, so it owes Send and may be FnMut. A panic inside it stops the call like a Break and is re-raised once the call is final.

PredicateA search filter, run on the executor's threads together; a panic rejects the key and is re-raised once the call is final.
lib.rs:1573 ↗
pub type Predicate<'a> = dyn Fn(Key) -> bool + Sync + 'a;