use crate::fts::bm25::DocLen;
use crate::fts::bm25::Tf;
use crate::fts::search::{GlobalDocId, SearchCounters};
use crate::meta::DocBitmap;
use super::{TermCursor, TopK};
fn length_of(lengths: &[u32], row: u32) -> u32 {
usize::try_from(row)
.ok()
.and_then(|slot| lengths.get(slot).copied())
.unwrap_or(1)
}
pub fn score_all(
cursors: &mut [TermCursor<'_>],
lengths: &[u32],
segment: u32,
heap: &mut TopK,
counters: &mut SearchCounters,
allow_list: Option<&DocBitmap>,
) {
match score_all_controlled(
cursors,
lengths,
segment,
heap,
counters,
allow_list,
|| Ok::<(), std::convert::Infallible>(()),
) {
Ok(()) => {}
Err(never) => match never {},
}
}
pub(crate) fn score_all_controlled<E>(
cursors: &mut [TermCursor<'_>],
lengths: &[u32],
segment: u32,
heap: &mut TopK,
counters: &mut SearchCounters,
allow_list: Option<&DocBitmap>,
check: impl FnMut() -> Result<(), E>,
) -> Result<(), E> {
let mut work = crate::fts::control::WorkCheck::new(check);
work.check_now()?;
let mut totals: Vec<(u32, f64)> = Vec::new();
for cursor in cursors.iter_mut() {
work.step()?;
cursor.stream.reset_controlled(&mut work)?;
while let Some(row) = cursor.stream.current_row() {
work.step()?;
counters.postings_decoded = counters.postings_decoded.saturating_add(1);
let tf = cursor.stream.current_tf_controlled(&mut work)?.unwrap_or(0);
let score = cursor.scorer.score(Tf(tf), DocLen(length_of(lengths, row)));
match totals.binary_search_by_key(&row, |(candidate, _)| *candidate) {
Ok(slot) => {
if let Some((_, total)) = totals.get_mut(slot) {
*total += score;
}
}
Err(slot) => totals.insert(slot, (row, score)),
}
cursor.stream.advance_controlled(&mut work)?;
}
}
for (row, score) in totals {
work.step()?;
counters.docs_evaluated = counters.docs_evaluated.saturating_add(1);
if allow_list.is_none_or(|allowed| allowed.contains(row)) {
heap.offer(GlobalDocId { segment, row }, score);
}
}
work.check_now()
}
pub fn run(
cursors: &mut [TermCursor<'_>],
lengths: &[u32],
segment: u32,
heap: &mut TopK,
counters: &mut SearchCounters,
allow_list: Option<&DocBitmap>,
) {
match run_controlled(
cursors,
lengths,
segment,
heap,
counters,
allow_list,
|| Ok::<(), std::convert::Infallible>(()),
) {
Ok(()) => {}
Err(never) => match never {},
}
}
pub(crate) fn run_controlled<E>(
cursors: &mut [TermCursor<'_>],
lengths: &[u32],
segment: u32,
heap: &mut TopK,
counters: &mut SearchCounters,
allow_list: Option<&DocBitmap>,
check: impl FnMut() -> Result<(), E>,
) -> Result<(), E> {
let mut work = crate::fts::control::WorkCheck::new(check);
work.check_now()?;
let mut order: Vec<usize> = (0..cursors.len()).collect();
order.sort_by(|left, right| {
let a = cursors.get(*left).map_or(0.0, |c| c.upper_bound);
let b = cursors.get(*right).map_or(0.0, |c| c.upper_bound);
a.partial_cmp(&b).unwrap_or(std::cmp::Ordering::Equal)
});
for cursor in cursors.iter_mut() {
work.step()?;
cursor.stream.reset_controlled(&mut work)?;
}
let mut contributions: Vec<f64> = vec![0.0; cursors.len()];
let total_bound: f64 = order
.iter()
.filter_map(|slot| cursors.get(*slot))
.map(|cursor| cursor.upper_bound)
.sum();
loop {
work.step()?;
let threshold = heap.threshold();
let mut essential_from = 0_usize;
let mut accumulated = 0.0_f64;
while essential_from < order.len() {
work.step()?;
let bound = order
.get(essential_from)
.and_then(|slot| cursors.get(*slot))
.map_or(0.0, |cursor| cursor.upper_bound);
if heap.is_full() && accumulated + bound <= threshold {
accumulated += bound;
essential_from += 1;
} else {
break;
}
}
let essential = order.get(essential_from..).unwrap_or(&[]);
if essential.is_empty() {
break;
}
let mut candidate: Option<u32> = None;
for slot in essential {
work.step()?;
let Some(cursor) = cursors.get(*slot) else {
continue;
};
if let Some(row) = cursor.current() {
candidate = Some(candidate.map_or(row, |best: u32| best.min(row)));
}
}
let Some(row) = candidate else {
break;
};
contributions.fill(0.0);
let mut running = 0.0_f64;
let mut remaining_bound = total_bound;
let mut abandoned = false;
for slot in order.iter().rev() {
work.step()?;
let Some(cursor) = cursors.get_mut(*slot) else {
continue;
};
if heap.is_full() && cursor.current() != Some(row) {
let ceiling = cursor
.stream
.bound_for_controlled(row, &cursor.scorer, &mut work)?;
if running + ceiling + (remaining_bound - cursor.upper_bound) < threshold {
abandoned = true;
break;
}
}
cursor.stream.seek_controlled(row, &mut work)?;
let contribution = if cursor.current() == Some(row) {
counters.postings_decoded = counters.postings_decoded.saturating_add(1);
cursor
.stream
.current_tf_controlled(&mut work)?
.map_or(0.0, |tf| {
cursor.scorer.score(Tf(tf), DocLen(length_of(lengths, row)))
})
} else {
0.0
};
if let Some(entry) = contributions.get_mut(*slot) {
*entry = contribution;
}
running += contribution;
remaining_bound -= cursor.upper_bound;
if running + remaining_bound < threshold && heap.is_full() {
abandoned = true;
break;
}
}
if !abandoned {
let total: f64 = contributions.iter().sum();
counters.docs_evaluated = counters.docs_evaluated.saturating_add(1);
if allow_list.is_none_or(|allowed| allowed.contains(row)) {
heap.offer(GlobalDocId { segment, row }, total);
}
}
for cursor in cursors.iter_mut() {
work.step()?;
if cursor.current() == Some(row) {
cursor.stream.advance_controlled(&mut work)?;
}
}
if cursors.iter().all(TermCursor::exhausted) {
break;
}
}
work.check_now()
}