use super::cursor::PostingCursor;
use super::frontier::{Frontier, Lane, Skip};
use super::sink::{ScoreSink, TopKSink};
use super::{DimId, RecordId, Weight};
pub const MAX_WINDOW: u64 = 1 << 22;
#[derive(Clone, Copy, Debug)]
pub struct SearchOptions {
pub pruning: bool,
pub window: u64,
}
impl Default for SearchOptions {
fn default() -> Self {
Self {
pruning: true,
window: 4096,
}
}
}
impl SearchOptions {
pub fn exhaustive() -> Self {
Self {
pruning: false,
..Self::default()
}
}
}
#[derive(Debug, Default)]
pub struct Scratch {
scores: Vec<f32>,
seen: Vec<bool>,
lanes: Vec<(DimId, Weight)>,
}
impl Scratch {
pub fn new() -> Self {
Self::default()
}
fn prepare(&mut self, len: usize) {
self.scores.clear();
self.scores.resize(len, 0.0);
self.seen.clear();
self.seen.resize(len, false);
}
fn merge_query(&mut self, query: &[(DimId, Weight)]) -> &[(DimId, Weight)] {
self.lanes.clear();
self.lanes.extend_from_slice(query);
self.lanes.sort_by_key(|&(dim, _)| dim);
self.lanes.dedup_by(|later, earlier| {
if later.0 == earlier.0 {
earlier.1 += later.1;
true
} else {
false
}
});
self.lanes.retain(|&(_, w)| w != 0.0);
&self.lanes
}
}
pub fn search<C, F, R>(
query: &[(DimId, Weight)],
top_k: usize,
filter: F,
cursors: R,
) -> Vec<(RecordId, f32)>
where
C: PostingCursor,
F: Fn(RecordId) -> bool,
R: FnMut(DimId) -> Option<C>,
{
let mut scratch = Scratch::new();
search_with(
query,
filter,
cursors,
TopKSink::new(top_k),
SearchOptions::default(),
&mut scratch,
)
}
pub fn search_ids<C, R, S>(
query: &[(DimId, Weight)],
ids: &[RecordId],
mut cursors: R,
mut sink: S,
scratch: &mut Scratch,
) -> Vec<(RecordId, f32)>
where
C: PostingCursor,
R: FnMut(DimId) -> Option<C>,
S: ScoreSink,
{
debug_assert!(ids.windows(2).all(|w| w[0] < w[1]), "ids must be sorted and unique");
let mut lanes: Vec<(Weight, C)> = scratch
.merge_query(query)
.iter()
.filter_map(|&(dim, w)| cursors(dim).map(|c| (w, c)))
.collect();
for &id in ids {
let mut score = 0.0f32;
let mut seen = false;
for (w, cursor) in lanes.iter_mut() {
if let Some(p) = cursor.seek(id) {
if p.id == id {
score += *w * p.weight;
seen = true;
}
}
}
if seen {
sink.offer(id, score);
}
}
sink.into_results()
}
pub fn search_with<C, F, R, S>(
query: &[(DimId, Weight)],
filter: F,
mut cursors: R,
mut sink: S,
options: SearchOptions,
scratch: &mut Scratch,
) -> Vec<(RecordId, f32)>
where
C: PostingCursor,
F: Fn(RecordId) -> bool,
R: FnMut(DimId) -> Option<C>,
S: ScoreSink,
{
let lanes = scratch
.merge_query(query)
.iter()
.filter_map(|&(dim, w)| cursors(dim).map(|c| Lane::new(w, c)));
let mut frontier = Frontier::new(lanes);
let window = options.window.clamp(1, MAX_WINDOW);
let mut pruned_at: Option<f32> = None;
loop {
frontier.retire_exhausted();
if frontier.is_empty() {
break;
}
if options.pruning {
if let Some(threshold) = sink.threshold() {
if pruned_at != Some(threshold) {
pruned_at = Some(threshold);
if frontier.skip_below(threshold) == Skip::Nothing {
break;
}
}
}
}
let Some(lo) = frontier.min_id() else {
break;
};
let last = frontier.max_last_id().unwrap_or(lo).max(lo);
let hi = lo.saturating_add(window - 1).min(last);
let len = (hi - lo) as usize + 1;
scratch.prepare(len);
frontier.score_window(lo, hi, &mut scratch.scores, &mut scratch.seen);
let mut floor = sink.threshold().unwrap_or(f32::NEG_INFINITY);
for slot in 0..len {
if !scratch.seen[slot] {
continue;
}
let score = scratch.scores[slot];
if score <= floor {
continue;
}
let id = lo + slot as RecordId;
if filter(id) {
sink.offer(id, score);
floor = sink.threshold().unwrap_or(f32::NEG_INFINITY);
}
}
}
sink.into_results()
}