1use std::io::{self, Write};
21
22use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
23use rustc_hash::FxHashMap;
24
25use crate::DocId;
26use crate::directories::OwnedBytes;
27
28const MAGIC: u32 = 0x4B4E_4843; const VERSION: u32 = 1;
30const HEADER_SIZE: usize = 12;
31const TOC_ENTRY_SIZE: usize = 24;
32
33pub const MAX_CHUNK_LENGTH: u32 = u16::MAX as u32;
35
36#[derive(Debug, Default, Clone)]
38pub struct ChunkMapBuilder {
39 doc_ids: Vec<DocId>,
40 ordinals: Vec<u16>,
41 lengths: Vec<u16>,
42 total_tokens: u64,
43}
44
45impl ChunkMapBuilder {
46 pub fn len(&self) -> usize {
48 self.doc_ids.len()
49 }
50
51 pub fn is_empty(&self) -> bool {
52 self.doc_ids.is_empty()
53 }
54
55 pub fn push(&mut self, doc_id: DocId, ordinal: u16, token_count: u32) -> io::Result<u32> {
57 let vid = u32::try_from(self.doc_ids.len()).map_err(|_| {
58 io::Error::new(
59 io::ErrorKind::InvalidData,
60 "chunked text field exceeds u32::MAX chunks in one segment",
61 )
62 })?;
63 self.doc_ids.push(doc_id);
64 self.ordinals.push(ordinal);
65 self.lengths.push(token_count.min(MAX_CHUNK_LENGTH) as u16);
66 self.total_tokens += u64::from(token_count);
67 Ok(vid)
68 }
69
70 pub fn estimated_bytes(&self) -> usize {
72 self.doc_ids.capacity() * 4 + self.ordinals.capacity() * 2 + self.lengths.capacity() * 2
73 }
74
75 fn section_bytes(&self) -> u64 {
76 self.doc_ids.len() as u64 * 8
77 }
78}
79
80pub fn write_chunk_maps<W: Write + ?Sized>(
84 writer: &mut W,
85 fields: &[(u32, &ChunkMapBuilder)],
86) -> io::Result<u64> {
87 let mut offset = (HEADER_SIZE + TOC_ENTRY_SIZE * fields.len()) as u64;
88 writer.write_u32::<LittleEndian>(MAGIC)?;
89 writer.write_u32::<LittleEndian>(VERSION)?;
90 writer.write_u32::<LittleEndian>(fields.len() as u32)?;
91 for (field_id, map) in fields {
92 writer.write_u32::<LittleEndian>(*field_id)?;
93 writer.write_u32::<LittleEndian>(map.len() as u32)?;
94 writer.write_u64::<LittleEndian>(map.total_tokens)?;
95 writer.write_u64::<LittleEndian>(offset)?;
96 offset += map.section_bytes();
97 }
98 for (_, map) in fields {
99 for doc_id in &map.doc_ids {
100 writer.write_u32::<LittleEndian>(*doc_id)?;
101 }
102 for ordinal in &map.ordinals {
103 writer.write_u16::<LittleEndian>(*ordinal)?;
104 }
105 for length in &map.lengths {
106 writer.write_u16::<LittleEndian>(*length)?;
107 }
108 }
109 Ok(offset)
110}
111
112#[derive(Debug, Clone)]
114pub struct ChunkMap {
115 doc_ids: OwnedBytes,
116 ordinals: OwnedBytes,
117 lengths: OwnedBytes,
118 num_chunks: u32,
119 total_tokens: u64,
120}
121
122impl ChunkMap {
123 #[inline]
125 pub fn num_chunks(&self) -> u32 {
126 self.num_chunks
127 }
128
129 pub fn total_tokens(&self) -> u64 {
131 self.total_tokens
132 }
133
134 pub fn avg_len(&self) -> f32 {
136 if self.num_chunks == 0 {
137 1.0
138 } else {
139 (self.total_tokens as f64 / f64::from(self.num_chunks)) as f32
140 }
141 }
142
143 #[inline]
145 pub fn doc_id(&self, vid: u32) -> DocId {
146 let at = vid as usize * 4;
147 let b = &self.doc_ids.as_slice()[at..at + 4];
148 u32::from_le_bytes([b[0], b[1], b[2], b[3]])
149 }
150
151 #[inline]
153 pub fn ordinal(&self, vid: u32) -> u16 {
154 let at = vid as usize * 2;
155 let b = &self.ordinals.as_slice()[at..at + 2];
156 u16::from_le_bytes([b[0], b[1]])
157 }
158
159 #[inline]
161 pub fn length(&self, vid: u32) -> u32 {
162 let at = vid as usize * 2;
163 let b = &self.lengths.as_slice()[at..at + 2];
164 u32::from(u16::from_le_bytes([b[0], b[1]]))
165 }
166
167 #[inline]
169 pub fn resolve(&self, vid: u32) -> (DocId, u16) {
170 (self.doc_id(vid), self.ordinal(vid))
171 }
172
173 pub(crate) fn doc_id_bytes(&self) -> &[u8] {
175 self.doc_ids.as_slice()
176 }
177
178 pub(crate) fn ordinal_bytes(&self) -> &[u8] {
180 self.ordinals.as_slice()
181 }
182
183 pub(crate) fn length_bytes(&self) -> &[u8] {
185 self.lengths.as_slice()
186 }
187}
188
189pub fn read_chunk_maps(bytes: OwnedBytes) -> io::Result<FxHashMap<u32, ChunkMap>> {
191 let data = bytes.as_slice();
192 if data.len() < HEADER_SIZE {
193 return Err(io::Error::new(
194 io::ErrorKind::InvalidData,
195 "chunk map file shorter than its header",
196 ));
197 }
198 let mut cursor = io::Cursor::new(data);
199 let magic = cursor.read_u32::<LittleEndian>()?;
200 if magic != MAGIC {
201 return Err(io::Error::new(
202 io::ErrorKind::InvalidData,
203 format!("chunk map magic mismatch: {magic:#x}"),
204 ));
205 }
206 let version = cursor.read_u32::<LittleEndian>()?;
207 if version != VERSION {
208 return Err(io::Error::new(
209 io::ErrorKind::InvalidData,
210 format!("unsupported chunk map version {version} (expected {VERSION})"),
211 ));
212 }
213 let num_fields = cursor.read_u32::<LittleEndian>()? as usize;
214 if data.len() < HEADER_SIZE + TOC_ENTRY_SIZE * num_fields {
215 return Err(io::Error::new(
216 io::ErrorKind::InvalidData,
217 "chunk map table of contents truncated",
218 ));
219 }
220 let mut maps = FxHashMap::default();
221 for _ in 0..num_fields {
222 let field_id = cursor.read_u32::<LittleEndian>()?;
223 let num_chunks = cursor.read_u32::<LittleEndian>()?;
224 let total_tokens = cursor.read_u64::<LittleEndian>()?;
225 let offset = cursor.read_u64::<LittleEndian>()? as usize;
226 let n = num_chunks as usize;
227 let end = offset
228 .checked_add(n.checked_mul(8).ok_or_else(|| {
229 io::Error::new(io::ErrorKind::InvalidData, "chunk map size overflow")
230 })?)
231 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "chunk map size overflow"))?;
232 if end > data.len() {
233 return Err(io::Error::new(
234 io::ErrorKind::InvalidData,
235 format!("chunk map section of field {field_id} exceeds file length"),
236 ));
237 }
238 let doc_ids = bytes.slice(offset..offset + n * 4);
239 let ordinals = bytes.slice(offset + n * 4..offset + n * 6);
240 let lengths = bytes.slice(offset + n * 6..end);
241 maps.insert(
242 field_id,
243 ChunkMap {
244 doc_ids,
245 ordinals,
246 lengths,
247 num_chunks,
248 total_tokens,
249 },
250 );
251 }
252 Ok(maps)
253}
254
255pub struct ChunkMapSource<'a> {
257 pub map: &'a ChunkMap,
258 pub doc_offset: u32,
260}
261
262pub fn write_merged_chunk_maps<W: Write + ?Sized>(
269 writer: &mut W,
270 fields: &[(u32, Vec<ChunkMapSource<'_>>)],
271) -> io::Result<u64> {
272 let live: Vec<&(u32, Vec<ChunkMapSource<'_>>)> = fields
273 .iter()
274 .filter(|(_, sources)| sources.iter().any(|s| s.map.num_chunks() > 0))
275 .collect();
276 let mut offset = (HEADER_SIZE + TOC_ENTRY_SIZE * live.len()) as u64;
277 writer.write_u32::<LittleEndian>(MAGIC)?;
278 writer.write_u32::<LittleEndian>(VERSION)?;
279 writer.write_u32::<LittleEndian>(live.len() as u32)?;
280 for (field_id, sources) in &live {
281 let mut num_chunks = 0u64;
282 let mut total_tokens = 0u64;
283 for source in sources {
284 num_chunks += u64::from(source.map.num_chunks());
285 total_tokens += source.map.total_tokens();
286 }
287 let num_chunks = u32::try_from(num_chunks).map_err(|_| {
288 io::Error::new(
289 io::ErrorKind::InvalidData,
290 format!("chunked field {field_id} exceeds u32::MAX chunks after merge"),
291 )
292 })?;
293 writer.write_u32::<LittleEndian>(*field_id)?;
294 writer.write_u32::<LittleEndian>(num_chunks)?;
295 writer.write_u64::<LittleEndian>(total_tokens)?;
296 writer.write_u64::<LittleEndian>(offset)?;
297 offset += u64::from(num_chunks) * 8;
298 }
299 let mut patched: Vec<u8> = Vec::new();
300 for (_, sources) in &live {
301 for source in sources {
302 if source.doc_offset == 0 {
303 writer.write_all(source.map.doc_id_bytes())?;
304 continue;
305 }
306 patched.clear();
307 patched.reserve(source.map.doc_id_bytes().len());
308 for chunk in source.map.doc_id_bytes().chunks_exact(4) {
309 let doc = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
310 let remapped = doc.checked_add(source.doc_offset).ok_or_else(|| {
311 io::Error::new(
312 io::ErrorKind::InvalidData,
313 "document id overflow while merging chunk maps",
314 )
315 })?;
316 patched.extend_from_slice(&remapped.to_le_bytes());
317 }
318 writer.write_all(&patched)?;
319 }
320 for source in sources {
321 writer.write_all(source.map.ordinal_bytes())?;
322 }
323 for source in sources {
324 writer.write_all(source.map.length_bytes())?;
325 }
326 }
327 Ok(offset)
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 fn build(entries: &[(u32, u16, u32)]) -> ChunkMapBuilder {
335 let mut builder = ChunkMapBuilder::default();
336 for &(doc, ord, len) in entries {
337 builder.push(doc, ord, len).unwrap();
338 }
339 builder
340 }
341
342 #[test]
343 fn round_trips_two_fields() {
344 let a = build(&[(0, 0, 10), (0, 1, 20), (3, 0, 70_000)]);
345 let b = build(&[(1, 0, 5)]);
346 let mut out = Vec::new();
347 write_chunk_maps(&mut out, &[(2, &a), (7, &b)]).unwrap();
348 let maps = read_chunk_maps(OwnedBytes::new(out)).unwrap();
349 let a = &maps[&2];
350 assert_eq!(a.num_chunks(), 3);
351 assert_eq!(a.resolve(0), (0, 0));
352 assert_eq!(a.resolve(1), (0, 1));
353 assert_eq!(a.resolve(2), (3, 0));
354 assert_eq!(a.length(1), 20);
355 assert_eq!(a.length(2), MAX_CHUNK_LENGTH, "lengths saturate at u16");
356 assert_eq!(a.total_tokens(), 70_030);
357 assert_eq!(maps[&7].resolve(0), (1, 0));
358 assert_eq!(maps[&7].avg_len(), 5.0);
359 }
360
361 #[test]
362 fn merged_maps_offset_doc_ids_and_keep_ordinals() {
363 let first = build(&[(0, 0, 10), (1, 0, 11), (1, 1, 12)]);
364 let second = build(&[(0, 0, 20), (0, 1, 21)]);
365 let mut raw_first = Vec::new();
366 write_chunk_maps(&mut raw_first, &[(4, &first)]).unwrap();
367 let mut raw_second = Vec::new();
368 write_chunk_maps(&mut raw_second, &[(4, &second)]).unwrap();
369 let first = read_chunk_maps(OwnedBytes::new(raw_first)).unwrap();
370 let second = read_chunk_maps(OwnedBytes::new(raw_second)).unwrap();
371
372 let mut merged = Vec::new();
373 write_merged_chunk_maps(
374 &mut merged,
375 &[(
376 4,
377 vec![
378 ChunkMapSource {
379 map: &first[&4],
380 doc_offset: 0,
381 },
382 ChunkMapSource {
383 map: &second[&4],
384 doc_offset: 2,
385 },
386 ],
387 )],
388 )
389 .unwrap();
390 let merged = read_chunk_maps(OwnedBytes::new(merged)).unwrap();
391 let map = &merged[&4];
392 assert_eq!(map.num_chunks(), 5);
393 assert_eq!(map.total_tokens(), 74);
394 assert_eq!(
395 (0..5).map(|v| map.resolve(v)).collect::<Vec<_>>(),
396 vec![(0, 0), (1, 0), (1, 1), (2, 0), (2, 1)]
397 );
398 assert_eq!(
399 (0..5).map(|v| map.length(v)).collect::<Vec<_>>(),
400 vec![10, 11, 12, 20, 21]
401 );
402 }
403
404 #[test]
405 fn rejects_foreign_or_truncated_files() {
406 assert!(read_chunk_maps(OwnedBytes::new(vec![0u8; 4])).is_err());
407 let mut bad_magic = Vec::new();
408 bad_magic.write_u32::<LittleEndian>(0xDEAD_BEEF).unwrap();
409 bad_magic.write_u32::<LittleEndian>(VERSION).unwrap();
410 bad_magic.write_u32::<LittleEndian>(0).unwrap();
411 assert!(read_chunk_maps(OwnedBytes::new(bad_magic)).is_err());
412
413 let a = build(&[(0, 0, 10)]);
414 let mut out = Vec::new();
415 write_chunk_maps(&mut out, &[(1, &a)]).unwrap();
416 out.truncate(out.len() - 1);
417 assert!(read_chunk_maps(OwnedBytes::new(out)).is_err());
418 }
419}