use super::cursor::PostingCursor;
use super::{RecordId, Weight};
#[derive(Debug)]
pub struct Lane<C> {
pub query_weight: Weight,
pub cursor: C,
}
impl<C: PostingCursor> Lane<C> {
pub fn new(query_weight: Weight, cursor: C) -> Self {
Self {
query_weight,
cursor,
}
}
#[inline]
pub fn current_id(&self) -> Option<RecordId> {
self.cursor.peek().map(|p| p.id)
}
pub fn headroom(&self) -> f64 {
let q = self.query_weight as f64;
let best = if q >= 0.0 {
q * self.cursor.upper_bound() as f64
} else {
q * self.cursor.lower_bound() as f64
};
if best.is_nan() {
0.0
} else {
best.max(0.0)
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Skip {
Candidates,
Nothing,
}
#[derive(Debug)]
pub struct Frontier<C> {
lanes: Vec<Lane<C>>,
order: Vec<usize>,
}
impl<C: PostingCursor> Frontier<C> {
pub fn new(lanes: impl IntoIterator<Item = Lane<C>>) -> Self {
let lanes: Vec<Lane<C>> = lanes
.into_iter()
.filter(|l| l.query_weight != 0.0 && !l.cursor.is_exhausted())
.collect();
let order = Vec::with_capacity(lanes.len());
Self { lanes, order }
}
pub fn len(&self) -> usize {
self.lanes.len()
}
pub fn is_empty(&self) -> bool {
self.lanes.is_empty()
}
pub fn lanes(&self) -> &[Lane<C>] {
&self.lanes
}
pub fn retire_exhausted(&mut self) {
self.lanes.retain(|l| !l.cursor.is_exhausted());
}
pub fn min_id(&self) -> Option<RecordId> {
self.lanes.iter().filter_map(Lane::current_id).min()
}
pub fn max_last_id(&self) -> Option<RecordId> {
self.lanes.iter().filter_map(|l| l.cursor.last_id()).max()
}
pub fn best_possible(&self) -> f64 {
self.lanes.iter().map(Lane::headroom).sum()
}
pub fn skip_below(&mut self, threshold: f32) -> Skip {
self.retire_exhausted();
if self.lanes.is_empty() {
return Skip::Nothing;
}
self.order.clear();
self.order.extend(0..self.lanes.len());
let lanes = &self.lanes;
self.order
.sort_by_key(|&i| lanes[i].current_id().unwrap_or(RecordId::MAX));
let mut acc = 0.0f64;
let mut pivot_pos = None;
for (pos, &i) in self.order.iter().enumerate() {
acc += self.lanes[i].headroom();
if can_beat(acc, threshold) {
pivot_pos = Some(pos);
break;
}
}
let Some(pivot_pos) = pivot_pos else {
return Skip::Nothing;
};
let pivot_id = self.lanes[self.order[pivot_pos]]
.current_id()
.expect("pivot lane is not exhausted");
for &i in &self.order[..pivot_pos] {
self.lanes[i].cursor.seek(pivot_id);
}
self.retire_exhausted();
Skip::Candidates
}
pub fn score_window(&mut self, lo: RecordId, hi: RecordId, scores: &mut [f32], seen: &mut [bool]) {
debug_assert!(lo <= hi);
for lane in &mut self.lanes {
let q = lane.query_weight;
lane.cursor.drain_through(hi, |id, w| {
debug_assert!(id >= lo, "cursor positioned before the window");
let slot = (id - lo) as usize;
scores[slot] += q * w;
seen[slot] = true;
});
}
}
}
pub(crate) fn can_beat(bound: f64, threshold: f32) -> bool {
if bound.is_infinite() {
return bound > 0.0;
}
let t = threshold as f64;
let magnitude = bound.abs().max(t.abs());
let slack = magnitude * (f32::EPSILON as f64 * 8.0);
bound + slack > t
}