| // 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_madd, preprocess_vectors, MetricType}; |
| use crate::index_io_util::{ |
| decode_delta_varint_ids, encode_delta_varint_ids, validate_search_inputs, |
| }; |
| use crate::io::{PreadCursor, ReadRequest, SeekRead, SeekWrite}; |
| use crate::ivfpq::RowIdFilter; |
| use crate::ivfrq::IVFRQIndex; |
| use crate::kmeans; |
| use crate::rq::{ |
| is_supported_query_bits, RQCodeFactors, RQDistanceContext, RQRotation, RaBitQuantizer, |
| DEFAULT_RQ_QUERY_BITS, RQ_BYTE_LUT_MIN_LIST_SIZE, |
| }; |
| use crate::topk::TopKHeap; |
| use roaring::RoaringTreemap; |
| use std::io; |
| |
| pub const IVF_RQ_MAGIC: u32 = 0x49565251; // "IVRQ" |
| pub const IVF_RQ_VERSION: u32 = 1; |
| pub const IVF_RQ_HEADER_SIZE: usize = 64; |
| |
| const FLAG_DELTA_IDS: u32 = 1 << 0; |
| const REQUIRED_FLAGS: u32 = FLAG_DELTA_IDS; |
| const SUPPORTED_FLAGS: u32 = REQUIRED_FLAGS; |
| const FACTOR_BYTES: usize = 12; |
| const MAX_RQ_BATCH_READ_BYTES: usize = 16 * 1024 * 1024; |
| |
| pub const IVF_RQ_NUM_BITS_ONE: u32 = 1; |
| pub const IVF_RQ_ROTATION_TYPE_KAC: u32 = 1; |
| pub const IVF_RQ_FACTOR_LAYOUT_RABITQ_V1: u32 = 1; |
| |
| const FORMAT_FLAG_EX_CODES_PRESENT: u32 = 1 << 0; |
| const FORMAT_FLAG_ERROR_FACTOR_PRESENT: u32 = 1 << 1; |
| const SUPPORTED_FORMAT_FLAGS: u32 = FORMAT_FLAG_EX_CODES_PRESENT | FORMAT_FLAG_ERROR_FACTOR_PRESENT; |
| const CURRENT_FORMAT_FLAGS: u32 = 0; |
| |
| struct SortedRQList { |
| ids: Vec<i64>, |
| id_bytes: Vec<u8>, |
| codes: Vec<u8>, |
| factors: Vec<RQCodeFactors>, |
| } |
| |
| pub fn write_ivfrq_index(index: &IVFRQIndex, out: &mut dyn SeekWrite) -> io::Result<()> { |
| validate_index_shape(index)?; |
| let d = index.d; |
| let nlist = index.nlist; |
| let code_size = index.code_size(); |
| 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::with_capacity(nlist); |
| for list_id in 0..nlist { |
| let count = index.ids[list_id].len(); |
| if count == 0 { |
| sorted_lists.push(SortedRQList { |
| ids: Vec::new(), |
| id_bytes: Vec::new(), |
| codes: Vec::new(), |
| factors: 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_codes = Vec::with_capacity(count * code_size); |
| let mut sorted_factors = Vec::with_capacity(count); |
| for idx in order { |
| sorted_codes |
| .extend_from_slice(&index.codes[list_id][idx * code_size..(idx + 1) * code_size]); |
| sorted_factors.push(index.factors[list_id][idx]); |
| } |
| let (_, id_bytes) = encode_delta_varint_ids(&sorted_ids); |
| sorted_lists.push(SortedRQList { |
| ids: sorted_ids, |
| id_bytes, |
| codes: sorted_codes, |
| factors: sorted_factors, |
| }); |
| } |
| |
| write_u32_le(out, IVF_RQ_MAGIC)?; |
| write_u32_le(out, IVF_RQ_VERSION)?; |
| write_i32_le(out, usize_to_i32(d, "dimension")?)?; |
| write_i32_le(out, usize_to_i32(nlist, "nlist")?)?; |
| write_u32_le(out, index.metric as u32)?; |
| write_u32_le(out, REQUIRED_FLAGS)?; |
| write_i64_le(out, total_vectors)?; |
| write_u64_le(out, index.rotation_seed)?; |
| write_u32_le(out, index.rotation_rounds)?; |
| write_i32_le(out, usize_to_i32(code_size, "code size")?)?; |
| write_u32_le(out, IVF_RQ_NUM_BITS_ONE)?; |
| write_u32_le(out, IVF_RQ_ROTATION_TYPE_KAC)?; |
| write_u32_le(out, IVF_RQ_FACTOR_LAYOUT_RABITQ_V1)?; |
| write_u32_le(out, CURRENT_FORMAT_FLAGS)?; |
| |
| 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-RQ 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-RQ 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].ids.len(); |
| list_counts[list_id] = usize_to_i32(count, "list count")?; |
| if count > 0 { |
| let id_bytes_len = sorted_lists[list_id].id_bytes.len(); |
| list_id_bytes_lens[list_id] = usize_to_i32(id_bytes_len, "delta ID section")?; |
| let code_bytes = checked_list_bytes(count, code_size)?; |
| let factor_bytes = checked_list_bytes(count, FACTOR_BYTES)?; |
| let list_bytes = 12usize |
| .checked_add(id_bytes_len) |
| .and_then(|len| len.checked_add(code_bytes)) |
| .and_then(|len| len.checked_add(factor_bytes)) |
| .ok_or_else(|| { |
| io::Error::new(io::ErrorKind::InvalidInput, "IVF-RQ list size overflow") |
| })?; |
| current_offset = current_offset |
| .checked_add(list_bytes as u64) |
| .ok_or_else(|| { |
| io::Error::new(io::ErrorKind::InvalidInput, "IVF-RQ 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_list in sorted_lists { |
| if sorted_list.ids.is_empty() { |
| continue; |
| } |
| write_i64_le(out, sorted_list.ids[0])?; |
| write_i32_le(out, sorted_list.id_bytes.len() as i32)?; |
| out.write_all(&sorted_list.id_bytes)?; |
| out.write_all(&sorted_list.codes)?; |
| write_factors(out, &sorted_list.factors)?; |
| } |
| |
| Ok(()) |
| } |
| |
| pub struct IVFRQIndexReader<R: SeekRead> { |
| reader: R, |
| pub d: usize, |
| pub nlist: usize, |
| pub metric: MetricType, |
| pub total_vectors: i64, |
| pub rotation_seed: u64, |
| pub rotation_rounds: u32, |
| pub code_size: usize, |
| pub num_bits: u32, |
| pub rotation_type: u32, |
| pub factor_layout: u32, |
| pub format_flags: u32, |
| pub quantizer_centroids: Vec<f32>, |
| pub list_offsets: Vec<i64>, |
| pub list_counts: Vec<i32>, |
| pub list_id_bytes_lens: Vec<i32>, |
| quantizer: RaBitQuantizer, |
| rotation: RQRotation, |
| delta_ids: bool, |
| loaded: bool, |
| } |
| |
| #[derive(Clone, Copy)] |
| struct RQListPayloadMeta { |
| list_id: usize, |
| count: usize, |
| offset: u64, |
| id_bytes_len: usize, |
| payload_len: usize, |
| } |
| |
| impl<R: SeekRead> IVFRQIndexReader<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 != IVF_RQ_MAGIC { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidData, |
| format!("Invalid IVF-RQ magic: 0x{:08X}", magic), |
| )); |
| } |
| let version = read_u32_le(&mut cursor)?; |
| if version != IVF_RQ_VERSION { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidData, |
| format!("Unsupported IVF-RQ 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 flags = read_u32_le(&mut cursor)?; |
| let total_vectors = read_i64_le(&mut cursor)?; |
| if total_vectors < 0 { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidData, |
| format!("negative total vector count: {}", total_vectors), |
| )); |
| } |
| let rotation_seed = read_u64_le(&mut cursor)?; |
| let rotation_rounds = read_u32_le(&mut cursor)?; |
| let code_size = validate_positive_i32(read_i32_le(&mut cursor)?, "code_size")? as usize; |
| let num_bits = read_u32_le(&mut cursor)?; |
| let rotation_type = read_u32_le(&mut cursor)?; |
| let factor_layout = read_u32_le(&mut cursor)?; |
| let format_flags = read_u32_le(&mut cursor)?; |
| let expected_code_size = d.div_ceil(8); |
| if code_size != expected_code_size { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidData, |
| format!( |
| "IVF-RQ code_size {} does not match dimension-derived size {}", |
| code_size, expected_code_size |
| ), |
| )); |
| } |
| validate_rq_format(num_bits, rotation_type, factor_layout, format_flags)?; |
| |
| let unknown_flags = flags & !SUPPORTED_FLAGS; |
| if unknown_flags != 0 { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidData, |
| format!("Unsupported IVF-RQ flags: 0x{:08X}", unknown_flags), |
| )); |
| } |
| if flags & REQUIRED_FLAGS != REQUIRED_FLAGS { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidData, |
| "IVF-RQ v1 requires delta IDs", |
| )); |
| } |
| |
| Ok(Self { |
| reader, |
| d, |
| nlist, |
| metric, |
| total_vectors, |
| rotation_seed, |
| rotation_rounds, |
| code_size, |
| num_bits, |
| rotation_type, |
| factor_layout, |
| format_flags, |
| quantizer_centroids: Vec::new(), |
| list_offsets: Vec::new(), |
| list_counts: Vec::new(), |
| list_id_bytes_lens: Vec::new(), |
| quantizer: RaBitQuantizer::new(d), |
| rotation: RQRotation::new(d, rotation_seed, rotation_rounds), |
| 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, IVF_RQ_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<RQReadList> { |
| self.ensure_loaded()?; |
| let Some(meta) = self.list_payload_meta(list_id)? else { |
| return Ok(RQReadList { |
| list_id, |
| ids: Vec::new(), |
| codes: Vec::new(), |
| factors: Vec::new(), |
| }); |
| }; |
| let mut payload = vec![0u8; meta.payload_len]; |
| self.reader |
| .pread(&mut [ReadRequest::new(meta.offset, &mut payload)])?; |
| self.decode_inverted_list_payload(meta, &payload) |
| } |
| |
| fn read_inverted_lists(&mut self, list_ids: &[usize]) -> io::Result<Vec<RQReadList>> { |
| self.ensure_loaded()?; |
| |
| let mut results: Vec<Option<RQReadList>> = (0..list_ids.len()).map(|_| None).collect(); |
| let mut metas = Vec::new(); |
| let mut payloads = Vec::new(); |
| |
| for (input_index, &list_id) in list_ids.iter().enumerate() { |
| let Some(meta) = self.list_payload_meta(list_id)? else { |
| results[input_index] = Some(RQReadList { |
| list_id, |
| ids: Vec::new(), |
| codes: Vec::new(), |
| factors: Vec::new(), |
| }); |
| continue; |
| }; |
| metas.push((input_index, meta)); |
| payloads.push(vec![0u8; meta.payload_len]); |
| } |
| |
| if !metas.is_empty() { |
| { |
| let mut requests: Vec<_> = payloads |
| .iter_mut() |
| .zip(metas.iter()) |
| .map(|(payload, (_, meta))| { |
| ReadRequest::new(meta.offset, payload.as_mut_slice()) |
| }) |
| .collect(); |
| self.reader.pread(&mut requests)?; |
| } |
| |
| for ((input_index, meta), payload) in metas.into_iter().zip(payloads) { |
| results[input_index] = Some(self.decode_inverted_list_payload(meta, &payload)?); |
| } |
| } |
| |
| results |
| .into_iter() |
| .map(|result| { |
| result.ok_or_else(|| { |
| io::Error::new( |
| io::ErrorKind::InvalidData, |
| "missing IVF-RQ inverted list read result", |
| ) |
| }) |
| }) |
| .collect() |
| } |
| |
| fn batch_read_end(&self, list_ids: &[usize]) -> io::Result<usize> { |
| let mut payload_bytes = 0usize; |
| for (index, &list_id) in list_ids.iter().enumerate() { |
| let Some(meta) = self.list_payload_meta(list_id)? else { |
| continue; |
| }; |
| let next_payload_bytes = |
| payload_bytes.checked_add(meta.payload_len).ok_or_else(|| { |
| io::Error::new( |
| io::ErrorKind::InvalidData, |
| "IVF-RQ batched payload size overflow", |
| ) |
| })?; |
| if index > 0 && next_payload_bytes > MAX_RQ_BATCH_READ_BYTES { |
| return Ok(index); |
| } |
| payload_bytes = next_payload_bytes; |
| } |
| Ok(list_ids.len()) |
| } |
| |
| fn list_payload_meta(&self, list_id: usize) -> io::Result<Option<RQListPayloadMeta>> { |
| 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(None); |
| } |
| if !self.delta_ids { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidData, |
| "IVF-RQ reader only supports delta IDs", |
| )); |
| } |
| let offset = checked_list_offset(self.list_offsets[list_id], list_id)?; |
| let id_bytes_len = self.list_id_bytes_lens[list_id] as usize; |
| let code_bytes = checked_list_bytes(count, self.code_size)?; |
| let factor_bytes = checked_list_bytes(count, FACTOR_BYTES)?; |
| let payload_len = 12usize |
| .checked_add(id_bytes_len) |
| .and_then(|len| len.checked_add(code_bytes)) |
| .and_then(|len| len.checked_add(factor_bytes)) |
| .ok_or_else(|| { |
| io::Error::new(io::ErrorKind::InvalidData, "IVF-RQ list payload overflow") |
| })?; |
| Ok(Some(RQListPayloadMeta { |
| list_id, |
| count, |
| offset, |
| id_bytes_len, |
| payload_len, |
| })) |
| } |
| |
| fn decode_inverted_list_payload( |
| &self, |
| meta: RQListPayloadMeta, |
| payload: &[u8], |
| ) -> io::Result<RQReadList> { |
| if payload.len() != meta.payload_len { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidData, |
| format!( |
| "IVF-RQ list {} payload length {} does not match expected {}", |
| meta.list_id, |
| payload.len(), |
| meta.payload_len |
| ), |
| )); |
| } |
| 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 != meta.id_bytes_len { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidData, |
| "IVF-RQ id_bytes_len mismatch", |
| )); |
| } |
| let ids = |
| decode_delta_varint_ids(base_id, &payload[12..12 + meta.id_bytes_len], meta.count)?; |
| let code_start = 12 + meta.id_bytes_len; |
| let code_bytes = checked_list_bytes(meta.count, self.code_size)?; |
| let factor_start = code_start + code_bytes; |
| Ok(RQReadList { |
| list_id: meta.list_id, |
| ids, |
| codes: payload[code_start..factor_start].to_vec(), |
| factors: bytes_to_factors(&payload[factor_start..])?, |
| }) |
| } |
| |
| pub fn search( |
| &mut self, |
| query: &[f32], |
| k: usize, |
| nprobe: usize, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| self.search_with_query_bits(query, k, nprobe, DEFAULT_RQ_QUERY_BITS) |
| } |
| |
| pub fn search_with_query_bits( |
| &mut self, |
| query: &[f32], |
| k: usize, |
| nprobe: usize, |
| query_bits: usize, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| self.search_with_filter(query, k, nprobe, None, query_bits) |
| } |
| |
| pub fn search_with_filter( |
| &mut self, |
| query: &[f32], |
| k: usize, |
| nprobe: usize, |
| filter: Option<&dyn RowIdFilter>, |
| query_bits: usize, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| search_batch_ivfrq_reader_filter_with_query_bits( |
| self, query, 1, k, nprobe, filter, query_bits, |
| ) |
| } |
| |
| 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), DEFAULT_RQ_QUERY_BITS) |
| } |
| |
| pub fn search_with_roaring_filter_and_query_bits( |
| &mut self, |
| query: &[f32], |
| k: usize, |
| nprobe: usize, |
| roaring_filter_bytes: &[u8], |
| query_bits: usize, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| let filter = decode_roaring_filter(roaring_filter_bytes)?; |
| self.search_with_filter(query, k, nprobe, Some(&filter), query_bits) |
| } |
| } |
| |
| pub struct RQReadList { |
| pub list_id: usize, |
| pub ids: Vec<i64>, |
| pub codes: Vec<u8>, |
| pub factors: Vec<RQCodeFactors>, |
| } |
| |
| pub fn search_batch_ivfrq_reader<R: SeekRead>( |
| reader: &mut IVFRQIndexReader<R>, |
| queries: &[f32], |
| nq: usize, |
| k: usize, |
| nprobe: usize, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| search_batch_ivfrq_reader_with_query_bits(reader, queries, nq, k, nprobe, DEFAULT_RQ_QUERY_BITS) |
| } |
| |
| pub fn search_batch_ivfrq_reader_with_query_bits<R: SeekRead>( |
| reader: &mut IVFRQIndexReader<R>, |
| queries: &[f32], |
| nq: usize, |
| k: usize, |
| nprobe: usize, |
| query_bits: usize, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| search_batch_ivfrq_reader_filter_with_query_bits( |
| reader, queries, nq, k, nprobe, None, query_bits, |
| ) |
| } |
| |
| pub fn search_batch_ivfrq_reader_filter<R: SeekRead>( |
| reader: &mut IVFRQIndexReader<R>, |
| queries: &[f32], |
| nq: usize, |
| k: usize, |
| nprobe: usize, |
| filter: Option<&dyn RowIdFilter>, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| search_batch_ivfrq_reader_filter_with_query_bits( |
| reader, |
| queries, |
| nq, |
| k, |
| nprobe, |
| filter, |
| DEFAULT_RQ_QUERY_BITS, |
| ) |
| } |
| |
| pub fn search_batch_ivfrq_reader_filter_with_query_bits<R: SeekRead>( |
| reader: &mut IVFRQIndexReader<R>, |
| queries: &[f32], |
| nq: usize, |
| k: usize, |
| nprobe: usize, |
| filter: Option<&dyn RowIdFilter>, |
| query_bits: usize, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| reader.ensure_loaded()?; |
| validate_search_inputs(queries, nq, reader.d, k, nprobe)?; |
| validate_query_bits(query_bits)?; |
| |
| let processed = preprocess_vectors(queries, nq, reader.d, reader.metric); |
| let (all_probe_indices, _) = kmeans::find_topk_batch( |
| &processed, |
| nq, |
| &reader.quantizer_centroids, |
| reader.nlist, |
| reader.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<TopKHeap> = (0..nq).map(|_| TopKHeap::new(k)).collect(); |
| let mut batch_start = 0; |
| while batch_start < unique_lists.len() { |
| let batch_end = batch_start + reader.batch_read_end(&unique_lists[batch_start..])?; |
| for read_list in reader.read_inverted_lists(&unique_lists[batch_start..batch_end])? { |
| let list_id = read_list.list_id; |
| let count = read_list.ids.len(); |
| if count == 0 { |
| continue; |
| } |
| let centroid = |
| &reader.quantizer_centroids[list_id * reader.d..(list_id + 1) * reader.d]; |
| for &qi in &list_to_queries[list_id] { |
| let query = &processed[qi * reader.d..(qi + 1) * reader.d]; |
| let rotated_query_residual = |
| rotated_residual(query, centroid, reader.d, &reader.rotation); |
| let distance_context = reader.quantizer.prepare_distance_context_with_query_bits( |
| rotated_query_residual, |
| query, |
| count >= RQ_BYTE_LUT_MIN_LIST_SIZE, |
| query_bits, |
| ); |
| scan_read_list( |
| &read_list, |
| &reader.quantizer, |
| reader.code_size, |
| reader.metric, |
| &distance_context, |
| filter, |
| &mut heaps[qi], |
| ); |
| } |
| } |
| batch_start = batch_end; |
| } |
| |
| 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], TopKHeap::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_ivfrq_reader_roaring_filter<R: SeekRead>( |
| reader: &mut IVFRQIndexReader<R>, |
| queries: &[f32], |
| nq: usize, |
| k: usize, |
| nprobe: usize, |
| roaring_filter_bytes: &[u8], |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| search_batch_ivfrq_reader_roaring_filter_with_query_bits( |
| reader, |
| queries, |
| nq, |
| k, |
| nprobe, |
| roaring_filter_bytes, |
| DEFAULT_RQ_QUERY_BITS, |
| ) |
| } |
| |
| pub fn search_batch_ivfrq_reader_roaring_filter_with_query_bits<R: SeekRead>( |
| reader: &mut IVFRQIndexReader<R>, |
| queries: &[f32], |
| nq: usize, |
| k: usize, |
| nprobe: usize, |
| roaring_filter_bytes: &[u8], |
| query_bits: usize, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| let filter = decode_roaring_filter(roaring_filter_bytes)?; |
| search_batch_ivfrq_reader_filter_with_query_bits( |
| reader, |
| queries, |
| nq, |
| k, |
| nprobe, |
| Some(&filter), |
| query_bits, |
| ) |
| } |
| |
| fn scan_read_list( |
| read_list: &RQReadList, |
| quantizer: &RaBitQuantizer, |
| code_size: usize, |
| metric: MetricType, |
| distance_context: &RQDistanceContext, |
| filter: Option<&dyn RowIdFilter>, |
| heap: &mut TopKHeap, |
| ) { |
| for (local_idx, &id) in read_list.ids.iter().enumerate() { |
| if filter.map(|f| !f.contains(id)).unwrap_or(false) { |
| continue; |
| } |
| let code = &read_list.codes[local_idx * code_size..(local_idx + 1) * code_size]; |
| let dist = quantizer.distance_to_code_prepared( |
| distance_context, |
| code, |
| read_list.factors[local_idx], |
| metric, |
| ); |
| heap.push(dist, id); |
| } |
| } |
| |
| fn rotated_residual(query: &[f32], centroid: &[f32], d: usize, rotation: &RQRotation) -> Vec<f32> { |
| let mut residual = vec![0.0f32; d]; |
| fvec_madd(query, centroid, -1.0, &mut residual); |
| rotation.apply(&mut residual); |
| residual |
| } |
| |
| fn validate_query_bits(query_bits: usize) -> io::Result<()> { |
| if is_supported_query_bits(query_bits) { |
| Ok(()) |
| } else { |
| Err(io::Error::new( |
| io::ErrorKind::InvalidInput, |
| format!( |
| "invalid IVF-RQ query_bits {}; expected 0, 4, or 8", |
| query_bits |
| ), |
| )) |
| } |
| } |
| |
| fn validate_rq_format( |
| num_bits: u32, |
| rotation_type: u32, |
| factor_layout: u32, |
| format_flags: u32, |
| ) -> io::Result<()> { |
| if !matches!(num_bits, 1 | 2 | 4 | 8) { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidData, |
| format!( |
| "unsupported IVF-RQ num_bits {}; expected 1, 2, 4, or 8", |
| num_bits |
| ), |
| )); |
| } |
| if rotation_type != IVF_RQ_ROTATION_TYPE_KAC { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidData, |
| format!( |
| "unsupported IVF-RQ rotation_type {}; expected {}", |
| rotation_type, IVF_RQ_ROTATION_TYPE_KAC |
| ), |
| )); |
| } |
| if factor_layout != IVF_RQ_FACTOR_LAYOUT_RABITQ_V1 { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidData, |
| format!( |
| "unsupported IVF-RQ factor_layout {}; expected {}", |
| factor_layout, IVF_RQ_FACTOR_LAYOUT_RABITQ_V1 |
| ), |
| )); |
| } |
| let unknown_format_flags = format_flags & !SUPPORTED_FORMAT_FLAGS; |
| if unknown_format_flags != 0 { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidData, |
| format!( |
| "unsupported IVF-RQ format_flags: 0x{:08X}", |
| unknown_format_flags |
| ), |
| )); |
| } |
| if num_bits != IVF_RQ_NUM_BITS_ONE || format_flags != CURRENT_FORMAT_FLAGS { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidData, |
| format!( |
| "IVF-RQ reader currently supports only num_bits=1 without optional sections; got num_bits={}, format_flags=0x{:08X}", |
| num_bits, format_flags |
| ), |
| )); |
| } |
| Ok(()) |
| } |
| |
| fn validate_index_shape(index: &IVFRQIndex) -> 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.codes.len() != index.nlist |
| || index.factors.len() != index.nlist |
| { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidInput, |
| "IVF-RQ 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 |
| ), |
| )); |
| } |
| let code_size = index.code_size(); |
| for list_id in 0..index.nlist { |
| let count = index.ids[list_id].len(); |
| let expected_code_len = checked_list_bytes(count, code_size)?; |
| if index.codes[list_id].len() != expected_code_len { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidInput, |
| format!( |
| "list {} code length {} does not match ids*code_size {}", |
| list_id, |
| index.codes[list_id].len(), |
| expected_code_len |
| ), |
| )); |
| } |
| if index.factors[list_id].len() != count { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidInput, |
| format!( |
| "list {} factor count {} does not match ids {}", |
| list_id, |
| index.factors[list_id].len(), |
| count |
| ), |
| )); |
| } |
| } |
| Ok(()) |
| } |
| |
| fn write_factors(out: &mut dyn SeekWrite, factors: &[RQCodeFactors]) -> io::Result<()> { |
| let mut bytes = Vec::with_capacity(factors.len() * FACTOR_BYTES); |
| for factor in factors { |
| bytes.extend_from_slice(&factor.residual_norm_sqr.to_le_bytes()); |
| bytes.extend_from_slice(&factor.vector_norm_sqr.to_le_bytes()); |
| bytes.extend_from_slice(&factor.dp_multiplier.to_le_bytes()); |
| } |
| out.write_all(&bytes) |
| } |
| |
| fn bytes_to_factors(bytes: &[u8]) -> io::Result<Vec<RQCodeFactors>> { |
| if !bytes.len().is_multiple_of(FACTOR_BYTES) { |
| return Err(io::Error::new( |
| io::ErrorKind::InvalidData, |
| "IVF-RQ factor section is not 12-byte aligned", |
| )); |
| } |
| Ok(bytes |
| .chunks_exact(FACTOR_BYTES) |
| .map(|chunk| RQCodeFactors { |
| residual_norm_sqr: f32::from_le_bytes(chunk[0..4].try_into().unwrap()), |
| vector_norm_sqr: f32::from_le_bytes(chunk[4..8].try_into().unwrap()), |
| dp_multiplier: f32::from_le_bytes(chunk[8..12].try_into().unwrap()), |
| }) |
| .collect()) |
| } |
| |
| 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_u64_le(out: &mut dyn SeekWrite, v: u64) -> 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 read_u64_le<R: SeekRead + ?Sized>(reader: &mut PreadCursor<'_, R>) -> io::Result<u64> { |
| let mut buf = [0u8; 8]; |
| reader.read_exact(&mut buf)?; |
| Ok(u64::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 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-RQ 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-RQ 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 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::io::{PosWriter, ReadRequest, SeekRead}; |
| use std::io::{Cursor, Read, Seek, SeekFrom}; |
| use std::sync::{Arc, Mutex}; |
| |
| #[derive(Default)] |
| struct ReaderStats { |
| max_ranges_per_batch: usize, |
| } |
| |
| struct CountingPreadCursor { |
| inner: Cursor<Vec<u8>>, |
| stats: Arc<Mutex<ReaderStats>>, |
| } |
| |
| impl CountingPreadCursor { |
| fn new(data: Vec<u8>, stats: Arc<Mutex<ReaderStats>>) -> Self { |
| Self { |
| inner: Cursor::new(data), |
| stats, |
| } |
| } |
| } |
| |
| impl SeekRead for CountingPreadCursor { |
| fn pread(&mut self, ranges: &mut [ReadRequest<'_>]) -> io::Result<()> { |
| let mut stats = self.stats.lock().unwrap(); |
| stats.max_ranges_per_batch = stats.max_ranges_per_batch.max(ranges.len()); |
| drop(stats); |
| for range in ranges { |
| self.inner.seek(SeekFrom::Start(range.pos))?; |
| self.inner.read_exact(range.buf)?; |
| } |
| Ok(()) |
| } |
| } |
| |
| #[test] |
| fn ivfrq_write_read_search_roundtrip() { |
| let d = 8; |
| 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, 4.0, 5.0, 6.0, 7.0] |
| }) |
| .collect(); |
| let ids: Vec<i64> = (1000..1000 + n as i64).collect(); |
| let mut index = IVFRQIndex::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_ivfrq_index(&index, &mut writer).unwrap(); |
| |
| let mut reader = IVFRQIndexReader::open(Cursor::new(buf)).unwrap(); |
| let (labels, distances) = reader.search(&data[7 * d..8 * d], 5, nlist).unwrap(); |
| |
| assert_eq!(labels[0], ids[7]); |
| assert!(distances[0] <= 1e-4); |
| } |
| |
| #[test] |
| fn ivfrq_reader_search_with_query_bits() { |
| let d = 16; |
| let nlist = 4; |
| let n = 128; |
| let data: Vec<f32> = (0..n) |
| .flat_map(|i| { |
| let cluster = (i % nlist) as f32 * 25.0; |
| (0..d).map(move |j| cluster + i as f32 * 0.02 + j as f32 * 0.125) |
| }) |
| .collect(); |
| let ids: Vec<i64> = (1000..1000 + n as i64).collect(); |
| let mut index = IVFRQIndex::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_ivfrq_index(&index, &mut writer).unwrap(); |
| |
| let query = &data[37 * d..38 * d]; |
| for query_bits in [0, 4, 8] { |
| let mut reader = IVFRQIndexReader::open(Cursor::new(buf.clone())).unwrap(); |
| let (labels, distances) = reader |
| .search_with_query_bits(query, 5, nlist, query_bits) |
| .unwrap(); |
| assert_eq!(labels[0], ids[37], "query_bits={}", query_bits); |
| if query_bits == 0 { |
| assert!(distances[0] <= 1e-3); |
| } else { |
| assert!(distances[0].is_finite()); |
| } |
| } |
| } |
| |
| #[test] |
| fn ivfrq_batch_reader_reads_lists_in_one_pread_batch() { |
| let mut index = IVFRQIndex::new(8, 3, MetricType::L2); |
| index.quantizer_centroids = vec![0.0; index.nlist * index.d]; |
| index.ids = vec![vec![10], vec![20], vec![30]]; |
| index.codes = vec![vec![0b0000_0001], vec![0b0000_0010], vec![0b0000_0100]]; |
| index.factors = vec![ |
| vec![RQCodeFactors::zero()], |
| vec![RQCodeFactors::zero()], |
| vec![RQCodeFactors::zero()], |
| ]; |
| |
| let mut bytes = Vec::new(); |
| write_ivfrq_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let stats = Arc::new(Mutex::new(ReaderStats::default())); |
| let stream = CountingPreadCursor::new(bytes, Arc::clone(&stats)); |
| let mut reader = IVFRQIndexReader::open(stream).unwrap(); |
| |
| let (ids, _) = search_batch_ivfrq_reader(&mut reader, &[0.0; 8], 1, 1, 3).unwrap(); |
| |
| assert!(ids[0] >= 0); |
| assert_eq!(stats.lock().unwrap().max_ranges_per_batch, 3); |
| } |
| |
| #[test] |
| fn ivfrq_reader_rejects_invalid_query_bits() { |
| let d = 8; |
| let nlist = 1; |
| let n = 8; |
| let data: Vec<f32> = (0..n) |
| .flat_map(|i| (0..d).map(move |j| i as f32 + j as f32 * 0.25)) |
| .collect(); |
| let ids: Vec<i64> = (0..n as i64).collect(); |
| let mut index = IVFRQIndex::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_ivfrq_index(&index, &mut writer).unwrap(); |
| |
| let mut reader = IVFRQIndexReader::open(Cursor::new(buf)).unwrap(); |
| let err = reader |
| .search_with_query_bits(&data[0..d], 1, nlist, 7) |
| .unwrap_err(); |
| assert!(err.to_string().contains("query_bits")); |
| } |
| |
| #[test] |
| fn ivfrq_header_records_current_rq_format_fields() { |
| let index = tiny_ivfrq_index(); |
| |
| let mut buf = Vec::new(); |
| let mut writer = PosWriter::new(&mut buf); |
| write_ivfrq_index(&index, &mut writer).unwrap(); |
| |
| assert_eq!( |
| u32::from_le_bytes(buf[48..52].try_into().unwrap()), |
| IVF_RQ_NUM_BITS_ONE |
| ); |
| assert_eq!( |
| u32::from_le_bytes(buf[52..56].try_into().unwrap()), |
| IVF_RQ_ROTATION_TYPE_KAC |
| ); |
| assert_eq!( |
| u32::from_le_bytes(buf[56..60].try_into().unwrap()), |
| IVF_RQ_FACTOR_LAYOUT_RABITQ_V1 |
| ); |
| assert_eq!(u32::from_le_bytes(buf[60..64].try_into().unwrap()), 0); |
| |
| let reader = IVFRQIndexReader::open(Cursor::new(buf)).unwrap(); |
| assert_eq!(reader.num_bits, IVF_RQ_NUM_BITS_ONE); |
| assert_eq!(reader.rotation_type, IVF_RQ_ROTATION_TYPE_KAC); |
| assert_eq!(reader.factor_layout, IVF_RQ_FACTOR_LAYOUT_RABITQ_V1); |
| assert_eq!(reader.format_flags, 0); |
| } |
| |
| #[test] |
| fn ivfrq_reader_rejects_reserved_future_format_fields() { |
| let index = tiny_ivfrq_index(); |
| |
| let mut buf = Vec::new(); |
| let mut writer = PosWriter::new(&mut buf); |
| write_ivfrq_index(&index, &mut writer).unwrap(); |
| |
| let mut future_num_bits = buf.clone(); |
| future_num_bits[48..52].copy_from_slice(&2u32.to_le_bytes()); |
| let err = open_err(future_num_bits); |
| assert!(err.to_string().contains("num_bits=1")); |
| |
| let mut unknown_rotation = buf.clone(); |
| unknown_rotation[52..56].copy_from_slice(&99u32.to_le_bytes()); |
| let err = open_err(unknown_rotation); |
| assert!(err.to_string().contains("rotation_type")); |
| |
| let mut unknown_layout = buf.clone(); |
| unknown_layout[56..60].copy_from_slice(&99u32.to_le_bytes()); |
| let err = open_err(unknown_layout); |
| assert!(err.to_string().contains("factor_layout")); |
| |
| let mut optional_sections = buf; |
| optional_sections[60..64].copy_from_slice(&FORMAT_FLAG_EX_CODES_PRESENT.to_le_bytes()); |
| let err = open_err(optional_sections); |
| assert!(err.to_string().contains("optional sections")); |
| } |
| |
| fn open_err(buf: Vec<u8>) -> io::Error { |
| match IVFRQIndexReader::open(Cursor::new(buf)) { |
| Ok(_) => panic!("expected IVF-RQ open to fail"), |
| Err(err) => err, |
| } |
| } |
| |
| fn tiny_ivfrq_index() -> IVFRQIndex { |
| let d = 8; |
| let nlist = 2; |
| let data: Vec<f32> = vec![ |
| 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, |
| ]; |
| let ids = vec![7, 42]; |
| let mut index = IVFRQIndex::new(d, nlist, MetricType::L2); |
| index.train(&data, 2); |
| index.add(&data, &ids, 2); |
| index |
| } |
| } |