use std::sync::Arc;
use arrow::array::AsArray;
use arrow::datatypes::{UInt32Type, UInt64Type};
use arrow_array::{Array, UInt32Array, UInt64Array};
use lance_core::ROW_ID;
use lance_core::Result;
use lance_core::deepsize::DeepSizeOf;
use tokio::sync::OnceCell;
use crate::scalar::RowIdRemapper;
use crate::scalar::inverted::index::{DocSet, NUM_TOKEN_COL};
use crate::scalar::{IndexReader, IndexStore};
use lance_select::mask::RowAddrMask;
pub enum LazyDocSet {
Loaded(LoadedDocSet),
Deferred(Box<DeferredDocSet>),
}
pub struct LoadedDocSet {
docs: Arc<DocSet>,
num_rows: usize,
total_tokens: u64,
}
struct NumTokensSnapshot {
column: Arc<UInt32Array>,
docs: Arc<DocSet>,
}
pub struct DeferredDocSet {
store: Arc<dyn IndexStore>,
docs_path: String,
is_legacy: bool,
frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
quantized_scoring: bool,
num_rows: usize,
num_tokens: OnceCell<NumTokensSnapshot>,
row_ids_col: OnceCell<Arc<UInt64Array>>,
full: OnceCell<Arc<DocSet>>,
}
impl std::fmt::Debug for LazyDocSet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Loaded(l) => f
.debug_struct("LazyDocSet::Loaded")
.field("num_rows", &l.num_rows)
.field("total_tokens", &l.total_tokens)
.finish(),
Self::Deferred(d) => f
.debug_struct("LazyDocSet::Deferred")
.field("num_rows", &d.num_rows)
.field(
"total_tokens_loaded",
&(d.num_tokens.initialized() || d.full.initialized()),
)
.field("num_tokens_loaded", &d.num_tokens.initialized())
.field("full_loaded", &d.full.initialized())
.finish(),
}
}
}
impl DeepSizeOf for LazyDocSet {
fn deep_size_of_children(&self, ctx: &mut lance_core::deepsize::Context) -> usize {
match self {
Self::Loaded(l) => l.docs.deep_size_of_children(ctx),
Self::Deferred(d) => {
d.full
.get()
.map(|d| d.deep_size_of_children(ctx))
.unwrap_or(0)
+ d.num_tokens
.get()
.map(|snapshot| {
let arr: &dyn Array = snapshot.column.as_ref();
snapshot.docs.deep_size_of_children(ctx)
+ arr.deep_size_of_children(ctx)
})
.unwrap_or(0)
+ d.row_ids_col
.get()
.map(|arr| {
let arr: &dyn Array = arr.as_ref();
arr.deep_size_of_children(ctx)
})
.unwrap_or(0)
}
}
}
}
impl LazyDocSet {
#[allow(clippy::too_many_arguments)]
pub fn new(
store: Arc<dyn IndexStore>,
docs_path: String,
num_rows: usize,
is_legacy: bool,
frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
quantized_scoring: bool,
) -> Self {
Self::Deferred(Box::new(DeferredDocSet {
store,
docs_path,
is_legacy,
frag_reuse_index,
quantized_scoring,
num_rows,
num_tokens: OnceCell::new(),
row_ids_col: OnceCell::new(),
full: OnceCell::new(),
}))
}
pub fn from_loaded(docs: DocSet) -> Self {
let num_rows = docs.len();
let total_tokens = docs.total_tokens_num();
Self::Loaded(LoadedDocSet {
docs: Arc::new(docs),
num_rows,
total_tokens,
})
}
pub fn len(&self) -> usize {
match self {
Self::Loaded(l) => l.num_rows,
Self::Deferred(d) => d.num_rows,
}
}
pub fn total_tokens_cached(&self) -> Option<u64> {
match self {
Self::Loaded(l) => Some(l.total_tokens),
Self::Deferred(d) => d
.full
.get()
.map(|docs| docs.total_tokens_num())
.or_else(|| {
d.num_tokens
.get()
.map(|snapshot| snapshot.docs.total_tokens_num())
}),
}
}
pub fn has_frag_reuse_remap(&self) -> bool {
match self {
Self::Loaded(_) => false,
Self::Deferred(d) => d.frag_reuse_index.is_some(),
}
}
pub async fn total_tokens_num(&self) -> Result<u64> {
match self {
Self::Loaded(l) => Ok(l.total_tokens),
Self::Deferred(d) => d.total_tokens_num().await,
}
}
pub async fn ensure_loaded(&self) -> Result<Arc<DocSet>> {
match self {
Self::Loaded(l) => Ok(l.docs.clone()),
Self::Deferred(d) => d.ensure_loaded().await,
}
}
pub async fn ensure_num_tokens_loaded(&self) -> Result<Arc<DocSet>> {
match self {
Self::Loaded(l) => Ok(l.docs.clone()),
Self::Deferred(d) => d.ensure_num_tokens_loaded().await,
}
}
pub async fn docs_for_wand(&self, mask: &RowAddrMask) -> Result<Arc<DocSet>> {
if mask.is_select_all() && !self.has_frag_reuse_remap() {
self.ensure_num_tokens_loaded().await
} else {
self.ensure_loaded().await
}
}
pub async fn resolve_row_ids(&self, doc_ids: &[u32]) -> Result<Vec<u64>> {
match self {
Self::Loaded(l) => Ok(doc_ids.iter().map(|&d| l.docs.row_id(d)).collect()),
Self::Deferred(d) => d.resolve_row_ids(doc_ids).await,
}
}
}
impl DeferredDocSet {
async fn reader(&self) -> Result<Arc<dyn IndexReader>> {
self.store.open_index_file(&self.docs_path).await
}
async fn total_tokens_num(&self) -> Result<u64> {
if let Some(full) = self.full.get() {
return Ok(full.total_tokens_num());
}
Ok(self.num_tokens_snapshot().await?.docs.total_tokens_num())
}
async fn num_tokens_snapshot(&self) -> Result<&NumTokensSnapshot> {
self.num_tokens
.get_or_try_init(|| async {
let reader = self.reader().await?;
let batch = reader
.read_range(0..self.num_rows, Some(&[NUM_TOKEN_COL]))
.await?;
let column = Arc::new(batch[NUM_TOKEN_COL].as_primitive::<UInt32Type>().clone());
let total_tokens = column.values().iter().map(|&n| n as u64).sum();
let mut docs = DocSet::from_cached_num_tokens(column.as_ref(), total_tokens);
docs.set_quantized_scoring(self.quantized_scoring);
Result::Ok(NumTokensSnapshot {
column,
docs: Arc::new(docs),
})
})
.await
}
async fn row_ids_column(&self) -> Result<Arc<UInt64Array>> {
self.row_ids_col
.get_or_try_init(|| async {
let reader = self.reader().await?;
let batch = reader.read_range(0..self.num_rows, Some(&[ROW_ID])).await?;
Result::Ok(Arc::new(batch[ROW_ID].as_primitive::<UInt64Type>().clone()))
})
.await
.cloned()
}
async fn ensure_loaded(&self) -> Result<Arc<DocSet>> {
let docs = self
.full
.get_or_try_init(|| async {
let mut docs = if let Some(num_tokens) = self.num_tokens.get() {
let row_ids = self.row_ids_column().await?;
DocSet::from_columns(
row_ids.as_ref(),
num_tokens.column.as_ref(),
self.is_legacy,
self.frag_reuse_index.clone(),
)?
} else {
DocSet::load(
self.reader().await?,
self.is_legacy,
self.frag_reuse_index.clone(),
)
.await?
};
docs.set_quantized_scoring(self.quantized_scoring);
Result::Ok(Arc::new(docs))
})
.await?
.clone();
Ok(docs)
}
async fn ensure_num_tokens_loaded(&self) -> Result<Arc<DocSet>> {
if let Some(full) = self.full.get() {
return Ok(full.clone());
}
Ok(self.num_tokens_snapshot().await?.docs.clone())
}
async fn resolve_row_ids(&self, doc_ids: &[u32]) -> Result<Vec<u64>> {
if let Some(full) = self.full.get()
&& full.has_row_ids()
{
return Ok(doc_ids.iter().map(|&d| full.row_id(d)).collect());
}
if let Some(arr) = self.row_ids_col.get() {
return Ok(doc_ids.iter().map(|&d| arr.value(d as usize)).collect());
}
let ranges: Vec<std::ops::Range<usize>> = doc_ids
.iter()
.map(|&d| d as usize..d as usize + 1)
.collect();
let reader = self.reader().await?;
let batch = reader.read_ranges(&ranges, Some(&[ROW_ID])).await?;
let arr = batch[ROW_ID].as_primitive::<UInt64Type>();
Ok((0..arr.len()).map(|i| arr.value(i)).collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scalar::lance_format::LanceIndexStore;
use lance_core::cache::LanceCache;
use lance_core::utils::tempfile::TempObjDir;
use lance_io::object_store::ObjectStore;
#[tokio::test]
async fn test_full_docset_is_a_complete_cached_snapshot() {
let temp_dir = TempObjDir::default();
let store = Arc::new(LanceIndexStore::new(
ObjectStore::local().into(),
temp_dir.clone(),
Arc::new(LanceCache::no_cache()),
));
let docs = LazyDocSet::new(store, "unused".to_owned(), 3, false, None, false);
assert_eq!(docs.total_tokens_cached(), None);
let row_ids = UInt64Array::from(vec![10, 20, 30]);
let num_tokens = UInt32Array::from(vec![3, 5, 8]);
let full = Arc::new(DocSet::from_columns(&row_ids, &num_tokens, false, None).unwrap());
let LazyDocSet::Deferred(deferred) = &docs else {
panic!("expected a deferred DocSet");
};
deferred.full.set(full.clone()).unwrap();
let wand_docs = docs.ensure_num_tokens_loaded().await.unwrap();
assert!(Arc::ptr_eq(&wand_docs, &full));
assert_eq!(wand_docs.total_tokens_num(), 16);
assert_eq!(docs.total_tokens_cached(), Some(16));
}
}