use super::*;
#[derive(Debug, Clone, DeepSizeOf, Copy)]
pub enum DocInfo {
Located(LocatedDocInfo),
Raw(RawDocInfo),
}
impl DocInfo {
pub fn doc_id(&self) -> u64 {
match self {
Self::Raw(info) => info.doc_id as u64,
Self::Located(info) => info.row_id,
}
}
pub fn frequency(&self) -> u32 {
match self {
Self::Raw(info) => info.frequency,
Self::Located(info) => info.frequency as u32,
}
}
}
impl Eq for DocInfo {}
impl PartialEq for DocInfo {
fn eq(&self, other: &Self) -> bool {
self.doc_id() == other.doc_id()
}
}
impl PartialOrd for DocInfo {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for DocInfo {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.doc_id().cmp(&other.doc_id())
}
}
#[derive(Debug, Clone, Default, DeepSizeOf, Copy)]
pub struct LocatedDocInfo {
pub row_id: u64,
pub frequency: f32,
}
impl LocatedDocInfo {
pub fn new(row_id: u64, frequency: f32) -> Self {
Self { row_id, frequency }
}
}
impl Eq for LocatedDocInfo {}
impl PartialEq for LocatedDocInfo {
fn eq(&self, other: &Self) -> bool {
self.row_id == other.row_id
}
}
impl PartialOrd for LocatedDocInfo {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for LocatedDocInfo {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.row_id.cmp(&other.row_id)
}
}
#[derive(Debug, Clone, Default, DeepSizeOf, Copy)]
pub struct RawDocInfo {
pub doc_id: u32,
pub frequency: u32,
}
impl RawDocInfo {
pub fn new(doc_id: u32, frequency: u32) -> Self {
Self { doc_id, frequency }
}
}
impl Eq for RawDocInfo {}
impl PartialEq for RawDocInfo {
fn eq(&self, other: &Self) -> bool {
self.doc_id == other.doc_id
}
}
impl PartialOrd for RawDocInfo {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for RawDocInfo {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.doc_id.cmp(&other.doc_id)
}
}
pub(in super::super) fn quantize_doc_length(value: u32) -> u8 {
let num_bits = 32 - value.leading_zeros();
if num_bits < 4 {
value as u8
} else {
let shift = num_bits - 4;
(((value >> shift) as u8) & 0x07) | (((shift + 1) as u8) << 3)
}
}
#[inline]
pub(in super::super) fn dequantize_doc_length(code: u8) -> u32 {
DEQUANTIZED_DOC_LENGTHS[code as usize]
}
pub(in super::super) static DEQUANTIZED_DOC_LENGTHS: [u32; 256] = build_dequantized_doc_lengths();
pub(super) const fn build_dequantized_doc_lengths() -> [u32; 256] {
let mut table = [0u32; 256];
let mut code = 0usize;
while code < 256 {
let bits = (code & 0x07) as u64;
let shift = (code >> 3) as i64 - 1;
let decoded = if shift < 0 {
bits
} else {
(bits | 0x08) << shift
};
table[code] = if decoded > u32::MAX as u64 {
u32::MAX
} else {
decoded as u32
};
code += 1;
}
table
}
#[derive(Debug, Clone)]
pub(super) enum NumTokens {
Owned(Vec<u32>),
Shared(ScalarBuffer<u32>),
}
impl Default for NumTokens {
fn default() -> Self {
Self::Owned(Vec::new())
}
}
impl std::ops::Deref for NumTokens {
type Target = [u32];
fn deref(&self) -> &Self::Target {
match self {
Self::Owned(values) => values,
Self::Shared(values) => values,
}
}
}
impl DeepSizeOf for NumTokens {
fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
match self {
Self::Owned(values) => values.deep_size_of_children(context),
Self::Shared(values) => values.deep_size_of_children(context),
}
}
}
impl NumTokens {
fn with_capacity(capacity: usize) -> Self {
Self::Owned(Vec::with_capacity(capacity))
}
fn into_owned(self) -> Vec<u32> {
match self {
Self::Owned(values) => values,
Self::Shared(values) => values.to_vec(),
}
}
fn push(&mut self, value: u32) {
match self {
Self::Owned(values) => values.push(value),
Self::Shared(values) => {
let mut owned = values.to_vec();
owned.push(value);
*self = Self::Owned(owned);
}
}
}
fn memory_size(&self) -> usize {
match self {
Self::Owned(values) => values.capacity() * std::mem::size_of::<u32>(),
Self::Shared(values) => values.inner().capacity(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct DocSet {
pub(super) row_ids: Vec<u64>,
pub(super) num_tokens: NumTokens,
pub(super) doc_indices: Vec<Vec<u32>>,
pub(super) inv: Vec<(u64, u32)>,
pub(super) total_tokens: u64,
pub(super) scoring_quantized: bool,
pub(super) norms: Arc<std::sync::OnceLock<Box<[u8]>>>,
}
impl DeepSizeOf for DocSet {
fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
self.row_ids.deep_size_of_children(context)
+ self.num_tokens.deep_size_of_children(context)
+ self.doc_indices.deep_size_of_children(context)
+ self.inv.deep_size_of_children(context)
+ self
.norms
.get()
.map(|slab| std::mem::size_of_val(slab.as_ref()))
.unwrap_or(0)
}
}
impl DocSet {
pub(crate) fn with_coordinate_rank(coordinate_rank: usize) -> Self {
Self {
doc_indices: (0..coordinate_rank).map(|_| Vec::new()).collect(),
..Default::default()
}
}
#[inline]
pub fn len(&self) -> usize {
self.num_tokens.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[inline]
pub fn has_row_ids(&self) -> bool {
!self.row_ids.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&u64, &u32)> {
self.row_ids.iter().zip(self.num_tokens.iter())
}
pub fn row_id(&self, doc_id: u32) -> u64 {
self.row_ids[doc_id as usize]
}
pub fn doc_index(&self, doc_id: u32) -> Vec<u32> {
self.doc_indices
.iter()
.map(|coordinates| coordinates[doc_id as usize])
.collect()
}
pub fn coordinate(&self, doc_id: u32, rank: usize) -> u32 {
self.doc_indices[rank][doc_id as usize]
}
pub fn coordinate_rank(&self) -> usize {
self.doc_indices.len()
}
pub fn doc_ids(&self, row_id: u64) -> impl Iterator<Item = u64> + '_ {
if self.inv.is_empty() {
let found = self.row_ids.binary_search(&row_id).is_ok();
Either::Left(found.then_some(row_id).into_iter())
} else {
let lo = self.inv.partition_point(|entry| entry.0 < row_id);
let hi = self.inv.partition_point(|entry| entry.0 <= row_id);
Either::Right(self.inv[lo..hi].iter().map(|entry| entry.1 as u64))
}
}
pub fn total_tokens_num(&self) -> u64 {
self.total_tokens
}
#[inline]
pub fn average_length(&self) -> f32 {
self.total_tokens as f32 / self.len() as f32
}
pub fn calculate_block_max_scores<'a>(
&self,
doc_ids: impl Iterator<Item = &'a u32>,
freqs: impl Iterator<Item = &'a u32>,
) -> Vec<f32> {
self.calculate_block_max_scores_with_block_size(doc_ids, freqs, LEGACY_BLOCK_SIZE)
}
pub fn calculate_block_max_scores_with_block_size<'a>(
&self,
doc_ids: impl Iterator<Item = &'a u32>,
freqs: impl Iterator<Item = &'a u32>,
block_size: usize,
) -> Vec<f32> {
validate_block_size(block_size).expect("invalid posting list block size");
let avgdl = self.average_length();
let length = doc_ids.size_hint().0;
let num_blocks = length.div_ceil(block_size);
let mut block_max_scores = Vec::with_capacity(num_blocks);
let idf_scale = idf(length, self.len()) * (K1 + 1.0);
let mut max_score = f32::MIN;
for (i, (doc_id, freq)) in doc_ids.zip(freqs).enumerate() {
let doc_norm = K1 * (1.0 - B + B * self.num_tokens(*doc_id) as f32 / avgdl);
let freq = *freq as f32;
let score = freq / (freq + doc_norm);
if score > max_score {
max_score = score;
}
if (i + 1) % block_size == 0 {
max_score *= idf_scale;
block_max_scores.push(max_score);
max_score = f32::MIN;
}
}
if !length.is_multiple_of(block_size) {
max_score *= idf_scale;
block_max_scores.push(max_score);
}
block_max_scores
}
pub fn to_batch(&self) -> Result<RecordBatch> {
let row_id_col = UInt64Array::from_iter_values(self.row_ids.iter().cloned());
let num_tokens_col = UInt32Array::from_iter_values(self.num_tokens.iter().cloned());
let mut fields = vec![
arrow_schema::Field::new(ROW_ID, DataType::UInt64, false),
arrow_schema::Field::new(NUM_TOKEN_COL, DataType::UInt32, false),
];
let mut columns = vec![
Arc::new(row_id_col) as ArrayRef,
Arc::new(num_tokens_col) as ArrayRef,
];
for (rank, coordinates) in self.doc_indices.iter().enumerate() {
fields.push(arrow_schema::Field::new(
doc_index_storage_column(rank),
DataType::UInt32,
false,
));
columns.push(
Arc::new(UInt32Array::from_iter_values(coordinates.iter().copied())) as ArrayRef,
);
}
let schema = arrow_schema::Schema::new(fields);
let batch = RecordBatch::try_new(Arc::new(schema), columns)?;
Ok(batch)
}
pub async fn load(
reader: Arc<dyn IndexReader>,
is_legacy: bool,
frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
) -> Result<Self> {
let batch = reader.read_range(0..reader.num_rows(), None).await?;
let row_id_col = batch[ROW_ID].as_primitive::<datatypes::UInt64Type>();
let num_tokens_col = batch[NUM_TOKEN_COL].as_primitive::<datatypes::UInt32Type>();
let mut doc_indices = Vec::new();
for rank in 0.. {
let column_name = doc_index_storage_column(rank);
let Some(column) = batch.column_by_name(&column_name) else {
break;
};
doc_indices.push(column.as_primitive::<datatypes::UInt32Type>());
}
Self::from_columns_with_doc_indices(
row_id_col,
num_tokens_col,
&doc_indices,
is_legacy,
frag_reuse_index,
)
}
pub fn from_num_tokens_only(num_tokens_col: &arrow_array::UInt32Array) -> Self {
let total_tokens = num_tokens_col.values().iter().map(|&n| n as u64).sum();
Self::from_cached_num_tokens(num_tokens_col, total_tokens)
}
pub(crate) fn from_cached_num_tokens(
num_tokens_col: &arrow_array::UInt32Array,
total_tokens: u64,
) -> Self {
Self {
row_ids: Vec::new(),
num_tokens: NumTokens::Shared(num_tokens_col.values().clone()),
doc_indices: Vec::new(),
inv: Vec::new(),
total_tokens,
scoring_quantized: false,
norms: Arc::new(std::sync::OnceLock::new()),
}
}
pub fn from_columns(
row_id_col: &UInt64Array,
num_tokens_col: &arrow_array::UInt32Array,
is_legacy: bool,
frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
) -> Result<Self> {
Self::from_columns_with_doc_indices(
row_id_col,
num_tokens_col,
&[],
is_legacy,
frag_reuse_index,
)
}
pub fn from_columns_with_doc_indices(
row_id_col: &UInt64Array,
num_tokens_col: &arrow_array::UInt32Array,
doc_index_cols: &[&arrow_array::UInt32Array],
is_legacy: bool,
frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
) -> Result<Self> {
if doc_index_cols
.iter()
.any(|column| column.len() != row_id_col.len())
{
return Err(Error::index(
"FTS document coordinate columns must have the same length as row ids".to_string(),
));
}
let doc_indices = doc_index_cols
.iter()
.map(|column| column.values().to_vec())
.collect::<Vec<_>>();
if is_legacy {
let (row_ids, num_tokens): (Vec<_>, Vec<_>) = row_id_col
.values()
.iter()
.filter_map(|id| {
if let Some(frag_reuse_index_ref) = frag_reuse_index.as_ref() {
frag_reuse_index_ref.remap_row_id(*id)
} else {
Some(*id)
}
})
.zip(num_tokens_col.values().iter())
.sorted_unstable_by_key(|x| x.0)
.unzip();
let total_tokens = num_tokens.iter().map(|&x| x as u64).sum();
return Ok(Self {
row_ids,
num_tokens: NumTokens::Owned(num_tokens),
doc_indices,
inv: Vec::new(),
total_tokens,
scoring_quantized: false,
norms: Arc::new(std::sync::OnceLock::new()),
});
}
if let Some(frag_reuse_index_ref) = frag_reuse_index.as_ref() {
let mut row_ids = Vec::with_capacity(row_id_col.len());
let num_tokens = num_tokens_col.values().to_vec();
let mut inv = Vec::with_capacity(row_id_col.len());
for (doc_id, row_id) in row_id_col.values().iter().enumerate() {
match frag_reuse_index_ref.remap_row_id(*row_id) {
Some(new_row_id) => {
row_ids.push(new_row_id);
inv.push((new_row_id, doc_id as u32));
}
None => {
row_ids.push(RowAddress::TOMBSTONE_ROW);
}
}
}
inv.sort_unstable_by_key(|entry| entry.0);
let total_tokens = num_tokens.iter().map(|&x| x as u64).sum();
return Ok(Self {
row_ids,
num_tokens: NumTokens::Owned(num_tokens),
doc_indices,
inv,
total_tokens,
scoring_quantized: false,
norms: Arc::new(std::sync::OnceLock::new()),
});
}
let row_ids = row_id_col.values().to_vec();
let num_tokens = num_tokens_col.values().to_vec();
let mut inv: Vec<(u64, u32)> = row_ids
.iter()
.enumerate()
.map(|(doc_id, row_id)| (*row_id, doc_id as u32))
.collect();
if !row_ids.is_sorted() {
inv.sort_unstable_by_key(|entry| entry.0);
}
let total_tokens = num_tokens.iter().map(|&x| x as u64).sum();
Ok(Self {
row_ids,
num_tokens: NumTokens::Owned(num_tokens),
doc_indices,
inv,
total_tokens,
scoring_quantized: false,
norms: Arc::new(std::sync::OnceLock::new()),
})
}
pub fn remap(&mut self, mapping: &RowAddrRemap) -> Vec<u32> {
let mut removed = Vec::new();
let len = self.len();
let row_ids = std::mem::replace(&mut self.row_ids, Vec::with_capacity(len));
let num_tokens =
std::mem::replace(&mut self.num_tokens, NumTokens::with_capacity(len)).into_owned();
let doc_indices = std::mem::take(&mut self.doc_indices);
self.doc_indices = doc_indices
.iter()
.map(|_| Vec::with_capacity(len))
.collect();
self.invalidate_norms();
self.total_tokens = 0;
for (doc_id, (row_id, num_token)) in std::iter::zip(row_ids, num_tokens).enumerate() {
match mapping.get(row_id) {
Some(Some(new_row_id)) => {
self.row_ids.push(new_row_id);
self.num_tokens.push(num_token);
for (new_coordinates, old_coordinates) in
self.doc_indices.iter_mut().zip(&doc_indices)
{
new_coordinates.push(old_coordinates[doc_id]);
}
self.total_tokens += num_token as u64;
}
Some(None) => {
removed.push(doc_id as u32);
}
None => {
self.row_ids.push(row_id);
self.num_tokens.push(num_token);
for (new_coordinates, old_coordinates) in
self.doc_indices.iter_mut().zip(&doc_indices)
{
new_coordinates.push(old_coordinates[doc_id]);
}
self.total_tokens += num_token as u64;
}
}
}
removed
}
#[inline]
pub fn num_tokens(&self, doc_id: u32) -> u32 {
self.num_tokens[doc_id as usize]
}
pub fn set_quantized_scoring(&mut self, quantized: bool) {
self.scoring_quantized = quantized;
}
pub fn scoring_norms(&self) -> Option<&[u8]> {
if !self.scoring_quantized {
return None;
}
Some(
self.norms
.get_or_init(|| {
self.num_tokens
.iter()
.map(|&n| quantize_doc_length(n))
.collect()
})
.as_ref(),
)
}
#[inline]
pub fn scoring_num_tokens(&self, doc_id: u32) -> u32 {
match self.scoring_norms() {
Some(norms) => dequantize_doc_length(norms[doc_id as usize]),
None => self.num_tokens[doc_id as usize],
}
}
#[inline]
pub fn num_tokens_by_row_id(&self, row_id: u64) -> u32 {
self.row_ids
.binary_search(&row_id)
.map(|idx| self.num_tokens[idx])
.unwrap_or(0)
}
pub fn append(&mut self, row_id: u64, num_tokens: u32) -> u32 {
self.row_ids.push(row_id);
self.num_tokens.push(num_tokens);
self.total_tokens += num_tokens as u64;
self.invalidate_norms();
self.row_ids.len() as u32 - 1
}
pub fn append_with_doc_index(
&mut self,
row_id: u64,
num_tokens: u32,
doc_index: &[u32],
) -> Result<u32> {
if self.row_ids.is_empty() && self.doc_indices.is_empty() {
self.doc_indices = (0..doc_index.len()).map(|_| Vec::new()).collect();
}
if self.doc_indices.len() != doc_index.len() {
return Err(Error::index(format!(
"all documents in an FTS partition must have the same coordinate rank: expected {}, got {}",
self.doc_indices.len(),
doc_index.len()
)));
}
self.row_ids.push(row_id);
self.num_tokens.push(num_tokens);
for (coordinates, value) in self.doc_indices.iter_mut().zip(doc_index) {
coordinates.push(*value);
}
self.total_tokens += num_tokens as u64;
self.invalidate_norms();
Ok(self.row_ids.len() as u32 - 1)
}
fn invalidate_norms(&mut self) {
if self.norms.get().is_some() {
self.norms = Arc::new(std::sync::OnceLock::new());
}
}
pub(crate) fn memory_size(&self) -> usize {
self.row_ids.capacity() * std::mem::size_of::<u64>()
+ self.num_tokens.memory_size()
+ self
.doc_indices
.iter()
.map(|coordinates| coordinates.capacity() * std::mem::size_of::<u32>())
.sum::<usize>()
+ self.inv.capacity() * std::mem::size_of::<(u64, u32)>()
}
}