#![allow(clippy::cast_precision_loss)]
mod scoring;
mod strategy;
#[cfg(test)]
mod bmw_parity_tests;
use super::inverted_index::SparseInvertedIndex;
use super::types::{ScoredDoc, SparseVector};
use strategy::{linear_scan_search, maxscore_search};
const FULL_SCAN_THRESHOLD: f32 = 0.3;
const SMALL_CORPUS_LINEAR_THRESHOLD: u64 = 100_000;
const MAX_DENSE_ACCUMULATOR: u64 = 1_000_000;
#[must_use]
pub fn sparse_search(
index: &SparseInvertedIndex,
query: &SparseVector,
k: usize,
) -> Vec<ScoredDoc> {
if k == 0 || query.is_empty() || index.doc_count() == 0 {
return Vec::new();
}
let doc_count = index.doc_count();
let has_negative_weight = query.values.iter().any(|&w| w < 0.0);
if doc_count <= SMALL_CORPUS_LINEAR_THRESHOLD || has_negative_weight {
return linear_scan_search(index, query, k);
}
let mut total_postings: usize = 0;
for &term_id in &query.indices {
total_postings += index.posting_count(term_id);
}
let coverage_threshold = FULL_SCAN_THRESHOLD * doc_count as f32 * query.nnz() as f32;
if (total_postings as f32) > coverage_threshold {
linear_scan_search(index, query, k)
} else {
maxscore_search(index, query, k)
}
}
#[must_use]
pub fn sparse_search_filtered(
index: &SparseInvertedIndex,
query: &SparseVector,
k: usize,
filter: Option<&dyn Fn(u64) -> bool>,
) -> Vec<ScoredDoc> {
let Some(filter) = filter else {
return sparse_search(index, query, k);
};
let candidates = sparse_search(index, query, k.saturating_mul(4).max(k + 10));
let mut filtered: Vec<ScoredDoc> = candidates
.into_iter()
.filter(|doc| filter(doc.doc_id))
.collect();
if filtered.len() >= k {
filtered.truncate(k);
return filtered;
}
let candidates = sparse_search(index, query, k.saturating_mul(8).max(k + 20));
filtered = candidates
.into_iter()
.filter(|doc| filter(doc.doc_id))
.collect();
filtered.truncate(k);
filtered
}
#[cfg(test)]
pub(crate) fn brute_force_search(
index: &SparseInvertedIndex,
query: &SparseVector,
k: usize,
) -> Vec<ScoredDoc> {
use rustc_hash::FxHashMap;
if k == 0 || query.is_empty() || index.doc_count() == 0 {
return Vec::new();
}
let mut scores: FxHashMap<u64, f32> = FxHashMap::default();
for (&term_id, &qw) in query.indices.iter().zip(query.values.iter()) {
let postings = index.get_all_postings(term_id);
for entry in &postings {
*scores.entry(entry.doc_id).or_insert(0.0) += qw * entry.weight;
}
}
let mut all_docs: Vec<ScoredDoc> = scores
.into_iter()
.map(|(doc_id, score)| ScoredDoc { score, doc_id })
.collect();
all_docs.sort_unstable_by(|a, b| b.cmp(a)); all_docs.truncate(k);
all_docs
}
#[cfg(test)]
#[path = "search_tests.rs"]
mod tests;