use std::collections::HashMap;
use std::sync::Arc;
use lance_select::RowAddrMask;
use super::super::documents::AddressKeyedDocuments;
use super::super::index::{PostingList, live_posting_rows};
use super::super::scorer::CombinedFieldsBM25Scorer;
pub(super) struct CombinedTermPostings {
pub(super) idf: f32,
pub(super) postings: Vec<(u64, f32)>,
}
impl CombinedTermPostings {
#[inline]
pub(super) fn tf_prime(&self, row_id: u64) -> f32 {
match self.postings.binary_search_by_key(&row_id, |(id, _)| *id) {
Ok(idx) => self.postings[idx].1,
Err(_) => 0.0,
}
}
}
pub(super) struct LoadedSource {
pub(super) weight: f32,
pub(super) docs: AddressKeyedDocuments,
pub(super) is_legacy: bool,
pub(super) posting: PostingList,
}
pub(super) fn build_term_postings(
term: &str,
sources: Vec<LoadedSource>,
mask: &Arc<RowAddrMask>,
scorer: &CombinedFieldsBM25Scorer,
) -> CombinedTermPostings {
let mut acc: HashMap<u64, f32> = HashMap::new();
for source in &sources {
for (row_id, freq) in live_posting_rows(&source.posting, &source.docs, source.is_legacy) {
if !mask.selected(row_id) {
continue;
}
*acc.entry(row_id).or_insert(0.0) += source.weight * freq as f32;
}
}
let idf = scorer.query_weight(term);
let mut postings: Vec<(u64, f32)> = acc.into_iter().collect();
postings.sort_unstable_by_key(|(row_id, _)| *row_id);
CombinedTermPostings { idf, postings }
}
#[cfg(test)]
mod tests {
use super::super::testing::{compressed_list, modern_identity_docs};
use super::*;
use lance_core::utils::address::RowAddress;
use lance_select::RowAddrTreeMap;
#[tokio::test]
async fn test_build_term_postings_merges_legacy_and_compressed() {
use super::super::super::index::PlainPostingList;
use arrow::buffer::ScalarBuffer;
let scorer = CombinedFieldsBM25Scorer::new(1000, 12.0, HashMap::new());
let legacy = PostingList::Plain(PlainPostingList::new(
ScalarBuffer::from(vec![10u64, 20, 20, 30]),
ScalarBuffer::from(vec![1.0f32, 2.0, 3.0, 1.0]),
Some(0.0),
None,
));
let compressed = PostingList::Compressed(compressed_list(&[(20, 5), (42, 7)]));
let docs = modern_identity_docs(&vec![1u32; 64], &[]).await;
let sources = vec![
LoadedSource {
weight: 2.0,
docs: docs.clone(),
is_legacy: true,
posting: legacy,
},
LoadedSource {
weight: 1.0,
docs,
is_legacy: false,
posting: compressed,
},
];
let mask = Arc::new(RowAddrMask::all_rows().also_block(RowAddrTreeMap::from_iter([42u64])));
let term = build_term_postings("t", sources, &mask, &scorer);
assert_eq!(term.postings, vec![(10, 2.0), (20, 15.0), (30, 2.0)]);
}
#[tokio::test]
async fn test_build_term_postings_skips_tombstoned_addresses() {
const DEAD_ROW: u64 = 20;
let scorer = CombinedFieldsBM25Scorer::new(1000, 12.0, HashMap::new());
let docs = modern_identity_docs(&[4u32; 40], &[DEAD_ROW]).await;
assert_eq!(
docs.row_address(DEAD_ROW as u32),
RowAddress::TOMBSTONE_ROW,
"the deleted document must keep its slot as a tombstone"
);
assert_eq!(docs.doc_length_at(RowAddress::TOMBSTONE_ROW), 0);
let sources = vec![LoadedSource {
weight: 2.0,
docs,
is_legacy: false,
posting: PostingList::Compressed(compressed_list(&[
(10, 1),
(DEAD_ROW as u32, 7),
(30, 3),
])),
}];
let term = build_term_postings("t", sources, &Arc::new(RowAddrMask::default()), &scorer);
assert_eq!(
term.postings,
vec![(10, 2.0), (30, 6.0)],
"the tombstoned address must never be accumulated, and the live rows \
must keep their exact contributions"
);
}
}