use super::*;
#[derive(Clone)]
pub struct Positions(pub(in super::super) CompressedPositionStorage);
pub(super) fn sliced_cache_bytes(array: &dyn Array) -> usize {
let validity_bytes = array
.nulls()
.map(|nulls| nulls.len().div_ceil(8))
.unwrap_or(0);
match array.data_type() {
DataType::LargeBinary => {
let array = array.as_binary::<i64>();
let data_bytes = if array.is_empty() {
0
} else {
let offsets = array.value_offsets();
(offsets[array.len()] - offsets[0]) as usize
};
data_bytes + (array.len() + 1) * std::mem::size_of::<i64>() + validity_bytes
}
DataType::List(_) => {
let array = array.as_list::<i32>();
let (child_start, child_end) = if array.is_empty() {
(0, 0)
} else {
let offsets = array.value_offsets();
(offsets[0] as usize, offsets[array.len()] as usize)
};
let offset_bytes = (array.len() + 1) * std::mem::size_of::<i32>();
let child = array.values().slice(child_start, child_end - child_start);
offset_bytes + validity_bytes + sliced_cache_bytes(child.as_ref())
}
other => match other.primitive_width() {
Some(width) => array.len() * width + validity_bytes,
None => array.get_buffer_memory_size(),
},
}
}
impl DeepSizeOf for Positions {
fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
self.0.deep_size_of_children(context)
}
}
#[derive(Debug, Clone)]
pub struct PostingListKey {
pub token_id: u32,
}
impl CacheKey for PostingListKey {
type ValueType = PostingList;
fn key(&self) -> std::borrow::Cow<'_, str> {
format!("postings-{}", self.token_id).into()
}
fn type_name() -> &'static str {
"PostingList"
}
fn schema() -> CacheKeySchema {
CacheKeySchema::new("lance.scalar.inverted.posting-list-key", 1)
}
fn write_key(&self, builder: &mut KeyBuilder) {
builder.write_u32(self.token_id);
}
fn codec() -> Option<CacheCodec> {
Some(CacheCodec::from_impl::<PostingList>())
}
}
#[derive(Debug, Clone)]
pub struct PostingListGroupKey {
pub start: u32,
pub end: u32,
}
impl CacheKey for PostingListGroupKey {
type ValueType = PostingListGroup;
fn key(&self) -> std::borrow::Cow<'_, str> {
format!("postings-{}-{}", self.start, self.end).into()
}
fn type_name() -> &'static str {
"PostingListGroup"
}
fn schema() -> CacheKeySchema {
CacheKeySchema::new("lance.scalar.inverted.posting-list-group-key", 1)
}
fn write_key(&self, builder: &mut KeyBuilder) {
builder.write_u32(self.start);
builder.write_u32(self.end);
}
fn codec() -> Option<CacheCodec> {
Some(CacheCodec::from_impl::<PostingListGroup>())
}
}
#[derive(Debug, Clone)]
pub(super) struct ImpactAwareCacheKey<K> {
inner: K,
has_impacts: bool,
}
impl<K: CacheKey> CacheKey for ImpactAwareCacheKey<K> {
type ValueType = K::ValueType;
fn key(&self) -> std::borrow::Cow<'_, str> {
if self.has_impacts {
format!("{}-impacts", self.inner.key()).into()
} else {
self.inner.key()
}
}
fn type_name() -> &'static str {
K::type_name()
}
fn stable_type_id() -> &'static str {
K::stable_type_id()
}
fn schema() -> CacheKeySchema {
CacheKeySchema::new("lance.scalar.inverted.impact-aware-key", 1)
}
fn write_key(&self, builder: &mut KeyBuilder) {
let inner_schema = K::schema();
builder.write_str(K::stable_type_id());
builder.write_str(inner_schema.id());
builder.write_u32(inner_schema.version());
builder.write_variant(if self.has_impacts { 1 } else { 0 });
self.inner.write_key(builder);
}
fn codec() -> Option<CacheCodec> {
K::codec()
}
}
pub(super) fn posting_list_cache_key(
token_id: u32,
has_impacts: bool,
) -> ImpactAwareCacheKey<PostingListKey> {
ImpactAwareCacheKey {
inner: PostingListKey { token_id },
has_impacts,
}
}
pub(super) fn posting_list_group_cache_key(
start: u32,
end: u32,
has_impacts: bool,
) -> ImpactAwareCacheKey<PostingListGroupKey> {
ImpactAwareCacheKey {
inner: PostingListGroupKey { start, end },
has_impacts,
}
}
#[derive(Debug, Clone, DeepSizeOf)]
pub(super) struct PostingMetadataValue {
pub(super) max_score: f32,
pub(super) length: u32,
}
#[derive(Debug, Clone)]
pub(super) struct PostingMetadataKey {
pub(super) token_id: u32,
}
impl CacheKey for PostingMetadataKey {
type ValueType = PostingMetadataValue;
fn key(&self) -> std::borrow::Cow<'_, str> {
format!("posting-metadata-{}", self.token_id).into()
}
fn type_name() -> &'static str {
"PostingMetadata"
}
fn schema() -> CacheKeySchema {
CacheKeySchema::new("lance.scalar.inverted.posting-metadata-key", 1)
}
fn write_key(&self, builder: &mut KeyBuilder) {
builder.write_u32(self.token_id);
}
}
#[derive(Debug, Clone)]
pub struct PositionKey {
pub token_id: u32,
}
impl CacheKey for PositionKey {
type ValueType = Positions;
fn key(&self) -> std::borrow::Cow<'_, str> {
format!("positions-{}", self.token_id).into()
}
fn type_name() -> &'static str {
"Position"
}
fn schema() -> CacheKeySchema {
CacheKeySchema::new("lance.scalar.inverted.position-key", 1)
}
fn write_key(&self, builder: &mut KeyBuilder) {
builder.write_u32(self.token_id);
}
fn codec() -> Option<CacheCodec> {
Some(CacheCodec::from_impl::<Positions>())
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum CompressedPositionStorage {
LegacyPerDoc(ListArray),
SharedStream(SharedPositionStream),
}
impl DeepSizeOf for CompressedPositionStorage {
fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
match self {
Self::LegacyPerDoc(positions) => sliced_cache_bytes(positions),
Self::SharedStream(stream) => stream.size(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SharedPositionStream {
codec: PositionStreamCodec,
block_offsets: Arc<[u32]>,
bytes: bytes::Bytes,
}
impl SharedPositionStream {
pub fn new(codec: PositionStreamCodec, block_offsets: Vec<u32>, bytes: bytes::Bytes) -> Self {
Self {
codec,
block_offsets: Arc::from(block_offsets.into_boxed_slice()),
bytes,
}
}
pub fn codec(&self) -> PositionStreamCodec {
self.codec
}
pub fn block_count(&self) -> usize {
self.block_offsets.len()
}
pub fn block_range(&self, index: usize) -> Range<usize> {
let start = self.block_offsets[index] as usize;
let end = self
.block_offsets
.get(index + 1)
.map(|offset| *offset as usize)
.unwrap_or(self.bytes.len());
start..end
}
pub fn block(&self, index: usize) -> &[u8] {
let range = self.block_range(index);
&self.bytes[range]
}
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
pub fn block_offsets(&self) -> &[u32] {
self.block_offsets.as_ref()
}
pub fn size(&self) -> usize {
self.block_offsets.len() * std::mem::size_of::<u32>() + self.bytes.len()
}
}
#[derive(Debug, Clone)]
pub struct PostingListGroup {
pub(in super::super) storage: PostingListGroupStorage,
}
#[derive(Debug, Clone)]
pub(in super::super) enum PostingListGroupStorage {
Packed(PackedPostingListGroup),
Materialized(Vec<PostingList>),
}
#[derive(Debug, Clone)]
pub(in super::super) struct PackedPostingListGroup {
pub(in super::super) batch: RecordBatch,
pub(in super::super) posting_tail_codec: PostingTailCodec,
pub(in super::super) block_size: usize,
pub(super) first_docs_states: Arc<[OnceLock<Box<[u32]>>]>,
pub(super) first_docs_state_capacity_bytes: usize,
pub(super) impact_states: Option<Arc<[OnceLock<Box<ImpactSkipData>>]>>,
pub(super) impact_state_capacity_bytes: usize,
}
impl DeepSizeOf for PostingListGroup {
fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
match &self.storage {
PostingListGroupStorage::Packed(group) => group
.batch
.columns()
.iter()
.map(|column| sliced_cache_bytes(column.as_ref()))
.sum::<usize>()
.saturating_add(group.first_docs_state_capacity_bytes)
.saturating_add(group.impact_state_capacity_bytes),
PostingListGroupStorage::Materialized(posting_lists) => {
posting_lists.deep_size_of_children(context)
}
}
}
}
impl PostingListGroup {
pub(in super::super) fn new(posting_lists: Vec<PostingList>) -> Self {
Self {
storage: PostingListGroupStorage::Materialized(posting_lists),
}
}
pub(in super::super) fn new_packed(
batch: RecordBatch,
posting_tail_codec: PostingTailCodec,
) -> Result<Self> {
let block_size = parse_posting_block_size(batch.schema_ref().metadata())?;
Self::new_packed_with_block_size(batch, posting_tail_codec, block_size)
}
pub(super) fn new_packed_with_block_size(
batch: RecordBatch,
posting_tail_codec: PostingTailCodec,
block_size: usize,
) -> Result<Self> {
validate_block_size(block_size)?;
if let Some(encoded_block_size) = batch.schema_ref().metadata().get(POSTING_BLOCK_SIZE_KEY)
{
let encoded_block_size = encoded_block_size.parse::<usize>().map_err(|err| {
Error::index(format!(
"invalid {POSTING_BLOCK_SIZE_KEY} metadata value {encoded_block_size:?}: {err}"
))
})?;
if encoded_block_size != block_size {
return Err(Error::index(format!(
"packed posting group {POSTING_BLOCK_SIZE_KEY}={encoded_block_size} does not match block_size={block_size}"
)));
}
}
let mut schema = batch.schema().as_ref().clone();
schema
.metadata
.insert(POSTING_BLOCK_SIZE_KEY.to_owned(), block_size.to_string());
let batch = batch.with_schema(Arc::new(schema))?;
let postings = batch
.column_by_name(POSTING_COL)
.and_then(|column| column.as_list_opt::<i32>())
.ok_or_else(|| {
Error::index(format!(
"packed posting group column {POSTING_COL} must be List<LargeBinary>"
))
})?;
if postings.values().data_type() != &DataType::LargeBinary {
return Err(Error::index(format!(
"packed posting group column {POSTING_COL} must contain LargeBinary values, got {}",
postings.values().data_type()
)));
}
if postings.null_count() != 0 {
return Err(Error::index(
"packed posting group column must not contain nulls".to_string(),
));
}
let total_posting_blocks = (0..batch.num_rows())
.map(|slot| postings.value_length(slot) as usize)
.sum::<usize>();
let first_docs_states: Arc<[OnceLock<Box<[u32]>>]> = (0..batch.num_rows())
.map(|_| OnceLock::new())
.collect::<Vec<_>>()
.into();
let first_docs_state_capacity_bytes = first_docs_states
.len()
.saturating_mul(std::mem::size_of::<OnceLock<Box<[u32]>>>())
.saturating_add(total_posting_blocks.saturating_mul(std::mem::size_of::<u32>()));
let (impact_states, impact_state_capacity_bytes) = if let Some(impacts) =
batch.column_by_name(IMPACT_COL)
{
let impacts = impacts.as_list_opt::<i32>().ok_or_else(|| {
Error::index(format!(
"packed posting group column {IMPACT_COL} must be List<LargeBinary>"
))
})?;
if impacts.values().data_type() != &DataType::LargeBinary {
return Err(Error::index(format!(
"packed posting group column {IMPACT_COL} must contain LargeBinary values, got {}",
impacts.values().data_type()
)));
}
if impacts.null_count() != 0 {
return Err(Error::index(format!(
"packed posting group column {IMPACT_COL} must not contain nulls"
)));
}
let mut derived_cache_bytes = 0usize;
for slot in 0..batch.num_rows() {
let posting_blocks = postings.value_length(slot) as usize;
let impact_entries = impacts.value_length(slot) as usize;
let expected_impact_entries =
posting_blocks.saturating_add(posting_blocks.div_ceil(IMPACT_LEVEL1_BLOCKS));
if impact_entries != expected_impact_entries {
return Err(Error::index(format!(
"packed posting group impact slot {slot} has {impact_entries} entries, expected {expected_impact_entries} for {posting_blocks} posting blocks"
)));
}
derived_cache_bytes = derived_cache_bytes.saturating_add(
ImpactSkipData::derived_cache_bytes_for_entries(impact_entries),
);
}
let states: Arc<[OnceLock<Box<ImpactSkipData>>]> = (0..batch.num_rows())
.map(|_| OnceLock::new())
.collect::<Vec<_>>()
.into();
let per_slot_bytes = std::mem::size_of::<OnceLock<Box<ImpactSkipData>>>()
.saturating_add(std::mem::size_of::<ImpactSkipData>());
let capacity_bytes = states
.len()
.saturating_mul(per_slot_bytes)
.saturating_add(derived_cache_bytes);
(Some(states), capacity_bytes)
} else {
(None, 0)
};
match (
batch.column_by_name(MAX_SCORE_COL),
batch.column_by_name(LENGTH_COL),
) {
(None, None) => {}
(Some(max_scores), Some(lengths)) => {
let max_scores = max_scores
.as_primitive_opt::<Float32Type>()
.ok_or_else(|| {
Error::index(format!(
"packed posting group column {MAX_SCORE_COL} must be Float32"
))
})?;
let lengths = lengths.as_primitive_opt::<UInt32Type>().ok_or_else(|| {
Error::index(format!(
"packed posting group column {LENGTH_COL} must be UInt32"
))
})?;
if max_scores.null_count() != 0 || lengths.null_count() != 0 {
return Err(Error::index(
"packed posting group metadata columns must not contain nulls".to_string(),
));
}
}
_ => {
return Err(Error::index(format!(
"packed posting group must contain both {MAX_SCORE_COL} and {LENGTH_COL}, or neither"
)));
}
}
Ok(Self {
storage: PostingListGroupStorage::Packed(PackedPostingListGroup {
batch,
posting_tail_codec,
block_size,
first_docs_states,
first_docs_state_capacity_bytes,
impact_states,
impact_state_capacity_bytes,
}),
})
}
pub(in super::super) fn len(&self) -> usize {
match &self.storage {
PostingListGroupStorage::Packed(group) => group.batch.num_rows(),
PostingListGroupStorage::Materialized(posting_lists) => posting_lists.len(),
}
}
#[cfg(test)]
pub(in super::super) fn is_packed(&self) -> bool {
matches!(&self.storage, PostingListGroupStorage::Packed(_))
}
pub(super) fn needs_external_metadata(&self) -> bool {
match &self.storage {
PostingListGroupStorage::Packed(group) => {
group.batch.column_by_name(MAX_SCORE_COL).is_none()
}
PostingListGroupStorage::Materialized(_) => false,
}
}
pub(in super::super) fn posting_list(
&self,
slot: usize,
max_score: Option<f32>,
length: Option<u32>,
) -> Result<Option<PostingList>> {
match &self.storage {
PostingListGroupStorage::Materialized(posting_lists) => {
Ok(posting_lists.get(slot).cloned())
}
PostingListGroupStorage::Packed(group) => {
if slot >= group.batch.num_rows() {
return Ok(None);
}
let postings = group
.batch
.column_by_name(POSTING_COL)
.and_then(|column| column.as_list_opt::<i32>())
.ok_or_else(|| {
Error::index(format!(
"packed posting group column {POSTING_COL} must be List<LargeBinary>"
))
})?;
let blocks = postings.value(slot);
let blocks = blocks.as_binary_opt::<i64>().ok_or_else(|| {
Error::index(format!(
"packed posting group slot {slot} is not LargeBinary"
))
})?;
let max_score = match group.batch.column_by_name(MAX_SCORE_COL) {
Some(column) => column
.as_primitive_opt::<Float32Type>()
.expect("packed group metadata was validated at construction")
.value(slot),
None => max_score.ok_or_else(|| {
Error::index("packed posting group requires max-score metadata".to_string())
})?,
};
let length = match group.batch.column_by_name(LENGTH_COL) {
Some(column) => column
.as_primitive_opt::<UInt32Type>()
.expect("packed group metadata was validated at construction")
.value(slot),
None => length.ok_or_else(|| {
Error::index("packed posting group requires length metadata".to_string())
})?,
};
let impacts = match (
group.impact_states.as_ref(),
group.batch.column_by_name(IMPACT_COL),
) {
(Some(states), Some(column)) => {
let state = states.get(slot).ok_or_else(|| {
Error::index(format!(
"packed posting group impact state missing slot {slot}"
))
})?;
let impact_lists = column.as_list_opt::<i32>().ok_or_else(|| {
Error::index(format!(
"packed posting group column {IMPACT_COL} must be List<LargeBinary>"
))
})?;
let entries = impact_lists.value(slot);
let entries = entries.as_binary_opt::<i64>().ok_or_else(|| {
Error::index(format!(
"packed posting group impact slot {slot} is not LargeBinary"
))
})?;
let impacts =
state.get_or_init(|| {
Box::new(ImpactSkipData::new(entries.clone(), blocks.len()).expect(
"packed impact entry count was validated at construction",
))
});
Some(impacts.as_ref().clone())
}
(None, None) => None,
_ => {
return Err(Error::internal(
"packed posting group impact column/state mismatch".to_string(),
));
}
};
Ok(Some(PostingList::Compressed(
CompressedPostingList::new(
blocks.clone(),
max_score,
length,
group.posting_tail_codec,
group.block_size,
None,
impacts,
)
.with_packed_first_docs(group.first_docs_states.clone(), slot),
)))
}
}
}
}