use crate::rowid::RowId;
use crate::Result;
use std::collections::HashMap;
use std::sync::Arc;
type Postings = HashMap<u32, Vec<(RowId, f32)>>;
#[derive(Clone)]
pub struct SparseIndex {
frozen: Arc<Vec<Arc<Postings>>>,
active: Postings,
}
impl SparseIndex {
pub fn new() -> Self {
Self {
frozen: Arc::new(Vec::new()),
active: HashMap::new(),
}
}
pub fn insert(&mut self, terms: &[(u32, f32)], row_id: RowId) {
for &(token, weight) in terms {
self.active.entry(token).or_default().push((row_id, weight));
}
}
pub fn search(&self, query: &[(u32, f32)], k: usize) -> Vec<(RowId, f64)> {
self.search_filtered(query, k, |_| true)
}
pub fn search_filtered(
&self,
query: &[(u32, f32)],
k: usize,
allowed: impl Fn(RowId) -> bool,
) -> Vec<(RowId, f64)> {
let mut scores: HashMap<u64, f64> = HashMap::new();
for &(token, q_weight) in query {
for postings in self.layers() {
if let Some(list) = postings.get(&token) {
for &(rid, d_weight) in list {
if allowed(rid) {
*scores.entry(rid.0).or_insert(0.0) +=
f64::from(q_weight) * f64::from(d_weight);
}
}
}
}
}
let mut ranked: Vec<(RowId, f64)> = scores
.into_iter()
.map(|(rid, score)| (RowId(rid), score))
.collect();
ranked.sort_by(|a, b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
ranked.truncate(k);
ranked
}
pub fn search_with_context(
&self,
query: &[(u32, f32)],
k: usize,
context: Option<&crate::query::AiExecutionContext>,
) -> Result<Vec<(RowId, f64)>> {
let mut scores: HashMap<u64, f64> = HashMap::new();
for &(token, q_weight) in query {
if let Some(context) = context {
context.checkpoint()?;
}
for postings in self.layers() {
if let Some(list) = postings.get(&token) {
for chunk in list.chunks(256) {
if let Some(context) = context {
context.consume(chunk.len())?;
}
for &(rid, d_weight) in chunk {
if !scores.contains_key(&rid.0)
&& scores.len() >= crate::query::MAX_RAW_INDEX_CANDIDATES
{
return Err(crate::MongrelError::WorkBudgetExceeded);
}
*scores.entry(rid.0).or_insert(0.0) +=
f64::from(q_weight) * f64::from(d_weight);
}
}
}
}
}
let mut ranked: Vec<_> = scores
.into_iter()
.map(|(rid, score)| (RowId(rid), score))
.collect();
if let Some(context) = context {
context.consume(ranked.len())?;
}
let order = |left: &(RowId, f64), right: &(RowId, f64)| {
right
.1
.total_cmp(&left.1)
.then_with(|| left.0.cmp(&right.0))
};
if ranked.len() > k {
ranked.select_nth_unstable_by(k, order);
ranked.truncate(k);
}
ranked.sort_by(order);
Ok(ranked)
}
pub fn candidate_row_ids(&self, query: &[(u32, f32)]) -> Vec<RowId> {
let mut row_ids = std::collections::HashSet::new();
for (token, _) in query {
for postings in self.layers() {
if let Some(list) = postings.get(token) {
row_ids.extend(list.iter().map(|(row_id, _)| *row_id));
}
}
}
row_ids.into_iter().collect()
}
pub fn is_empty(&self) -> bool {
self.active.is_empty() && self.frozen.is_empty()
}
pub fn entries(&self) -> Vec<(u32, Vec<(RowId, f32)>)> {
let mut entries = HashMap::<u32, Vec<(RowId, f32)>>::new();
for postings in self.layers() {
for (token, list) in postings {
entries.entry(*token).or_default().extend(list);
}
}
entries.into_iter().collect()
}
pub fn from_entries(entries: Vec<(u32, Vec<(RowId, f32)>)>) -> Self {
let mut active = HashMap::new();
for (t, list) in entries {
active.insert(t, list);
}
Self {
frozen: Arc::new(Vec::new()),
active,
}
}
fn layers(&self) -> impl Iterator<Item = &Postings> {
self.frozen
.iter()
.map(Arc::as_ref)
.chain(std::iter::once(&self.active))
}
pub(crate) fn seal(&mut self) {
if self.active.is_empty() {
return;
}
Arc::make_mut(&mut self.frozen).push(Arc::new(std::mem::take(&mut self.active)));
if self.frozen.len() >= crate::MAX_READ_GENERATION_LAYERS {
self.consolidate();
}
}
fn consolidate(&mut self) {
let entries = self.entries();
let mut postings = HashMap::new();
for (token, list) in entries {
postings.insert(token, list);
}
self.frozen = Arc::new(vec![Arc::new(postings)]);
}
#[cfg(test)]
pub(crate) fn frozen_layer_count(&self) -> usize {
self.frozen.len()
}
}
impl Default for SparseIndex {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ranks_by_sparse_overlap() {
let mut idx = SparseIndex::new();
idx.insert(&[(1, 2.0), (2, 1.0)], RowId(0));
idx.insert(&[(1, 1.0), (3, 3.0)], RowId(1));
idx.insert(&[(2, 5.0)], RowId(2));
let top = idx.search(&[(1, 1.0), (2, 1.0)], 3);
assert_eq!(top[0], (RowId(2), 5.0));
assert_eq!(top[1], (RowId(0), 3.0));
assert_eq!(top[2], (RowId(1), 1.0));
}
#[test]
fn unique_candidates_stop_at_raw_ceiling() {
let mut idx = SparseIndex::new();
for row_id in 0..=crate::query::MAX_RAW_INDEX_CANDIDATES {
idx.insert(&[(1, 1.0)], RowId(row_id as u64));
}
let context = crate::query::AiExecutionContext::new(None, usize::MAX);
assert!(matches!(
idx.search_with_context(&[(1, 1.0)], 1, Some(&context)),
Err(crate::MongrelError::WorkBudgetExceeded)
));
}
#[test]
fn sealed_generations_merge_postings_and_consolidate() {
let mut writer = SparseIndex::new();
for id in 0..crate::MAX_READ_GENERATION_LAYERS as u64 + 2 {
writer.insert(&[(1, 1.0)], RowId(id));
writer.seal();
}
assert!(writer.frozen_layer_count() < crate::MAX_READ_GENERATION_LAYERS);
let generation = writer.clone();
writer.insert(&[(1, 10.0)], RowId(99));
assert!(!generation
.candidate_row_ids(&[(1, 1.0)])
.contains(&RowId(99)));
assert!(writer.candidate_row_ids(&[(1, 1.0)]).contains(&RowId(99)));
}
}