1pub const IVF_INDEX_MAGIC: &[u8; 4] = b"IVF1";
22
23#[derive(Debug, Clone, PartialEq)]
25pub struct IvfListLayout {
26 pub centroid: Vec<f32>,
28 pub ids: Vec<u64>,
30 pub vectors: Vec<Vec<f32>>,
32}
33
34#[derive(Debug, Clone, PartialEq)]
36pub struct IvfIndexLayout {
37 pub n_lists: usize,
39 pub n_probes: usize,
41 pub dimension: usize,
43 pub max_iterations: usize,
45 pub convergence_threshold: f32,
47 pub trained: bool,
49 pub count: usize,
51 pub next_id: u64,
53 pub lists: Vec<IvfListLayout>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum IvfCodecError {
60 TooShort,
62 InvalidMagic,
64 Truncated,
66}
67
68impl std::fmt::Display for IvfCodecError {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 match self {
71 IvfCodecError::TooShort => write!(f, "data too short"),
72 IvfCodecError::InvalidMagic => write!(f, "invalid IVF magic"),
73 IvfCodecError::Truncated => write!(f, "truncated IVF payload"),
74 }
75 }
76}
77
78impl std::error::Error for IvfCodecError {}
79
80pub fn encode_ivf_index(layout: &IvfIndexLayout) -> Vec<u8> {
82 let mut bytes = Vec::new();
83 bytes.extend_from_slice(IVF_INDEX_MAGIC);
84 bytes.extend_from_slice(&(layout.n_lists as u32).to_le_bytes());
85 bytes.extend_from_slice(&(layout.n_probes as u32).to_le_bytes());
86 bytes.extend_from_slice(&(layout.dimension as u32).to_le_bytes());
87 bytes.extend_from_slice(&(layout.max_iterations as u32).to_le_bytes());
88 bytes.extend_from_slice(&layout.convergence_threshold.to_le_bytes());
89 bytes.push(if layout.trained { 1 } else { 0 });
90 bytes.extend_from_slice(&(layout.count as u64).to_le_bytes());
91 bytes.extend_from_slice(&layout.next_id.to_le_bytes());
92 bytes.extend_from_slice(&(layout.lists.len() as u32).to_le_bytes());
93
94 for list in &layout.lists {
95 bytes.extend_from_slice(&(list.centroid.len() as u32).to_le_bytes());
96 for value in &list.centroid {
97 bytes.extend_from_slice(&value.to_le_bytes());
98 }
99
100 bytes.extend_from_slice(&(list.ids.len() as u32).to_le_bytes());
101 for id in &list.ids {
102 bytes.extend_from_slice(&id.to_le_bytes());
103 }
104
105 bytes.extend_from_slice(&(list.vectors.len() as u32).to_le_bytes());
106 for vector in &list.vectors {
107 bytes.extend_from_slice(&(vector.len() as u32).to_le_bytes());
108 for value in vector {
109 bytes.extend_from_slice(&value.to_le_bytes());
110 }
111 }
112 }
113
114 bytes
115}
116
117fn read_u32(buf: &[u8], pos: &mut usize) -> Result<u32, IvfCodecError> {
118 if *pos + 4 > buf.len() {
119 return Err(IvfCodecError::Truncated);
120 }
121 let value = u32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
122 *pos += 4;
123 Ok(value)
124}
125
126fn read_u64(buf: &[u8], pos: &mut usize) -> Result<u64, IvfCodecError> {
127 if *pos + 8 > buf.len() {
128 return Err(IvfCodecError::Truncated);
129 }
130 let value = u64::from_le_bytes([
131 buf[*pos],
132 buf[*pos + 1],
133 buf[*pos + 2],
134 buf[*pos + 3],
135 buf[*pos + 4],
136 buf[*pos + 5],
137 buf[*pos + 6],
138 buf[*pos + 7],
139 ]);
140 *pos += 8;
141 Ok(value)
142}
143
144fn read_f32(buf: &[u8], pos: &mut usize) -> Result<f32, IvfCodecError> {
145 if *pos + 4 > buf.len() {
146 return Err(IvfCodecError::Truncated);
147 }
148 let value = f32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
149 *pos += 4;
150 Ok(value)
151}
152
153pub fn decode_ivf_index(bytes: &[u8]) -> Result<IvfIndexLayout, IvfCodecError> {
156 if bytes.len() < 41 {
157 return Err(IvfCodecError::TooShort);
158 }
159 if &bytes[0..4] != IVF_INDEX_MAGIC {
160 return Err(IvfCodecError::InvalidMagic);
161 }
162
163 let mut pos = 4usize;
164 let n_lists = read_u32(bytes, &mut pos)? as usize;
165 let n_probes = read_u32(bytes, &mut pos)? as usize;
166 let dimension = read_u32(bytes, &mut pos)? as usize;
167 let max_iterations = read_u32(bytes, &mut pos)? as usize;
168 let convergence_threshold = read_f32(bytes, &mut pos)?;
169
170 if pos >= bytes.len() {
171 return Err(IvfCodecError::Truncated);
172 }
173 let trained = bytes[pos] == 1;
174 pos += 1;
175 let count = read_u64(bytes, &mut pos)? as usize;
176 let next_id = read_u64(bytes, &mut pos)?;
177 let list_count = read_u32(bytes, &mut pos)? as usize;
178
179 let mut lists = Vec::with_capacity(list_count);
180 for _ in 0..list_count {
181 let centroid_len = read_u32(bytes, &mut pos)? as usize;
182 let mut centroid = Vec::with_capacity(centroid_len);
183 for _ in 0..centroid_len {
184 centroid.push(read_f32(bytes, &mut pos)?);
185 }
186
187 let id_count = read_u32(bytes, &mut pos)? as usize;
188 let mut ids = Vec::with_capacity(id_count);
189 for _ in 0..id_count {
190 ids.push(read_u64(bytes, &mut pos)?);
191 }
192
193 let vector_count = read_u32(bytes, &mut pos)? as usize;
194 let mut vectors = Vec::with_capacity(vector_count);
195 for _ in 0..vector_count {
196 let vector_len = read_u32(bytes, &mut pos)? as usize;
197 let mut vector = Vec::with_capacity(vector_len);
198 for _ in 0..vector_len {
199 vector.push(read_f32(bytes, &mut pos)?);
200 }
201 vectors.push(vector);
202 }
203
204 lists.push(IvfListLayout {
205 centroid,
206 ids,
207 vectors,
208 });
209 }
210
211 Ok(IvfIndexLayout {
212 n_lists,
213 n_probes,
214 dimension,
215 max_iterations,
216 convergence_threshold,
217 trained,
218 count,
219 next_id,
220 lists,
221 })
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 fn sample() -> IvfIndexLayout {
229 IvfIndexLayout {
230 n_lists: 4,
231 n_probes: 2,
232 dimension: 3,
233 max_iterations: 50,
234 convergence_threshold: 1e-4,
235 trained: true,
236 count: 3,
237 next_id: 3,
238 lists: vec![
239 IvfListLayout {
240 centroid: vec![0.0, 1.0, 2.0],
241 ids: vec![0, 2],
242 vectors: vec![vec![0.0, 1.0, 2.0], vec![0.1, 1.1, 2.1]],
243 },
244 IvfListLayout {
245 centroid: vec![9.0, 9.0, 9.0],
246 ids: vec![1],
247 vectors: vec![vec![9.0, 9.0, 9.0]],
248 },
249 ],
250 }
251 }
252
253 #[test]
254 fn round_trip_preserves_layout() {
255 let layout = sample();
256 let bytes = encode_ivf_index(&layout);
257 let decoded = decode_ivf_index(&bytes).expect("decode");
258 assert_eq!(decoded, layout);
259 }
260
261 #[test]
262 fn fixture_bytes_are_byte_identical() {
263 let layout = sample();
264 let bytes = encode_ivf_index(&layout);
265 assert_eq!(&bytes[0..4], b"IVF1", "magic must lead the payload");
266 assert_eq!(&bytes[4..8], &4u32.to_le_bytes());
268 assert_eq!(bytes[24], 1);
270 }
271
272 #[test]
273 fn rejects_short_and_bad_magic() {
274 assert_eq!(decode_ivf_index(&[0u8; 10]), Err(IvfCodecError::TooShort));
275 let mut bytes = encode_ivf_index(&sample());
276 bytes[0] = b'X';
277 assert_eq!(decode_ivf_index(&bytes), Err(IvfCodecError::InvalidMagic));
278 }
279}