use super::*;
#[derive(Debug, Clone, DeepSizeOf)]
#[allow(clippy::large_enum_variant)]
pub enum PostingList {
Plain(PlainPostingList),
Compressed(CompressedPostingList),
}
impl PostingList {
pub fn from_batch(
batch: &RecordBatch,
max_score: Option<f32>,
length: Option<u32>,
) -> Result<Self> {
let posting_tail_codec = parse_posting_tail_codec(batch.schema_ref().metadata())?;
let block_size = parse_posting_block_size(batch.schema_ref().metadata())?;
Self::from_batch_with_tail_codec(batch, max_score, length, posting_tail_codec, block_size)
}
pub fn from_batch_with_tail_codec(
batch: &RecordBatch,
max_score: Option<f32>,
length: Option<u32>,
posting_tail_codec: PostingTailCodec,
block_size: usize,
) -> Result<Self> {
let positions_layout = if batch.column_by_name(COMPRESSED_POSITION_COL).is_some() {
PositionsLayout::SharedStream(parse_shared_position_codec(
batch.schema_ref().metadata(),
)?)
} else if batch.column_by_name(POSITION_COL).is_some() {
PositionsLayout::LegacyPerDoc
} else {
PositionsLayout::None
};
Self::from_batch_with_tail_codec_and_positions_layout(
batch,
max_score,
length,
posting_tail_codec,
block_size,
positions_layout,
)
}
pub(super) fn from_batch_with_tail_codec_and_positions_layout(
batch: &RecordBatch,
max_score: Option<f32>,
length: Option<u32>,
posting_tail_codec: PostingTailCodec,
block_size: usize,
positions_layout: PositionsLayout,
) -> Result<Self> {
match batch.column_by_name(POSTING_COL) {
Some(_) => {
debug_assert!(max_score.is_some() && length.is_some());
let shared_position_codec = match positions_layout {
PositionsLayout::SharedStream(codec) => Some(codec),
_ => None,
};
let posting = CompressedPostingList::from_batch(
batch,
max_score.unwrap(),
length.unwrap(),
posting_tail_codec,
block_size,
shared_position_codec,
)?;
Ok(Self::Compressed(posting))
}
None => {
let posting = PlainPostingList::from_batch(batch, max_score);
Ok(Self::Plain(posting))
}
}
}
pub fn iter(&self) -> PostingListIterator<'_> {
PostingListIterator::new(self)
}
pub fn has_position(&self) -> bool {
match self {
Self::Plain(posting) => posting.positions.is_some(),
Self::Compressed(posting) => posting.positions.is_some(),
}
}
pub fn has_impacts(&self) -> bool {
match self {
Self::Plain(_) => false,
Self::Compressed(posting) => posting.impacts.is_some(),
}
}
pub fn set_positions(&mut self, positions: CompressedPositionStorage) {
match self {
Self::Plain(posting) => match positions {
CompressedPositionStorage::LegacyPerDoc(positions) => {
posting.positions = Some(positions)
}
CompressedPositionStorage::SharedStream(_) => {
unreachable!("shared position stream is not supported for plain postings")
}
},
Self::Compressed(posting) => {
posting.positions = Some(positions);
}
}
}
pub fn take_positions(&mut self) -> Option<CompressedPositionStorage> {
match self {
Self::Plain(posting) => posting
.positions
.take()
.map(CompressedPositionStorage::LegacyPerDoc),
Self::Compressed(posting) => posting.positions.take(),
}
}
pub fn max_score(&self) -> Option<f32> {
match self {
Self::Plain(posting) => posting.max_score,
Self::Compressed(posting) => Some(posting.max_score),
}
}
pub fn len(&self) -> usize {
match self {
Self::Plain(posting) => posting.len(),
Self::Compressed(posting) => posting.length as usize,
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn into_builder(self, docs: &DocSet) -> PostingListBuilder {
let posting_tail_codec = match &self {
Self::Plain(_) => PostingTailCodec::Fixed32,
Self::Compressed(posting) => posting.posting_tail_codec,
};
let block_size = match &self {
Self::Plain(_) => LEGACY_BLOCK_SIZE,
Self::Compressed(posting) => posting.block_size,
};
let mut builder = PostingListBuilder::new_with_posting_tail_codec_and_block_size(
self.has_position(),
posting_tail_codec,
block_size,
);
match self {
Self::Plain(posting) => {
struct Item {
doc_id: u32,
positions: PositionRecorder,
}
let doc_ids = docs
.row_ids
.iter()
.enumerate()
.map(|(doc_id, row_id)| (*row_id, doc_id as u32))
.collect::<HashMap<_, _>>();
let mut items = Vec::with_capacity(posting.len());
for (row_id, freq, positions) in posting.iter() {
let freq = freq as u32;
let positions = match positions {
Some(positions) => {
PositionRecorder::Position(positions.collect::<Vec<_>>().into())
}
None => PositionRecorder::Count(freq),
};
items.push(Item {
doc_id: doc_ids[&row_id],
positions,
});
}
items.sort_unstable_by_key(|item| item.doc_id);
for item in items {
builder.add(item.doc_id, item.positions);
}
}
Self::Compressed(posting) => {
posting.iter().for_each(|(doc_id, freq, positions)| {
let positions = match positions {
Some(positions) => {
PositionRecorder::Position(positions.collect::<Vec<_>>().into())
}
None => PositionRecorder::Count(freq),
};
builder.add(doc_id, positions);
});
}
}
builder
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct PlainPostingList {
pub row_ids: ScalarBuffer<u64>,
pub frequencies: ScalarBuffer<f32>,
pub max_score: Option<f32>,
pub positions: Option<ListArray>, }
impl DeepSizeOf for PlainPostingList {
fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
self.row_ids.len() * std::mem::size_of::<u64>()
+ self.frequencies.len() * std::mem::size_of::<f32>()
+ self
.positions
.as_ref()
.map(|positions| sliced_cache_bytes(positions))
.unwrap_or(0)
}
}
impl PlainPostingList {
pub fn new(
row_ids: ScalarBuffer<u64>,
frequencies: ScalarBuffer<f32>,
max_score: Option<f32>,
positions: Option<ListArray>,
) -> Self {
Self {
row_ids,
frequencies,
max_score,
positions,
}
}
pub fn from_batch(batch: &RecordBatch, max_score: Option<f32>) -> Self {
let row_ids = batch[ROW_ID].as_primitive::<UInt64Type>().values().clone();
let frequencies = batch[FREQUENCY_COL]
.as_primitive::<Float32Type>()
.values()
.clone();
let positions = batch
.column_by_name(POSITION_COL)
.map(|col| col.as_list::<i32>().clone());
Self::new(row_ids, frequencies, max_score, positions)
}
pub fn len(&self) -> usize {
self.row_ids.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn iter(&self) -> PlainPostingListIterator<'_> {
Box::new(
self.row_ids
.iter()
.zip(self.frequencies.iter())
.enumerate()
.map(|(idx, (doc_id, freq))| {
(
*doc_id,
*freq,
self.positions.as_ref().map(|p| {
let start = p.value_offsets()[idx] as usize;
let end = p.value_offsets()[idx + 1] as usize;
Box::new(
p.values().as_primitive::<Int32Type>().values()[start..end]
.iter()
.map(|pos| *pos as u32),
) as _
}),
)
}),
)
}
#[inline]
pub fn doc(&self, i: usize) -> LocatedDocInfo {
LocatedDocInfo::new(self.row_ids[i], self.frequencies[i])
}
pub fn positions(&self, index: usize) -> Option<Arc<dyn Array>> {
self.positions
.as_ref()
.map(|positions| positions.value(index))
}
pub fn max_score(&self) -> Option<f32> {
self.max_score
}
pub fn row_id(&self, i: usize) -> u64 {
self.row_ids[i]
}
}
#[derive(Debug, Clone)]
pub(super) enum FirstDocsState {
Standalone(Arc<OnceLock<Box<[u32]>>>),
Packed {
states: Arc<[OnceLock<Box<[u32]>>]>,
slot: usize,
},
}
impl FirstDocsState {
fn standalone() -> Self {
Self::Standalone(Arc::new(OnceLock::new()))
}
fn state(&self) -> &OnceLock<Box<[u32]>> {
match self {
Self::Standalone(state) => state,
Self::Packed { states, slot } => &states[*slot],
}
}
fn get_or_init(&self, initialize: impl FnOnce() -> Box<[u32]>) -> &[u32] {
self.state().get_or_init(initialize)
}
fn capacity_bytes(
&self,
block_count: usize,
context: &mut lance_core::deepsize::Context,
) -> usize {
if context.mark_seen(self.state() as *const _ as usize) {
std::mem::size_of::<OnceLock<Box<[u32]>>>()
.saturating_add(block_count.saturating_mul(std::mem::size_of::<u32>()))
} else {
0
}
}
#[cfg(test)]
fn shares_state_with(&self, other: &Self) -> bool {
std::ptr::eq(self.state(), other.state())
}
}
#[derive(Debug, Clone)]
pub struct CompressedPostingList {
pub max_score: f32,
pub length: u32,
pub blocks: LargeBinaryArray,
pub posting_tail_codec: PostingTailCodec,
pub block_size: usize,
pub positions: Option<CompressedPositionStorage>,
pub(crate) impacts: Option<ImpactSkipData>,
first_docs: FirstDocsState,
}
impl PartialEq for CompressedPostingList {
fn eq(&self, other: &Self) -> bool {
self.max_score == other.max_score
&& self.length == other.length
&& self.blocks == other.blocks
&& self.posting_tail_codec == other.posting_tail_codec
&& self.block_size == other.block_size
&& self.positions == other.positions
&& self.impacts == other.impacts
}
}
impl DeepSizeOf for CompressedPostingList {
fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
sliced_cache_bytes(&self.blocks)
+ self
.positions
.as_ref()
.map(|positions| positions.deep_size_of_children(context))
.unwrap_or(0)
+ self
.impacts
.as_ref()
.map(|impacts| {
sliced_cache_bytes(impacts.entries())
.saturating_add(impacts.derived_cache_bytes())
})
.unwrap_or(0)
+ self.first_docs.capacity_bytes(self.blocks.len(), context)
}
}
impl CompressedPostingList {
pub(crate) fn new(
blocks: LargeBinaryArray,
max_score: f32,
length: u32,
posting_tail_codec: PostingTailCodec,
block_size: usize,
positions: Option<CompressedPositionStorage>,
impacts: Option<ImpactSkipData>,
) -> Self {
debug_assert!(block_size.is_power_of_two());
Self {
max_score,
length,
blocks,
posting_tail_codec,
block_size,
positions,
impacts,
first_docs: FirstDocsState::standalone(),
}
}
pub(super) fn with_packed_first_docs(
mut self,
states: Arc<[OnceLock<Box<[u32]>>]>,
slot: usize,
) -> Self {
debug_assert!(slot < states.len());
self.first_docs = FirstDocsState::Packed { states, slot };
self
}
#[inline]
pub(crate) fn block_shift(&self) -> u32 {
self.block_size.trailing_zeros()
}
#[inline]
pub(crate) fn block_mask(&self) -> usize {
self.block_size - 1
}
pub fn from_batch(
batch: &RecordBatch,
max_score: f32,
length: u32,
posting_tail_codec: PostingTailCodec,
block_size: usize,
shared_position_codec: Option<PositionStreamCodec>,
) -> Result<Self> {
debug_assert_eq!(batch.num_rows(), 1);
let blocks = batch[POSTING_COL]
.as_list::<i32>()
.value(0)
.as_binary::<i64>()
.clone();
let positions = if let Some(col) = batch.column_by_name(COMPRESSED_POSITION_COL) {
let bytes = bytes::Bytes::from(col.as_binary::<i64>().value(0).to_vec());
let block_offsets = batch[POSITION_BLOCK_OFFSET_COL]
.as_list::<i32>()
.value(0)
.as_primitive::<UInt32Type>()
.values()
.to_vec();
let codec = shared_position_codec.unwrap_or_else(|| {
parse_shared_position_codec(batch.schema_ref().metadata())
.expect("shared position stream codec metadata should be valid")
});
Some(CompressedPositionStorage::SharedStream(
SharedPositionStream::new(codec, block_offsets, bytes),
))
} else {
batch.column_by_name(POSITION_COL).map(|col| {
CompressedPositionStorage::LegacyPerDoc(
col.as_list::<i32>().value(0).as_list::<i32>().clone(),
)
})
};
let impacts = batch
.column_by_name(IMPACT_COL)
.map(|col| {
let entries = col.as_list::<i32>().value(0).as_binary::<i64>().clone();
ImpactSkipData::new(entries, blocks.len())
})
.transpose()?;
Ok(Self {
max_score,
length,
blocks,
posting_tail_codec,
block_size,
positions,
impacts,
first_docs: FirstDocsState::standalone(),
})
}
pub fn iter(&self) -> CompressedPostingListIterator {
CompressedPostingListIterator::new(
self.length as usize,
self.blocks.clone(),
self.posting_tail_codec,
self.positions.clone(),
self.block_size,
)
}
pub fn block_max_score(&self, block_idx: usize) -> f32 {
if super::super::encoding::posting_block_score_prefix_len(self.block_size) == 0 {
return self.max_score;
}
let block = self.blocks.value(block_idx);
block[0..4].try_into().map(f32::from_le_bytes).unwrap()
}
#[inline]
pub fn block_least_doc_id(&self, block_idx: usize) -> u32 {
self.block_first_docs()[block_idx]
}
pub(crate) fn block_first_docs(&self) -> &[u32] {
self.first_docs.get_or_init(|| {
(0..self.blocks.len())
.map(|block_idx| {
let block = self.blocks.value(block_idx);
let remainder = self.length as usize % self.block_size;
if block_idx + 1 == self.blocks.len() && remainder > 0 {
return super::super::encoding::read_posting_tail_first_doc(
block,
self.posting_tail_codec,
self.block_size,
);
}
let prefix =
super::super::encoding::posting_block_score_prefix_len(self.block_size);
block[prefix..prefix + 4]
.try_into()
.map(u32::from_le_bytes)
.unwrap()
})
.collect::<Vec<_>>()
.into_boxed_slice()
})
}
#[cfg(test)]
pub(super) fn shares_first_docs_with(&self, other: &Self) -> bool {
self.first_docs.shares_state_with(&other.first_docs)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(super) struct EncodedBlocks {
offsets: Vec<u32>,
bytes: Vec<u8>,
}
impl EncodedBlocks {
pub(super) fn len(&self) -> usize {
self.offsets.len()
}
pub(super) fn size(&self) -> usize {
self.offsets.capacity() * std::mem::size_of::<u32>() + self.bytes.capacity()
}
pub(super) fn push_full_block(
&mut self,
doc_ids: &[u32],
frequencies: &[u32],
) -> Result<usize> {
let start = self.bytes.len();
self.offsets.push(start as u32);
super::super::encoding::encode_full_posting_block_into(
doc_ids,
frequencies,
&mut self.bytes,
)?;
Ok(self.bytes.len() - start)
}
pub(super) fn block(&self, index: usize) -> &[u8] {
let (start, end) = self.block_range(index);
&self.bytes[start..end]
}
fn block_range(&self, index: usize) -> (usize, usize) {
let start = self.offsets[index] as usize;
let end = self
.offsets
.get(index + 1)
.map(|offset| *offset as usize)
.unwrap_or(self.bytes.len());
(start, end)
}
pub(super) fn set_block_score(&mut self, index: usize, score: f32) {
let (start, _) = self.block_range(index);
self.bytes[start..start + 4].copy_from_slice(&score.to_le_bytes());
}
pub(super) fn append_remainder_block_with_codec(
&mut self,
doc_ids: &[u32],
frequencies: &[u32],
codec: PostingTailCodec,
block_size: usize,
) -> Result<()> {
self.offsets.push(self.bytes.len() as u32);
super::super::encoding::encode_remainder_posting_block_into(
doc_ids,
frequencies,
codec,
block_size,
&mut self.bytes,
)
}
pub(super) fn into_array(mut self) -> LargeBinaryArray {
let mut offsets = Vec::with_capacity(self.offsets.len() + 1);
offsets.extend(self.offsets.into_iter().map(i64::from));
offsets.push(self.bytes.len() as i64);
LargeBinaryArray::new(
OffsetBuffer::new(ScalarBuffer::from(offsets)),
Buffer::from_vec(std::mem::take(&mut self.bytes)),
None,
)
}
pub(super) fn iter(&self) -> impl Iterator<Item = &[u8]> {
(0..self.len()).map(|index| self.block(index))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(super) struct EncodedPositionBlocks {
offsets: Vec<u32>,
bytes: Vec<u8>,
}
impl EncodedPositionBlocks {
pub(super) fn size(&self) -> usize {
self.offsets.capacity() * std::mem::size_of::<u32>() + self.bytes.capacity()
}
pub(super) fn block(&self, index: usize) -> &[u8] {
let start = self.offsets[index] as usize;
let end = self
.offsets
.get(index + 1)
.map(|offset| *offset as usize)
.unwrap_or(self.bytes.len());
&self.bytes[start..end]
}
pub(super) fn push_encoded_block(&mut self, block: &[u8]) -> usize {
let start = self.bytes.len();
self.offsets.push(start as u32);
self.bytes.extend_from_slice(block);
self.bytes.len() - start
}
pub(super) fn into_stream(self) -> SharedPositionStream {
SharedPositionStream::new(
PositionStreamCodec::PackedDelta,
self.offsets,
bytes::Bytes::from(self.bytes),
)
}
}