rabitq-rs 0.9.0

Advanced vector search: RaBitQ quantization with IVF and MSTG (Multi-Scale Tree Graph) index
Documentation
use std::convert::TryInto;
use std::fs::File;
use std::io::{self, BufReader, ErrorKind, Read};
use std::path::Path;

const PROGRESS_INTERVAL: usize = 50_000;

fn read_vecs_from_reader<R, T, F>(
    mut reader: R,
    limit: Option<usize>,
    convert: F,
    progress_label: &str,
) -> io::Result<Vec<Vec<T>>>
where
    R: Read,
    F: Fn([u8; 4]) -> T,
{
    let mut vectors = Vec::new();

    loop {
        if let Some(max) = limit {
            if vectors.len() >= max {
                break;
            }
        }

        let mut dim_buf = [0u8; 4];
        match reader.read_exact(&mut dim_buf) {
            Ok(_) => {}
            Err(err) if err.kind() == ErrorKind::UnexpectedEof => break,
            Err(err) => return Err(err),
        }
        let dim = i32::from_le_bytes(dim_buf);
        if dim < 0 {
            return Err(io::Error::new(
                ErrorKind::InvalidData,
                format!("negative dimension {dim} encountered"),
            ));
        }
        let dim = dim as usize;

        let mut buffer = vec![0u8; dim * 4];
        reader.read_exact(&mut buffer)?;
        if !buffer.chunks_exact(4).remainder().is_empty() {
            return Err(io::Error::new(
                ErrorKind::InvalidData,
                "vector payload is not aligned to 4-byte elements",
            ));
        }

        let mut vector = Vec::with_capacity(dim);
        for chunk in buffer.chunks_exact(4) {
            let bytes: [u8; 4] = chunk.try_into().expect("chunk of length 4");
            vector.push(convert(bytes));
        }
        vectors.push(vector);
        log_progress(progress_label, vectors.len());
    }

    Ok(vectors)
}

pub fn read_fvecs_from_reader<R: Read>(
    reader: R,
    limit: Option<usize>,
) -> io::Result<Vec<Vec<f32>>> {
    read_vecs_from_reader(reader, limit, f32::from_le_bytes, "vectors")
}

pub fn read_ivecs_from_reader<R: Read>(
    reader: R,
    limit: Option<usize>,
) -> io::Result<Vec<Vec<i32>>> {
    read_vecs_from_reader(reader, limit, i32::from_le_bytes, "vectors")
}

pub fn read_fvecs<P: AsRef<Path>>(path: P, limit: Option<usize>) -> io::Result<Vec<Vec<f32>>> {
    let file = File::open(path)?;
    read_fvecs_from_reader(BufReader::new(file), limit)
}

pub fn read_ivecs<P: AsRef<Path>>(path: P, limit: Option<usize>) -> io::Result<Vec<Vec<i32>>> {
    let file = File::open(path)?;
    read_ivecs_from_reader(BufReader::new(file), limit)
}

pub fn read_ids_from_reader<R: Read>(reader: R, limit: Option<usize>) -> io::Result<Vec<usize>> {
    let rows = read_ivecs_from_reader(reader, limit)?;
    convert_ids(rows)
}

pub fn read_ids<P: AsRef<Path>>(path: P, limit: Option<usize>) -> io::Result<Vec<usize>> {
    let file = File::open(path)?;
    read_ids_from_reader(BufReader::new(file), limit)
}

pub fn read_groundtruth_from_reader<R: Read>(
    reader: R,
    limit: Option<usize>,
) -> io::Result<Vec<Vec<usize>>> {
    let rows = read_ivecs_from_reader(reader, limit)?;
    convert_rows(rows)
}

pub fn read_groundtruth<P: AsRef<Path>>(
    path: P,
    limit: Option<usize>,
) -> io::Result<Vec<Vec<usize>>> {
    let file = File::open(path)?;
    read_groundtruth_from_reader(BufReader::new(file), limit)
}

fn convert_ids(rows: Vec<Vec<i32>>) -> io::Result<Vec<usize>> {
    let mut ids = Vec::with_capacity(rows.len());
    for (idx, mut row) in rows.into_iter().enumerate() {
        if row.is_empty() {
            return Err(io::Error::new(
                ErrorKind::InvalidData,
                format!("cluster id row {idx} is empty"),
            ));
        }
        if row.len() > 1 {
            return Err(io::Error::new(
                ErrorKind::InvalidData,
                format!(
                    "cluster id row {idx} has {} elements, expected exactly 1",
                    row.len()
                ),
            ));
        }
        let value = row.remove(0);
        if value < 0 {
            return Err(io::Error::new(
                ErrorKind::InvalidData,
                format!("cluster id {value} at row {idx} is negative"),
            ));
        }
        ids.push(value as usize);
        log_progress("cluster assignments", ids.len());
    }
    Ok(ids)
}

fn convert_rows(rows: Vec<Vec<i32>>) -> io::Result<Vec<Vec<usize>>> {
    let mut converted = Vec::with_capacity(rows.len());
    for (row_idx, row) in rows.into_iter().enumerate() {
        let mut converted_row = Vec::with_capacity(row.len());
        for value in row {
            if value < 0 {
                return Err(io::Error::new(
                    ErrorKind::InvalidData,
                    format!("ground truth entry {value} in row {row_idx} is negative"),
                ));
            }
            converted_row.push(value as usize);
        }
        converted.push(converted_row);
        log_progress("ground truth rows", converted.len());
    }
    Ok(converted)
}

fn log_progress(label: &str, count: usize) {
    if should_log_progress(count, PROGRESS_INTERVAL) {
        println!("  processed {count} {label} so far...");
    }
}

fn should_log_progress(count: usize, interval: usize) -> bool {
    interval > 0 && count != 0 && count.is_multiple_of(interval)
}

#[cfg(test)]
mod tests {
    use super::should_log_progress;

    #[test]
    fn progress_triggers_on_exact_multiples() {
        assert!(!should_log_progress(0, 5));
        assert!(!should_log_progress(4, 5));
        assert!(should_log_progress(5, 5));
        assert!(!should_log_progress(6, 5));
        assert!(should_log_progress(10, 5));
    }

    #[test]
    fn progress_handles_zero_interval() {
        assert!(!should_log_progress(5, 0));
    }
}