blob: 56d860fe1813ba1221f46aeaf946cac49f68710c [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::distance::{fvec_distance, fvec_normalize, MetricType};
use crate::index_io_util::validate_reserved_zero;
use crate::io::{PreadCursor, ReadRequest, SeekRead, SeekWrite};
use crate::ivfflat::IVFFlatIndex;
use crate::ivfpq::RowIdFilter;
use crate::kmeans;
use roaring::RoaringTreemap;
use std::io;
pub const IVFFLAT_MAGIC: u32 = 0x4956464C; // "IVFL"
pub const IVFFLAT_VERSION: u32 = 1;
pub const IVFFLAT_HEADER_SIZE: usize = 64;
const FLAG_DELTA_IDS: u32 = 1 << 0;
const REQUIRED_FLAGS: u32 = FLAG_DELTA_IDS;
const SUPPORTED_FLAGS: u32 = REQUIRED_FLAGS;
pub fn write_ivfflat_index(index: &IVFFlatIndex, out: &mut dyn SeekWrite) -> io::Result<()> {
let d = index.d;
let nlist = index.nlist;
validate_index_shape(index)?;
let d_i32 = usize_to_i32(d, "dimension")?;
let nlist_i32 = usize_to_i32(nlist, "nlist")?;
let total_vectors = index.ids.iter().try_fold(0i64, |sum, ids| {
let count = usize_to_i64(ids.len(), "total vector count")?;
sum.checked_add(count).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"total vector count exceeds i64 length limit",
)
})
})?;
let mut sorted_lists: Vec<(Vec<i64>, Vec<u8>, Vec<f32>)> = Vec::with_capacity(nlist);
for list_id in 0..nlist {
let count = index.ids[list_id].len();
if count == 0 {
sorted_lists.push((Vec::new(), Vec::new(), Vec::new()));
continue;
}
let mut order: Vec<usize> = (0..count).collect();
order.sort_by_key(|&idx| index.ids[list_id][idx]);
let sorted_ids: Vec<i64> = order.iter().map(|&idx| index.ids[list_id][idx]).collect();
let mut sorted_vectors = Vec::with_capacity(count * d);
for idx in order {
sorted_vectors.extend_from_slice(&index.vectors[list_id][idx * d..(idx + 1) * d]);
}
let (_, id_bytes) = encode_delta_varint_ids(&sorted_ids);
sorted_lists.push((sorted_ids, id_bytes, sorted_vectors));
}
write_u32_le(out, IVFFLAT_MAGIC)?;
write_u32_le(out, IVFFLAT_VERSION)?;
write_i32_le(out, d_i32)?;
write_i32_le(out, nlist_i32)?;
write_u32_le(out, index.metric as u32)?;
write_i64_le(out, total_vectors)?;
write_u32_le(out, FLAG_DELTA_IDS)?;
out.write_all(&[0u8; 32])?;
write_f32_slice(out, &index.quantizer_centroids)?;
let offset_table_size = nlist.checked_mul(16).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"IVF-FLAT offset table size overflow",
)
})?;
let data_start = out
.pos()
.checked_add(offset_table_size as u64)
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"IVF-FLAT data start offset overflow",
)
})?;
let mut list_offsets = vec![0i64; nlist];
let mut list_counts = vec![0i32; nlist];
let mut list_id_bytes_lens = vec![0i32; nlist];
let mut current_offset = data_start;
for list_id in 0..nlist {
list_offsets[list_id] = u64_to_i64(current_offset, "list offset")?;
let count = sorted_lists[list_id].0.len();
list_counts[list_id] = usize_to_i32(count, "list count")?;
if count > 0 {
let id_bytes_len = sorted_lists[list_id].1.len();
list_id_bytes_lens[list_id] = usize_to_i32(id_bytes_len, "delta ID section")?;
let vector_bytes = checked_list_bytes(
count,
d.checked_mul(4).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"IVF-FLAT bytes per vector overflow",
)
})?,
)?;
let list_bytes = 12usize
.checked_add(id_bytes_len)
.and_then(|len| len.checked_add(vector_bytes))
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "IVF-FLAT list size overflow")
})?;
current_offset = current_offset
.checked_add(list_bytes as u64)
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "IVF-FLAT offset overflow")
})?;
}
}
for list_id in 0..nlist {
write_i64_le(out, list_offsets[list_id])?;
write_i32_le(out, list_counts[list_id])?;
write_i32_le(out, list_id_bytes_lens[list_id])?;
}
for (sorted_ids, id_bytes, sorted_vectors) in sorted_lists {
if sorted_ids.is_empty() {
continue;
}
write_i64_le(out, sorted_ids[0])?;
write_i32_le(out, id_bytes.len() as i32)?;
out.write_all(&id_bytes)?;
write_f32_slice(out, &sorted_vectors)?;
}
Ok(())
}
pub struct IVFFlatIndexReader<R: SeekRead> {
reader: R,
pub d: usize,
pub nlist: usize,
pub metric: MetricType,
pub total_vectors: i64,
pub quantizer_centroids: Vec<f32>,
pub list_offsets: Vec<i64>,
pub list_counts: Vec<i32>,
pub list_id_bytes_lens: Vec<i32>,
delta_ids: bool,
loaded: bool,
}
impl<R: SeekRead> IVFFlatIndexReader<R> {
pub fn open(mut reader: R) -> io::Result<Self> {
let mut cursor = PreadCursor::new(&mut reader, 0);
let magic = read_u32_le(&mut cursor)?;
if magic != IVFFLAT_MAGIC {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid IVFFLAT magic: 0x{:08X}", magic),
));
}
let version = read_u32_le(&mut cursor)?;
if version != IVFFLAT_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Unsupported IVFFLAT version: {}", version),
));
}
let d = validate_positive_i32(read_i32_le(&mut cursor)?, "d")? as usize;
let nlist = validate_positive_i32(read_i32_le(&mut cursor)?, "nlist")? as usize;
let metric_code = read_u32_le(&mut cursor)?;
let metric = MetricType::from_code(metric_code).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Unknown metric type: {}", metric_code),
)
})?;
let total_vectors = read_i64_le(&mut cursor)?;
let flags = read_u32_le(&mut cursor)?;
let mut reserved = [0u8; 32];
cursor.read_exact(&mut reserved)?;
validate_reserved_zero(&reserved, "IVFFLAT")?;
let unknown_flags = flags & !SUPPORTED_FLAGS;
if unknown_flags != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Unsupported IVFFLAT flags: 0x{:08X}", unknown_flags),
));
}
if flags & REQUIRED_FLAGS != REQUIRED_FLAGS {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"IVFFLAT v1 requires delta IDs",
));
}
Ok(Self {
reader,
d,
nlist,
metric,
total_vectors,
quantizer_centroids: Vec::new(),
list_offsets: Vec::new(),
list_counts: Vec::new(),
list_id_bytes_lens: Vec::new(),
delta_ids: true,
loaded: false,
})
}
pub fn ensure_loaded(&mut self) -> io::Result<()> {
if self.loaded {
return Ok(());
}
let mut cursor = PreadCursor::new(&mut self.reader, IVFFLAT_HEADER_SIZE as u64);
self.quantizer_centroids =
read_f32_vec(&mut cursor, checked_section_size(self.nlist, self.d)?)?;
self.list_offsets = vec![0; self.nlist];
self.list_counts = vec![0; self.nlist];
self.list_id_bytes_lens = vec![0; self.nlist];
for list_id in 0..self.nlist {
self.list_offsets[list_id] = read_i64_le(&mut cursor)?;
let count = read_i32_le(&mut cursor)?;
if count < 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("negative list count {} at list {}", count, list_id),
));
}
self.list_counts[list_id] = count;
let id_bytes_len = read_i32_le(&mut cursor)?;
if id_bytes_len < 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("negative id_bytes_len {} at list {}", id_bytes_len, list_id),
));
}
self.list_id_bytes_lens[list_id] = id_bytes_len;
}
self.loaded = true;
Ok(())
}
pub fn read_inverted_list(&mut self, list_id: usize) -> io::Result<(Vec<i64>, Vec<f32>)> {
self.ensure_loaded()?;
if list_id >= self.nlist {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("list_id {} out of range (nlist={})", list_id, self.nlist),
));
}
let count = self.list_counts[list_id] as usize;
if count == 0 {
return Ok((Vec::new(), Vec::new()));
}
let offset = checked_list_offset(self.list_offsets[list_id], list_id)?;
let vector_bytes = checked_list_bytes(count, self.d * 4)?;
if self.delta_ids {
let id_bytes_len = self.list_id_bytes_lens[list_id] as usize;
let payload_len = 12usize
.checked_add(id_bytes_len)
.and_then(|len| len.checked_add(vector_bytes))
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "IVF-FLAT list payload overflow")
})?;
let mut payload = vec![0u8; payload_len];
self.reader
.pread(&mut [ReadRequest::new(offset, &mut payload)])?;
let base_id = i64::from_le_bytes(payload[0..8].try_into().unwrap());
let encoded_len = i32::from_le_bytes(payload[8..12].try_into().unwrap());
if encoded_len < 0 || encoded_len as usize != id_bytes_len {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"IVF-FLAT id_bytes_len mismatch",
));
}
let ids = decode_delta_varint_ids(base_id, &payload[12..12 + id_bytes_len], count)?;
let vectors = bytes_to_f32_vec(&payload[12 + id_bytes_len..])?;
Ok((ids, vectors))
} else {
Err(io::Error::new(
io::ErrorKind::InvalidData,
"IVF-FLAT reader only supports delta IDs",
))
}
}
pub fn search(
&mut self,
query: &[f32],
k: usize,
nprobe: usize,
) -> io::Result<(Vec<i64>, Vec<f32>)> {
self.search_with_filter(query, k, nprobe, None)
}
pub fn search_with_filter(
&mut self,
query: &[f32],
k: usize,
nprobe: usize,
filter: Option<&dyn RowIdFilter>,
) -> io::Result<(Vec<i64>, Vec<f32>)> {
self.ensure_loaded()?;
if query.len() != self.d {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"query length {} does not match index dimension {}",
query.len(),
self.d
),
));
}
if k == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"k must be greater than 0",
));
}
if nprobe == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"nprobe must be greater than 0",
));
}
let mut q = query.to_vec();
if self.metric == MetricType::Cosine {
fvec_normalize(&mut q);
}
let (probe_indices, _) =
kmeans::find_topk(&q, &self.quantizer_centroids, self.nlist, self.d, nprobe);
let mut heap = ReaderTopKHeap::new(k);
for list_id in probe_indices {
let (ids, vectors) = self.read_inverted_list(list_id)?;
for (local_idx, &id) in ids.iter().enumerate() {
if let Some(f) = filter {
if !f.contains(id) {
continue;
}
}
let vector = &vectors[local_idx * self.d..(local_idx + 1) * self.d];
heap.push(fvec_distance(&q, vector, self.metric), id);
}
}
let sorted = heap.into_sorted();
let mut labels: Vec<i64> = sorted.iter().map(|&(_, id)| id).collect();
let mut distances: Vec<f32> = sorted.iter().map(|&(dist, _)| dist).collect();
labels.resize(k, -1);
distances.resize(k, f32::MAX);
Ok((labels, distances))
}
pub fn search_with_roaring_filter(
&mut self,
query: &[f32],
k: usize,
nprobe: usize,
roaring_filter_bytes: &[u8],
) -> io::Result<(Vec<i64>, Vec<f32>)> {
let filter = decode_roaring_filter(roaring_filter_bytes)?;
self.search_with_filter(query, k, nprobe, Some(&filter))
}
}
/// Batch search for IVF-FLAT readers. Each unique probed list is read once and
/// scanned for all queries that selected it.
pub fn search_batch_ivfflat_reader<R: SeekRead>(
reader: &mut IVFFlatIndexReader<R>,
queries: &[f32],
nq: usize,
k: usize,
nprobe: usize,
) -> io::Result<(Vec<i64>, Vec<f32>)> {
search_batch_ivfflat_reader_filter(reader, queries, nq, k, nprobe, None)
}
pub fn search_batch_ivfflat_reader_filter<R: SeekRead>(
reader: &mut IVFFlatIndexReader<R>,
queries: &[f32],
nq: usize,
k: usize,
nprobe: usize,
filter: Option<&dyn RowIdFilter>,
) -> io::Result<(Vec<i64>, Vec<f32>)> {
reader.ensure_loaded()?;
let d = reader.d;
if nq == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"nq must be greater than 0",
));
}
let expected_query_len = nq.checked_mul(d).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"nq * dimension overflows usize",
)
})?;
if queries.len() != expected_query_len {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"queries length {} does not match nq * dimension {}",
queries.len(),
expected_query_len
),
));
}
if k == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"k must be greater than 0",
));
}
if nprobe == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"nprobe must be greater than 0",
));
}
let mut processed = queries[..expected_query_len].to_vec();
if reader.metric == MetricType::Cosine {
for qi in 0..nq {
fvec_normalize(&mut processed[qi * d..(qi + 1) * d]);
}
}
let (all_probe_indices, _) = kmeans::find_topk_batch(
&processed,
nq,
&reader.quantizer_centroids,
reader.nlist,
d,
nprobe,
);
let mut list_to_queries = vec![Vec::new(); reader.nlist];
let mut unique_lists = Vec::new();
for (qi, probe_indices) in all_probe_indices.iter().enumerate() {
for &list_id in probe_indices {
if list_to_queries[list_id].is_empty() {
unique_lists.push(list_id);
}
list_to_queries[list_id].push(qi);
}
}
let mut heaps: Vec<ReaderTopKHeap> = (0..nq).map(|_| ReaderTopKHeap::new(k)).collect();
for list_id in unique_lists {
let count = reader.list_counts[list_id] as usize;
if count == 0 {
continue;
}
let (ids, vectors) = reader.read_inverted_list(list_id)?;
for &qi in &list_to_queries[list_id] {
let query = &processed[qi * d..(qi + 1) * d];
for (local_idx, &id) in ids.iter().enumerate() {
if let Some(f) = filter {
if !f.contains(id) {
continue;
}
}
let vector = &vectors[local_idx * d..(local_idx + 1) * d];
heaps[qi].push(fvec_distance(query, vector, reader.metric), id);
}
}
}
let mut result_ids = vec![-1i64; nq * k];
let mut result_dists = vec![f32::MAX; nq * k];
for qi in 0..nq {
let sorted = std::mem::replace(&mut heaps[qi], ReaderTopKHeap::new(0)).into_sorted();
let base = qi * k;
for (i, &(dist, id)) in sorted.iter().enumerate() {
result_ids[base + i] = id;
result_dists[base + i] = dist;
}
}
Ok((result_ids, result_dists))
}
pub fn search_batch_ivfflat_reader_roaring_filter<R: SeekRead>(
reader: &mut IVFFlatIndexReader<R>,
queries: &[f32],
nq: usize,
k: usize,
nprobe: usize,
roaring_filter_bytes: &[u8],
) -> io::Result<(Vec<i64>, Vec<f32>)> {
let filter = decode_roaring_filter(roaring_filter_bytes)?;
search_batch_ivfflat_reader_filter(reader, queries, nq, k, nprobe, Some(&filter))
}
struct ReaderTopKHeap {
k: usize,
data: Vec<(f32, i64)>,
}
impl ReaderTopKHeap {
fn new(k: usize) -> Self {
Self {
k,
data: Vec::with_capacity(k),
}
}
fn push(&mut self, dist: f32, id: i64) {
if self.k == 0 {
return;
}
if self.data.len() < self.k {
self.data.push((dist, id));
return;
}
if let Some((worst_idx, _)) = self
.data
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.0.partial_cmp(&b.0).unwrap())
{
if dist < self.data[worst_idx].0 {
self.data[worst_idx] = (dist, id);
}
}
}
fn into_sorted(mut self) -> Vec<(f32, i64)> {
self.data.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
self.data
}
}
fn write_u32_le(out: &mut dyn SeekWrite, v: u32) -> io::Result<()> {
out.write_all(&v.to_le_bytes())
}
fn write_i32_le(out: &mut dyn SeekWrite, v: i32) -> io::Result<()> {
out.write_all(&v.to_le_bytes())
}
fn write_i64_le(out: &mut dyn SeekWrite, v: i64) -> io::Result<()> {
out.write_all(&v.to_le_bytes())
}
fn write_f32_slice(out: &mut dyn SeekWrite, data: &[f32]) -> io::Result<()> {
let bytes: Vec<u8> = data.iter().flat_map(|f| f.to_le_bytes()).collect();
out.write_all(&bytes)
}
fn read_u32_le<R: SeekRead + ?Sized>(reader: &mut PreadCursor<'_, R>) -> io::Result<u32> {
let mut buf = [0u8; 4];
reader.read_exact(&mut buf)?;
Ok(u32::from_le_bytes(buf))
}
fn read_i32_le<R: SeekRead + ?Sized>(reader: &mut PreadCursor<'_, R>) -> io::Result<i32> {
let mut buf = [0u8; 4];
reader.read_exact(&mut buf)?;
Ok(i32::from_le_bytes(buf))
}
fn read_i64_le<R: SeekRead + ?Sized>(reader: &mut PreadCursor<'_, R>) -> io::Result<i64> {
let mut buf = [0u8; 8];
reader.read_exact(&mut buf)?;
Ok(i64::from_le_bytes(buf))
}
fn validate_positive_i32(val: i32, field: &str) -> io::Result<i32> {
if val <= 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("invalid header field {}: {} (must be positive)", field, val),
));
}
Ok(val)
}
fn validate_index_shape(index: &IVFFlatIndex) -> io::Result<()> {
if index.d == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"dimension must be greater than 0",
));
}
if index.nlist == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"nlist must be greater than 0",
));
}
if index.ids.len() != index.nlist || index.vectors.len() != index.nlist {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"IVF-FLAT list storage does not match nlist",
));
}
let centroid_len = checked_section_size(index.nlist, index.d)?;
if index.quantizer_centroids.len() != centroid_len {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"centroid length {} does not match nlist*d {}",
index.quantizer_centroids.len(),
centroid_len
),
));
}
for list_id in 0..index.nlist {
let expected_vector_len =
index.ids[list_id]
.len()
.checked_mul(index.d)
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"IVF-FLAT vector length overflow",
)
})?;
if index.vectors[list_id].len() != expected_vector_len {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"list {} vector length {} does not match ids*d {}",
list_id,
index.vectors[list_id].len(),
expected_vector_len
),
));
}
}
Ok(())
}
fn usize_to_i32(value: usize, field: &str) -> io::Result<i32> {
if value > i32::MAX as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("{} exceeds i32 length limit: {}", field, value),
));
}
Ok(value as i32)
}
fn usize_to_i64(value: usize, field: &str) -> io::Result<i64> {
if value > i64::MAX as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("{} exceeds i64 length limit: {}", field, value),
));
}
Ok(value as i64)
}
fn u64_to_i64(value: u64, field: &str) -> io::Result<i64> {
if value > i64::MAX as u64 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("{} exceeds i64 offset limit: {}", field, value),
));
}
Ok(value as i64)
}
const MAX_SECTION_ELEMENTS: usize = 1 << 30;
fn checked_section_size(a: usize, b: usize) -> io::Result<usize> {
let result = a.checked_mul(b).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"section size overflow in IVF-FLAT header",
)
})?;
if result > MAX_SECTION_ELEMENTS {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"section size {} exceeds maximum {}",
result, MAX_SECTION_ELEMENTS
),
));
}
Ok(result)
}
fn checked_list_offset(offset: i64, list_id: usize) -> io::Result<u64> {
if offset < 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("negative list offset {} at list {}", offset, list_id),
));
}
Ok(offset as u64)
}
fn checked_list_bytes(count: usize, bytes_per_entry: usize) -> io::Result<usize> {
count.checked_mul(bytes_per_entry).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"IVF-FLAT list byte size overflow",
)
})
}
fn read_f32_vec<R: SeekRead + ?Sized>(
reader: &mut PreadCursor<'_, R>,
count: usize,
) -> io::Result<Vec<f32>> {
let mut buf = vec![0u8; count * 4];
reader.read_exact(&mut buf)?;
bytes_to_f32_vec(&buf)
}
fn bytes_to_f32_vec(bytes: &[u8]) -> io::Result<Vec<f32>> {
if !bytes.len().is_multiple_of(4) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"f32 byte section is not 4-byte aligned",
));
}
Ok(bytes
.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect())
}
fn encode_varint(mut val: u64, buf: &mut Vec<u8>) {
while val >= 0x80 {
buf.push((val as u8) | 0x80);
val >>= 7;
}
buf.push(val as u8);
}
fn decode_varint(buf: &[u8], pos: &mut usize) -> io::Result<u64> {
let mut val = 0u64;
let mut shift = 0u32;
loop {
if *pos >= buf.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"truncated varint",
));
}
let b = buf[*pos] as u64;
*pos += 1;
let payload = b & 0x7F;
if shift == 63 && payload > 1 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"varint exceeds u64 range",
));
}
val |= payload << shift;
if b & 0x80 == 0 {
break;
}
shift += 7;
if shift > 63 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"varint exceeds 64 bits",
));
}
}
Ok(val)
}
fn encode_delta_varint_ids(ids: &[i64]) -> (i64, Vec<u8>) {
if ids.is_empty() {
return (0, Vec::new());
}
let base = ids[0];
let mut buf = Vec::with_capacity(ids.len() * 2);
let mut prev = base;
for &id in ids {
let delta = (id as u64).wrapping_sub(prev as u64);
encode_varint(delta, &mut buf);
prev = id;
}
(base, buf)
}
fn decode_delta_varint_ids(base: i64, buf: &[u8], count: usize) -> io::Result<Vec<i64>> {
let mut ids = Vec::with_capacity(count);
let mut pos = 0;
let mut current = base as u64;
let mut prev_signed = base;
for _ in 0..count {
let delta = decode_varint(buf, &mut pos)?;
current = current.wrapping_add(delta);
let id = current as i64;
if id < prev_signed {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"decoded ID sequence is not monotonically non-decreasing",
));
}
prev_signed = id;
ids.push(id);
}
Ok(ids)
}
fn decode_roaring_filter(bytes: &[u8]) -> io::Result<RoaringTreemap> {
RoaringTreemap::deserialize_from(bytes).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("invalid RoaringTreemap filter: {}", e),
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::distance::MetricType;
use crate::io::PosWriter;
use crate::ivfflat::IVFFlatIndex;
use std::io::Cursor;
#[test]
fn test_ivfflat_write_read_search_roundtrip() {
let d = 4;
let nlist = 4;
let n = 128;
let data: Vec<f32> = (0..n)
.flat_map(|i| {
let cluster = (i % nlist) as f32 * 100.0;
[cluster + i as f32 * 0.01, 1.0, 2.0, 3.0]
})
.collect();
let ids: Vec<i64> = (1000..1000 + n as i64).collect();
let mut index = IVFFlatIndex::new(d, nlist, MetricType::L2);
index.train(&data, n);
index.add(&data, &ids, n);
let mut expected_distances = vec![0.0; 5];
let mut expected_labels = vec![0; 5];
index.search(
&data[7 * d..8 * d],
1,
5,
nlist,
&mut expected_distances,
&mut expected_labels,
);
let mut buf = Vec::new();
let mut writer = PosWriter::new(&mut buf);
write_ivfflat_index(&index, &mut writer).unwrap();
let mut reader = IVFFlatIndexReader::open(Cursor::new(buf)).unwrap();
let (labels, distances) = reader.search(&data[7 * d..8 * d], 5, nlist).unwrap();
assert_eq!(labels, expected_labels);
assert_eq!(distances, expected_distances);
}
#[test]
fn test_ivfflat_reader_search_with_filter() {
use std::collections::HashSet;
let d = 2;
let nlist = 1;
let data = vec![0.0, 0.0, 0.1, 0.0, 10.0, 10.0];
let ids = vec![10, 11, 12];
let mut index = IVFFlatIndex::new(d, nlist, MetricType::L2);
index.train(&data, 3);
index.add(&data, &ids, 3);
let mut buf = Vec::new();
let mut writer = PosWriter::new(&mut buf);
write_ivfflat_index(&index, &mut writer).unwrap();
let filter: HashSet<i64> = [12].into_iter().collect();
let mut reader = IVFFlatIndexReader::open(Cursor::new(buf)).unwrap();
let (labels, distances) = reader
.search_with_filter(&[0.0, 0.0], 2, 1, Some(&filter))
.unwrap();
assert_eq!(labels, vec![12, -1]);
assert_eq!(distances[0], 200.0);
assert_eq!(distances[1], f32::MAX);
}
#[test]
fn test_ivfflat_reader_search_with_roaring_filter_bytes() {
let d = 2;
let nlist = 1;
let data = vec![0.0, 0.0, 0.1, 0.0, 10.0, 10.0];
let ids = vec![10, 11, 12];
let mut index = IVFFlatIndex::new(d, nlist, MetricType::L2);
index.train(&data, 3);
index.add(&data, &ids, 3);
let mut buf = Vec::new();
let mut writer = PosWriter::new(&mut buf);
write_ivfflat_index(&index, &mut writer).unwrap();
let mut allowed = RoaringTreemap::new();
allowed.insert(12);
let mut filter_bytes = Vec::new();
allowed.serialize_into(&mut filter_bytes).unwrap();
let mut reader = IVFFlatIndexReader::open(Cursor::new(buf)).unwrap();
let (labels, distances) = reader
.search_with_roaring_filter(&[0.0, 0.0], 2, 1, &filter_bytes)
.unwrap();
assert_eq!(labels, vec![12, -1]);
assert_eq!(distances[0], 200.0);
assert_eq!(distances[1], f32::MAX);
}
#[test]
fn test_ivfflat_batch_reader_matches_single_reader_search() {
let d = 4;
let nlist = 4;
let n = 128;
let data: Vec<f32> = (0..n)
.flat_map(|i| {
let cluster = (i % nlist) as f32 * 100.0;
[cluster + i as f32 * 0.01, 1.0, 2.0, 3.0]
})
.collect();
let ids: Vec<i64> = (1000..1000 + n as i64).collect();
let mut index = IVFFlatIndex::new(d, nlist, MetricType::L2);
index.train(&data, n);
index.add(&data, &ids, n);
let mut buf = Vec::new();
let mut writer = PosWriter::new(&mut buf);
write_ivfflat_index(&index, &mut writer).unwrap();
let queries = [&data[7 * d..8 * d], &data[63 * d..64 * d]].concat();
let k = 5;
let nprobe = 3;
let mut batch_reader = IVFFlatIndexReader::open(Cursor::new(buf.clone())).unwrap();
let (batch_labels, batch_distances) =
search_batch_ivfflat_reader(&mut batch_reader, &queries, 2, k, nprobe).unwrap();
for qi in 0..2 {
let mut single_reader = IVFFlatIndexReader::open(Cursor::new(buf.clone())).unwrap();
let query = &queries[qi * d..(qi + 1) * d];
let (single_labels, single_distances) = single_reader.search(query, k, nprobe).unwrap();
assert_eq!(&batch_labels[qi * k..(qi + 1) * k], single_labels);
assert_eq!(&batch_distances[qi * k..(qi + 1) * k], single_distances);
}
}
#[test]
fn test_ivfflat_batch_reader_search_with_roaring_filter_bytes() {
let d = 2;
let nlist = 1;
let data = vec![0.0, 0.0, 0.1, 0.0, 10.0, 10.0];
let ids = vec![10, 11, 12];
let mut index = IVFFlatIndex::new(d, nlist, MetricType::L2);
index.train(&data, 3);
index.add(&data, &ids, 3);
let mut buf = Vec::new();
let mut writer = PosWriter::new(&mut buf);
write_ivfflat_index(&index, &mut writer).unwrap();
let mut allowed = RoaringTreemap::new();
allowed.insert(12);
let mut filter_bytes = Vec::new();
allowed.serialize_into(&mut filter_bytes).unwrap();
let mut reader = IVFFlatIndexReader::open(Cursor::new(buf)).unwrap();
let queries = vec![0.0, 0.0, 10.0, 10.0];
let (labels, distances) = search_batch_ivfflat_reader_roaring_filter(
&mut reader,
&queries,
2,
2,
1,
&filter_bytes,
)
.unwrap();
assert_eq!(labels, vec![12, -1, 12, -1]);
assert_eq!(distances, vec![200.0, f32::MAX, 0.0, f32::MAX]);
}
#[test]
fn test_ivfflat_reader_validates_inputs() {
let d = 2;
let nlist = 1;
let data = vec![0.0, 0.0, 1.0, 1.0];
let ids = vec![1, 2];
let mut index = IVFFlatIndex::new(d, nlist, MetricType::L2);
index.train(&data, 2);
index.add(&data, &ids, 2);
let mut buf = Vec::new();
let mut writer = PosWriter::new(&mut buf);
write_ivfflat_index(&index, &mut writer).unwrap();
let mut reader = IVFFlatIndexReader::open(Cursor::new(buf.clone())).unwrap();
assert!(reader.search(&[0.0], 1, 1).is_err());
let mut reader = IVFFlatIndexReader::open(Cursor::new(buf.clone())).unwrap();
assert!(reader.search(&[0.0, 0.0], 0, 1).is_err());
let mut reader = IVFFlatIndexReader::open(Cursor::new(buf)).unwrap();
assert!(reader.search(&[0.0, 0.0], 1, 0).is_err());
}
#[test]
fn test_ivfflat_writer_validates_shape_before_writing() {
let mut index = IVFFlatIndex::new(2, 1, MetricType::L2);
index.quantizer_centroids = vec![0.0, 0.0];
index.ids[0] = vec![1, 2];
index.vectors[0] = vec![0.0, 0.0];
let mut buf = Vec::new();
let mut writer = PosWriter::new(&mut buf);
let err = write_ivfflat_index(&index, &mut writer).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
assert!(err.to_string().contains("vector length"));
}
#[test]
fn test_ivfflat_reader_rejects_bad_magic() {
let mut buf = vec![0u8; IVFFLAT_HEADER_SIZE];
buf[0..4].copy_from_slice(&0x12345678u32.to_le_bytes());
let err = match IVFFlatIndexReader::open(Cursor::new(buf)) {
Ok(_) => panic!("bad magic should be rejected"),
Err(err) => err,
};
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}
#[test]
fn test_ivfflat_reader_rejects_missing_required_flags() {
let mut buf = vec![0u8; IVFFLAT_HEADER_SIZE];
buf[0..4].copy_from_slice(&IVFFLAT_MAGIC.to_le_bytes());
buf[4..8].copy_from_slice(&IVFFLAT_VERSION.to_le_bytes());
buf[8..12].copy_from_slice(&2i32.to_le_bytes());
buf[12..16].copy_from_slice(&1i32.to_le_bytes());
buf[16..20].copy_from_slice(&(MetricType::L2 as u32).to_le_bytes());
buf[20..28].copy_from_slice(&0i64.to_le_bytes());
buf[28..32].copy_from_slice(&0u32.to_le_bytes());
let err = match IVFFlatIndexReader::open(Cursor::new(buf)) {
Ok(_) => panic!("missing required flags should be rejected"),
Err(err) => err,
};
assert!(err.to_string().contains("requires delta IDs"));
}
#[test]
fn test_ivfflat_reader_rejects_unknown_flags() {
let mut buf = vec![0u8; IVFFLAT_HEADER_SIZE];
buf[0..4].copy_from_slice(&IVFFLAT_MAGIC.to_le_bytes());
buf[4..8].copy_from_slice(&IVFFLAT_VERSION.to_le_bytes());
buf[8..12].copy_from_slice(&2i32.to_le_bytes());
buf[12..16].copy_from_slice(&1i32.to_le_bytes());
buf[16..20].copy_from_slice(&(MetricType::L2 as u32).to_le_bytes());
buf[20..28].copy_from_slice(&0i64.to_le_bytes());
buf[28..32].copy_from_slice(&(REQUIRED_FLAGS | (1 << 31)).to_le_bytes());
let err = match IVFFlatIndexReader::open(Cursor::new(buf)) {
Ok(_) => panic!("unknown flags should be rejected"),
Err(err) => err,
};
assert!(err.to_string().contains("Unsupported IVFFLAT flags"));
}
#[test]
fn test_ivfflat_reader_rejects_nonzero_reserved_bytes() {
let mut buf = vec![0u8; IVFFLAT_HEADER_SIZE];
buf[0..4].copy_from_slice(&IVFFLAT_MAGIC.to_le_bytes());
buf[4..8].copy_from_slice(&IVFFLAT_VERSION.to_le_bytes());
buf[8..12].copy_from_slice(&2i32.to_le_bytes());
buf[12..16].copy_from_slice(&1i32.to_le_bytes());
buf[16..20].copy_from_slice(&(MetricType::L2 as u32).to_le_bytes());
buf[20..28].copy_from_slice(&0i64.to_le_bytes());
buf[28..32].copy_from_slice(&REQUIRED_FLAGS.to_le_bytes());
buf[32] = 1;
let err = match IVFFlatIndexReader::open(Cursor::new(buf)) {
Ok(_) => panic!("non-zero reserved bytes should be rejected"),
Err(err) => err,
};
assert!(err.to_string().contains("reserved bytes must be zero"));
}
}