use std::borrow::Cow;
#[cfg(feature = "testing")]
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use crate::common::counter::hardware_counter::HardwareCounterCell;
use crate::common::storage_version::StorageVersion;
use crate::common::types::PointOffsetType;
use crate::common::universal_io::Result;
#[cfg(feature = "testing")]
use fs_err as fs;
#[cfg(feature = "testing")]
use zerocopy::{FromBytes, IntoBytes};
use crate::sparse::common::sparse_vector::RemappedSparseVector;
use crate::sparse::common::types::{DimId, DimOffset};
use crate::sparse::index::inverted_index::{InvertedIndex, out_of_bounds};
use crate::sparse::index::posting_list::{PostingList, PostingListIterator};
use crate::sparse::index::posting_list_common::PostingElementEx;
pub struct Version;
impl StorageVersion for Version {
fn current_raw() -> &'static str {
panic!("InvertedIndexRam is not supposed to be versioned");
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct InvertedIndexRam {
pub postings: Vec<PostingList>,
pub vector_count: usize,
pub total_sparse_size: usize,
}
impl InvertedIndex for InvertedIndexRam {
type Iter<'a> = PostingListIterator<'a>;
type Version = Version;
fn is_on_disk(&self) -> bool {
false
}
fn open(_path: &Path) -> Result<Self> {
panic!("InvertedIndexRam is not supposed to be loaded");
}
fn save(&self, _path: &Path) -> Result<()> {
panic!("InvertedIndexRam is not supposed to be saved");
}
fn get<'a>(
&'a self,
id: DimOffset,
_arena: &'a crate::sparse::SearchScratchArena,
_hw_counter: &'a HardwareCounterCell,
) -> Result<PostingListIterator<'a>> {
Ok(self.get(id)?.iter())
}
fn len(&self) -> usize {
self.postings.len()
}
fn posting_list_len(&self, id: DimOffset, _hw_counter: &HardwareCounterCell) -> Result<usize> {
Ok(self.get(id)?.elements.len())
}
fn files(_path: &Path) -> Vec<PathBuf> {
Vec::new()
}
fn immutable_files(_path: &Path) -> Vec<PathBuf> {
Vec::new()
}
fn remove(&mut self, id: PointOffsetType, old_vector: RemappedSparseVector) {
let old_vector_size = old_vector.len() * size_of::<PostingElementEx>();
for dim_id in old_vector.indices {
if let Some(posting) = self.postings.get_mut(dim_id as usize) {
posting.delete(id);
} else {
log::debug!("Posting list for dimension {dim_id} not found");
}
}
self.total_sparse_size = self.total_sparse_size.saturating_sub(old_vector_size);
self.vector_count = self.vector_count.saturating_sub(1);
}
fn upsert(
&mut self,
id: PointOffsetType,
vector: RemappedSparseVector,
old_vector: Option<RemappedSparseVector>,
) {
self.upsert(id, vector, old_vector);
}
fn from_ram_index<P: AsRef<Path>>(ram_index: Cow<InvertedIndexRam>, _path: P) -> Result<Self> {
Ok(ram_index.into_owned())
}
fn vector_count(&self) -> usize {
self.vector_count
}
fn total_sparse_vectors_size(&self) -> usize {
self.total_sparse_size
}
fn max_index(&self) -> Option<DimId> {
match self.postings.len() {
0 => None,
len => Some(len as DimId - 1),
}
}
}
impl InvertedIndexRam {
pub fn empty() -> InvertedIndexRam {
InvertedIndexRam {
postings: Vec::new(),
vector_count: 0,
total_sparse_size: 0,
}
}
pub fn get(&self, id: DimOffset) -> Result<&PostingList> {
self.postings
.get(id as usize)
.ok_or_else(|| out_of_bounds(id, self.len()))
}
pub fn upsert(
&mut self,
id: PointOffsetType,
vector: RemappedSparseVector,
old_vector: Option<RemappedSparseVector>,
) {
if let Some(old_vector) = &old_vector {
let elements_to_delete = old_vector
.indices
.iter()
.filter(|&dim_id| !vector.indices.contains(dim_id))
.map(|&dim_id| dim_id as usize);
for dim_id in elements_to_delete {
if let Some(posting) = self.postings.get_mut(dim_id) {
posting.delete(id);
} else {
log::debug!("Posting list for dimension {dim_id} not found");
}
}
}
let new_vector_size = vector.len() * size_of::<PostingElementEx>();
for (dim_id, weight) in vector.indices.into_iter().zip(vector.values) {
let dim_id = dim_id as usize;
match self.postings.get_mut(dim_id) {
Some(posting) => {
let posting_element = PostingElementEx::new(id, weight);
posting.upsert(posting_element);
}
None => {
self.postings.resize_with(dim_id + 1, PostingList::default);
self.postings[dim_id] = PostingList::new_one(id, weight);
}
}
}
if let Some(old) = old_vector {
self.total_sparse_size = self
.total_sparse_size
.saturating_sub(old.len() * size_of::<PostingElementEx>());
} else {
self.vector_count += 1;
}
self.total_sparse_size += new_vector_size
}
pub fn total_posting_elements_size(&self) -> usize {
self.postings
.iter()
.map(|posting| posting.elements.len() * size_of::<PostingElementEx>())
.sum()
}
}
#[cfg(feature = "testing")]
impl InvertedIndexRam {
const TEST_MAGIC: &'static [u8] = b"InvertedIndexRam for benchmarks.";
pub fn test_save(&self, path: &Path) -> std::io::Result<()> {
let InvertedIndexRam {
postings,
vector_count,
total_sparse_size,
} = self;
let mut f = BufWriter::new(fs::File::create(path)?);
f.write_all(Self::TEST_MAGIC)?;
f.write_all(postings.len().as_bytes())?;
for posting in postings {
f.write_all(posting.elements.len().as_bytes())?;
let bytes = posting.elements.as_bytes();
f.write_all(bytes)?;
f.write_all(&0usize.to_ne_bytes()[..bytes.len() % size_of::<usize>()])?;
}
f.write_all(vector_count.as_bytes())?;
f.write_all(total_sparse_size.as_bytes())?;
Ok(())
}
pub fn test_load(path: &Path) -> std::io::Result<Self> {
let f = fs::File::open(path)?;
let mmap = unsafe { memmap2::Mmap::map(&f)? };
let bytes = &mmap[..];
let (magic, bytes) =
<[u8]>::ref_from_prefix_with_elems(bytes, Self::TEST_MAGIC.len()).unwrap();
if magic != Self::TEST_MAGIC {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid magic",
));
}
let (postings_len, bytes) = usize::ref_from_prefix(bytes).unwrap();
let mut postings = Vec::with_capacity(*postings_len);
let mut bytes_cursor = bytes;
for _ in 0..*postings_len {
let (elements_len, bytes) = usize::ref_from_prefix(bytes_cursor).unwrap();
let (elements, bytes) =
<[PostingElementEx]>::ref_from_prefix_with_elems(bytes, *elements_len).unwrap();
let elements = elements.to_vec();
postings.push(PostingList { elements });
bytes_cursor = &bytes[bytes.as_ptr().align_offset(size_of::<usize>())..];
}
let bytes = bytes_cursor;
let (&vector_count, bytes) = usize::ref_from_prefix(bytes).unwrap();
let (&total_sparse_size, _bytes) = usize::ref_from_prefix(bytes).unwrap();
Ok(InvertedIndexRam {
postings,
vector_count,
total_sparse_size,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sparse::index::inverted_index::inverted_index_ram_builder::InvertedIndexBuilder;
#[test]
fn upsert_same_dimension_inverted_index_ram() {
let mut builder = InvertedIndexBuilder::new();
builder.add(1, [(1, 10.0), (2, 10.0), (3, 10.0)].into());
builder.add(2, [(1, 20.0), (2, 20.0), (3, 20.0)].into());
builder.add(3, [(1, 30.0), (2, 30.0), (3, 30.0)].into());
let mut inverted_index_ram = builder.build();
assert_eq!(inverted_index_ram.vector_count, 3);
inverted_index_ram.upsert(
4,
RemappedSparseVector::new(vec![1, 2, 3], vec![40.0, 40.0, 40.0]).unwrap(),
None,
);
for i in 1..4 {
let posting_list = inverted_index_ram.get(i).unwrap();
let posting_list = posting_list.elements.as_slice();
assert_eq!(posting_list.len(), 4);
assert_eq!(posting_list.first().unwrap().weight, 10.0);
assert_eq!(posting_list.get(1).unwrap().weight, 20.0);
assert_eq!(posting_list.get(2).unwrap().weight, 30.0);
assert_eq!(posting_list.get(3).unwrap().weight, 40.0);
}
}
#[test]
fn upsert_new_dimension_inverted_index_ram() {
let mut builder = InvertedIndexBuilder::new();
builder.add(1, [(1, 10.0), (2, 10.0), (3, 10.0)].into());
builder.add(2, [(1, 20.0), (2, 20.0), (3, 20.0)].into());
builder.add(3, [(1, 30.0), (2, 30.0), (3, 30.0)].into());
let mut inverted_index_ram = builder.build();
assert_eq!(inverted_index_ram.vector_count, 3);
assert_eq!(inverted_index_ram.postings.len(), 4);
inverted_index_ram.upsert(
4,
RemappedSparseVector::new(vec![1, 2, 30], vec![40.0, 40.0, 40.0]).unwrap(),
None,
);
assert_eq!(inverted_index_ram.postings.len(), 31);
for i in 1..3 {
let posting_list = inverted_index_ram.get(i).unwrap();
let posting_list = posting_list.elements.as_slice();
assert_eq!(posting_list.len(), 4);
assert_eq!(posting_list.first().unwrap().weight, 10.0);
assert_eq!(posting_list.get(1).unwrap().weight, 20.0);
assert_eq!(posting_list.get(2).unwrap().weight, 30.0);
assert_eq!(posting_list.get(3).unwrap().weight, 40.0);
}
let postings = inverted_index_ram.get(30).unwrap();
let postings = postings.elements.as_slice();
assert_eq!(postings.len(), 1);
let posting = postings.first().unwrap();
assert_eq!(posting.record_id, 4);
assert_eq!(posting.weight, 40.0);
}
#[test]
fn test_upsert_insert_equivalence() {
let first_vec: RemappedSparseVector = [(1, 10.0), (2, 10.0), (3, 10.0)].into();
let second_vec: RemappedSparseVector = [(1, 20.0), (2, 20.0), (3, 20.0)].into();
let third_vec: RemappedSparseVector = [(1, 30.0), (2, 30.0), (3, 30.0)].into();
let mut builder = InvertedIndexBuilder::new();
builder.add(1, first_vec.clone());
builder.add(2, second_vec.clone());
builder.add(3, third_vec.clone());
let inverted_index_ram_built = builder.build();
assert_eq!(inverted_index_ram_built.vector_count, 3);
let mut inverted_index_ram_upserted = InvertedIndexRam::empty();
inverted_index_ram_upserted.upsert(1, first_vec, None);
inverted_index_ram_upserted.upsert(2, second_vec, None);
inverted_index_ram_upserted.upsert(3, third_vec, None);
assert_eq!(
inverted_index_ram_built.postings.len(),
inverted_index_ram_upserted.postings.len()
);
assert_eq!(inverted_index_ram_built, inverted_index_ram_upserted);
}
}