1use std::io::{self, Write};
31
32use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
33use rustc_hash::FxHashMap;
34
35use crate::DocId;
36use crate::directories::OwnedBytes;
37
38const MAGIC: u32 = 0x4B4E_4843; const VERSION: u32 = 2;
40const HEADER_SIZE: usize = 12;
41const TOC_ENTRY_SIZE_V1: usize = 24;
42const TOC_ENTRY_SIZE: usize = 28;
43const KIND_CHUNK_MAP: u32 = 0;
44const KIND_DOC_LENGTHS: u32 = 1;
45
46pub const MAX_CHUNK_LENGTH: u32 = u16::MAX as u32;
48
49#[derive(Debug, Default, Clone)]
51pub struct ChunkMapBuilder {
52 doc_ids: Vec<DocId>,
53 ordinals: Vec<u16>,
54 lengths: Vec<u16>,
55 total_tokens: u64,
56}
57
58impl ChunkMapBuilder {
59 pub fn len(&self) -> usize {
61 self.doc_ids.len()
62 }
63
64 pub fn is_empty(&self) -> bool {
65 self.doc_ids.is_empty()
66 }
67
68 pub fn push(&mut self, doc_id: DocId, ordinal: u16, token_count: u32) -> io::Result<u32> {
70 let vid = u32::try_from(self.doc_ids.len()).map_err(|_| {
71 io::Error::new(
72 io::ErrorKind::InvalidData,
73 "chunked text field exceeds u32::MAX chunks in one segment",
74 )
75 })?;
76 self.doc_ids.push(doc_id);
77 self.ordinals.push(ordinal);
78 self.lengths.push(token_count.min(MAX_CHUNK_LENGTH) as u16);
79 self.total_tokens += u64::from(token_count);
80 Ok(vid)
81 }
82
83 pub fn estimated_bytes(&self) -> usize {
85 self.doc_ids.capacity() * 4 + self.ordinals.capacity() * 2 + self.lengths.capacity() * 2
86 }
87
88 fn section_bytes(&self) -> u64 {
89 self.doc_ids.len() as u64 * 8
90 }
91
92 pub fn length(&self, vid: u32) -> u32 {
94 self.lengths
95 .get(vid as usize)
96 .map_or(0, |len| u32::from(*len))
97 }
98}
99
100pub struct DocLengthsColumn<'a> {
102 pub field_id: u32,
103 pub lengths: &'a [u16],
105 pub total_tokens: u64,
107}
108
109pub fn write_chunk_maps<W: Write + ?Sized>(
115 writer: &mut W,
116 fields: &[(u32, &ChunkMapBuilder)],
117 norms: &[DocLengthsColumn<'_>],
118) -> io::Result<u64> {
119 let sections = fields.len() + norms.len();
120 let mut offset = (HEADER_SIZE + TOC_ENTRY_SIZE * sections) as u64;
121 writer.write_u32::<LittleEndian>(MAGIC)?;
122 writer.write_u32::<LittleEndian>(VERSION)?;
123 writer.write_u32::<LittleEndian>(sections as u32)?;
124 for (field_id, map) in fields {
125 writer.write_u32::<LittleEndian>(*field_id)?;
126 writer.write_u32::<LittleEndian>(KIND_CHUNK_MAP)?;
127 writer.write_u32::<LittleEndian>(map.len() as u32)?;
128 writer.write_u64::<LittleEndian>(map.total_tokens)?;
129 writer.write_u64::<LittleEndian>(offset)?;
130 offset += map.section_bytes();
131 }
132 for column in norms {
133 writer.write_u32::<LittleEndian>(column.field_id)?;
134 writer.write_u32::<LittleEndian>(KIND_DOC_LENGTHS)?;
135 writer.write_u32::<LittleEndian>(column.lengths.len() as u32)?;
136 writer.write_u64::<LittleEndian>(column.total_tokens)?;
137 writer.write_u64::<LittleEndian>(offset)?;
138 offset += column.lengths.len() as u64 * 2;
139 }
140 for (_, map) in fields {
141 for doc_id in &map.doc_ids {
142 writer.write_u32::<LittleEndian>(*doc_id)?;
143 }
144 for ordinal in &map.ordinals {
145 writer.write_u16::<LittleEndian>(*ordinal)?;
146 }
147 for length in &map.lengths {
148 writer.write_u16::<LittleEndian>(*length)?;
149 }
150 }
151 for column in norms {
152 for length in column.lengths {
153 writer.write_u16::<LittleEndian>(*length)?;
154 }
155 }
156 Ok(offset)
157}
158
159#[derive(Debug, Clone)]
162pub struct DocLengths {
163 lengths: OwnedBytes,
164 num_docs: u32,
165 total_tokens: u64,
166}
167
168impl DocLengths {
169 #[cfg(test)]
171 pub(crate) fn from_lengths(lengths: &[u16]) -> Self {
172 let mut bytes = Vec::with_capacity(lengths.len() * 2);
173 for len in lengths {
174 bytes.extend_from_slice(&len.to_le_bytes());
175 }
176 Self {
177 lengths: OwnedBytes::new(bytes),
178 num_docs: lengths.len() as u32,
179 total_tokens: lengths.iter().map(|&l| u64::from(l)).sum(),
180 }
181 }
182
183 pub fn num_docs(&self) -> u32 {
184 self.num_docs
185 }
186
187 pub fn total_tokens(&self) -> u64 {
188 self.total_tokens
189 }
190
191 pub fn avg_len(&self) -> f32 {
193 let with_value = self
194 .lengths
195 .as_slice()
196 .chunks_exact(2)
197 .filter(|b| b[0] != 0 || b[1] != 0)
198 .count();
199 if with_value == 0 {
200 1.0
201 } else {
202 (self.total_tokens as f64 / with_value as f64) as f32
203 }
204 }
205
206 #[inline]
209 pub fn length(&self, doc_id: DocId) -> u32 {
210 let at = doc_id as usize * 2;
211 self.lengths
212 .as_slice()
213 .get(at..at + 2)
214 .map_or(0, |b| u32::from(u16::from_le_bytes([b[0], b[1]])))
215 }
216
217 pub(crate) fn length_bytes(&self) -> &[u8] {
218 self.lengths.as_slice()
219 }
220}
221
222#[derive(Debug, Default)]
224pub struct ChunkMapFile {
225 pub chunk_maps: FxHashMap<u32, ChunkMap>,
226 pub doc_lengths: FxHashMap<u32, DocLengths>,
227}
228
229#[derive(Debug, Clone)]
231pub struct ChunkMap {
232 doc_ids: OwnedBytes,
233 ordinals: OwnedBytes,
234 lengths: OwnedBytes,
235 num_chunks: u32,
236 total_tokens: u64,
237 length_floor: u32,
241 doc_ids_monotonic: bool,
243}
244
245fn nominal_chunk_length(lengths: &[u8]) -> u32 {
247 let n = lengths.len() / 2;
248 if n == 0 {
249 return 0;
250 }
251 let mut histogram = vec![0u32; u16::MAX as usize + 1];
252 for pair in lengths.chunks_exact(2) {
253 histogram[u16::from_le_bytes([pair[0], pair[1]]) as usize] += 1;
254 }
255 let target = (n as u64 * 9).div_ceil(10);
257 let mut seen = 0u64;
258 for (len, &count) in histogram.iter().enumerate() {
259 seen += u64::from(count);
260 if seen >= target {
261 return len as u32;
262 }
263 }
264 u16::MAX as u32
265}
266
267impl ChunkMap {
268 pub(crate) fn is_doc_ordered(&self) -> bool {
269 self.doc_ids_monotonic
270 }
271
272 pub(crate) fn lower_bound_doc(&self, target: DocId) -> u32 {
275 debug_assert!(self.is_doc_ordered());
276 let (mut lo, mut hi) = (0, self.num_chunks);
277 while lo < hi {
278 let mid = lo + (hi - lo) / 2;
279 if self.doc_id(mid) < target {
280 lo = mid + 1;
281 } else {
282 hi = mid;
283 }
284 }
285 lo
286 }
287
288 #[inline]
290 pub fn num_chunks(&self) -> u32 {
291 self.num_chunks
292 }
293
294 pub fn total_tokens(&self) -> u64 {
296 self.total_tokens
297 }
298
299 pub fn avg_len(&self) -> f32 {
301 if self.num_chunks == 0 {
302 1.0
303 } else {
304 (self.total_tokens as f64 / f64::from(self.num_chunks)) as f32
305 }
306 }
307
308 #[inline]
310 pub fn length_floor(&self) -> u32 {
311 self.length_floor
312 }
313
314 #[inline]
317 pub fn bm25_length(&self, vid: u32) -> u32 {
318 self.length(vid).max(self.length_floor)
319 }
320
321 #[inline]
323 pub fn doc_id(&self, vid: u32) -> DocId {
324 let at = vid as usize * 4;
325 let b = &self.doc_ids.as_slice()[at..at + 4];
326 u32::from_le_bytes([b[0], b[1], b[2], b[3]])
327 }
328
329 #[inline]
331 pub fn ordinal(&self, vid: u32) -> u16 {
332 let at = vid as usize * 2;
333 let b = &self.ordinals.as_slice()[at..at + 2];
334 u16::from_le_bytes([b[0], b[1]])
335 }
336
337 #[inline]
339 pub fn length(&self, vid: u32) -> u32 {
340 let at = vid as usize * 2;
341 let b = &self.lengths.as_slice()[at..at + 2];
342 u32::from(u16::from_le_bytes([b[0], b[1]]))
343 }
344
345 #[inline]
347 pub fn resolve(&self, vid: u32) -> (DocId, u16) {
348 (self.doc_id(vid), self.ordinal(vid))
349 }
350
351 pub(crate) fn doc_id_bytes(&self) -> &[u8] {
353 self.doc_ids.as_slice()
354 }
355
356 pub(crate) fn ordinal_bytes(&self) -> &[u8] {
358 self.ordinals.as_slice()
359 }
360
361 pub(crate) fn length_bytes(&self) -> &[u8] {
363 self.lengths.as_slice()
364 }
365}
366
367pub fn read_chunk_maps(bytes: OwnedBytes) -> io::Result<ChunkMapFile> {
369 let data = bytes.as_slice();
370 if data.len() < HEADER_SIZE {
371 return Err(io::Error::new(
372 io::ErrorKind::InvalidData,
373 "chunk map file shorter than its header",
374 ));
375 }
376 let mut cursor = io::Cursor::new(data);
377 let magic = cursor.read_u32::<LittleEndian>()?;
378 if magic != MAGIC {
379 return Err(io::Error::new(
380 io::ErrorKind::InvalidData,
381 format!("chunk map magic mismatch: {magic:#x}"),
382 ));
383 }
384 let version = cursor.read_u32::<LittleEndian>()?;
385 let entry_size = match version {
386 1 => TOC_ENTRY_SIZE_V1,
387 VERSION => TOC_ENTRY_SIZE,
388 other => {
389 return Err(io::Error::new(
390 io::ErrorKind::InvalidData,
391 format!("unsupported chunk map version {other} (expected {VERSION})"),
392 ));
393 }
394 };
395 let num_sections = cursor.read_u32::<LittleEndian>()? as usize;
396 if data.len() < HEADER_SIZE + entry_size * num_sections {
397 return Err(io::Error::new(
398 io::ErrorKind::InvalidData,
399 "chunk map table of contents truncated",
400 ));
401 }
402 let overflow = || io::Error::new(io::ErrorKind::InvalidData, "chunk map size overflow");
403 let mut file = ChunkMapFile::default();
404 for _ in 0..num_sections {
405 let field_id = cursor.read_u32::<LittleEndian>()?;
406 let kind = if version == 1 {
407 KIND_CHUNK_MAP
408 } else {
409 cursor.read_u32::<LittleEndian>()?
410 };
411 let count = cursor.read_u32::<LittleEndian>()?;
412 let total_tokens = cursor.read_u64::<LittleEndian>()?;
413 let offset = cursor.read_u64::<LittleEndian>()? as usize;
414 let n = count as usize;
415 let bytes_per_entry = match kind {
416 KIND_CHUNK_MAP => 8,
417 KIND_DOC_LENGTHS => 2,
418 other => {
419 return Err(io::Error::new(
420 io::ErrorKind::InvalidData,
421 format!("unknown chunk map section kind {other} for field {field_id}"),
422 ));
423 }
424 };
425 let end = offset
426 .checked_add(n.checked_mul(bytes_per_entry).ok_or_else(overflow)?)
427 .ok_or_else(overflow)?;
428 if end > data.len() {
429 return Err(io::Error::new(
430 io::ErrorKind::InvalidData,
431 format!("chunk map section of field {field_id} exceeds file length"),
432 ));
433 }
434 match kind {
435 KIND_CHUNK_MAP => {
436 let doc_ids = bytes.slice(offset..offset + n * 4);
437 let ordinals = bytes.slice(offset + n * 4..offset + n * 6);
438 let lengths = bytes.slice(offset + n * 6..end);
439 let length_floor = nominal_chunk_length(lengths.as_slice());
440 let doc_ids_monotonic = doc_ids
441 .as_slice()
442 .chunks_exact(4)
443 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
444 .is_sorted();
445 file.chunk_maps.insert(
446 field_id,
447 ChunkMap {
448 doc_ids,
449 ordinals,
450 lengths,
451 num_chunks: count,
452 total_tokens,
453 length_floor,
454 doc_ids_monotonic,
455 },
456 );
457 }
458 _ => {
459 file.doc_lengths.insert(
460 field_id,
461 DocLengths {
462 lengths: bytes.slice(offset..end),
463 num_docs: count,
464 total_tokens,
465 },
466 );
467 }
468 }
469 }
470 Ok(file)
471}
472
473pub struct ChunkMapSource<'a> {
475 pub map: &'a ChunkMap,
476 pub doc_offset: u32,
478}
479
480pub struct DocLengthsSource<'a> {
483 pub lengths: Option<&'a DocLengths>,
484 pub num_docs: u32,
485}
486
487pub fn write_merged_chunk_maps<W: Write + ?Sized>(
495 writer: &mut W,
496 fields: &[(u32, Vec<ChunkMapSource<'_>>)],
497 norms: &[(u32, Vec<DocLengthsSource<'_>>)],
498) -> io::Result<u64> {
499 let live: Vec<&(u32, Vec<ChunkMapSource<'_>>)> = fields
500 .iter()
501 .filter(|(_, sources)| sources.iter().any(|s| s.map.num_chunks() > 0))
502 .collect();
503 let sections = live.len() + norms.len();
504 let mut offset = (HEADER_SIZE + TOC_ENTRY_SIZE * sections) as u64;
505 writer.write_u32::<LittleEndian>(MAGIC)?;
506 writer.write_u32::<LittleEndian>(VERSION)?;
507 writer.write_u32::<LittleEndian>(sections as u32)?;
508 for (field_id, sources) in &live {
509 let mut num_chunks = 0u64;
510 let mut total_tokens = 0u64;
511 for source in sources {
512 num_chunks += u64::from(source.map.num_chunks());
513 total_tokens += source.map.total_tokens();
514 }
515 let num_chunks = u32::try_from(num_chunks).map_err(|_| {
516 io::Error::new(
517 io::ErrorKind::InvalidData,
518 format!("chunked field {field_id} exceeds u32::MAX chunks after merge"),
519 )
520 })?;
521 writer.write_u32::<LittleEndian>(*field_id)?;
522 writer.write_u32::<LittleEndian>(KIND_CHUNK_MAP)?;
523 writer.write_u32::<LittleEndian>(num_chunks)?;
524 writer.write_u64::<LittleEndian>(total_tokens)?;
525 writer.write_u64::<LittleEndian>(offset)?;
526 offset += u64::from(num_chunks) * 8;
527 }
528 for (field_id, sources) in norms {
529 let num_docs: u64 = sources.iter().map(|s| u64::from(s.num_docs)).sum();
530 let num_docs = u32::try_from(num_docs).map_err(|_| {
531 io::Error::new(
532 io::ErrorKind::InvalidData,
533 format!("field {field_id} exceeds u32::MAX documents after merge"),
534 )
535 })?;
536 let total_tokens: u64 = sources
537 .iter()
538 .filter_map(|s| s.lengths.map(DocLengths::total_tokens))
539 .sum();
540 writer.write_u32::<LittleEndian>(*field_id)?;
541 writer.write_u32::<LittleEndian>(KIND_DOC_LENGTHS)?;
542 writer.write_u32::<LittleEndian>(num_docs)?;
543 writer.write_u64::<LittleEndian>(total_tokens)?;
544 writer.write_u64::<LittleEndian>(offset)?;
545 offset += u64::from(num_docs) * 2;
546 }
547 let mut patched: Vec<u8> = Vec::new();
548 for (_, sources) in &live {
549 for source in sources {
550 if source.doc_offset == 0 {
551 writer.write_all(source.map.doc_id_bytes())?;
552 continue;
553 }
554 patched.clear();
555 patched.reserve(source.map.doc_id_bytes().len());
556 for chunk in source.map.doc_id_bytes().chunks_exact(4) {
557 let doc = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
558 let remapped = doc.checked_add(source.doc_offset).ok_or_else(|| {
559 io::Error::new(
560 io::ErrorKind::InvalidData,
561 "document id overflow while merging chunk maps",
562 )
563 })?;
564 patched.extend_from_slice(&remapped.to_le_bytes());
565 }
566 writer.write_all(&patched)?;
567 }
568 for source in sources {
569 writer.write_all(source.map.ordinal_bytes())?;
570 }
571 for source in sources {
572 writer.write_all(source.map.length_bytes())?;
573 }
574 }
575 let zeros = [0u8; 2 * 1024];
576 for (_, sources) in norms {
577 for source in sources {
578 match source.lengths {
579 Some(lengths) if lengths.num_docs() == source.num_docs => {
580 writer.write_all(lengths.length_bytes())?;
581 }
582 Some(lengths) => {
583 return Err(io::Error::new(
584 io::ErrorKind::InvalidData,
585 format!(
586 "length column covers {} documents, segment has {}",
587 lengths.num_docs(),
588 source.num_docs
589 ),
590 ));
591 }
592 None => {
593 let mut remaining = source.num_docs as usize * 2;
594 while remaining > 0 {
595 let take = remaining.min(zeros.len());
596 writer.write_all(&zeros[..take])?;
597 remaining -= take;
598 }
599 }
600 }
601 }
602 }
603 Ok(offset)
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609
610 fn build(entries: &[(u32, u16, u32)]) -> ChunkMapBuilder {
611 let mut builder = ChunkMapBuilder::default();
612 for &(doc, ord, len) in entries {
613 builder.push(doc, ord, len).unwrap();
614 }
615 builder
616 }
617
618 #[test]
619 fn round_trips_two_fields() {
620 let a = build(&[(0, 0, 10), (0, 1, 20), (3, 0, 70_000)]);
621 let b = build(&[(1, 0, 5)]);
622 let mut out = Vec::new();
623 write_chunk_maps(&mut out, &[(2, &a), (7, &b)], &[]).unwrap();
624 let maps = read_chunk_maps(OwnedBytes::new(out)).unwrap().chunk_maps;
625 let a = &maps[&2];
626 assert_eq!(a.num_chunks(), 3);
627 assert_eq!(a.resolve(0), (0, 0));
628 assert_eq!(a.resolve(1), (0, 1));
629 assert_eq!(a.resolve(2), (3, 0));
630 assert_eq!(a.length(1), 20);
631 assert_eq!(a.length(2), MAX_CHUNK_LENGTH, "lengths saturate at u16");
632 assert_eq!(a.total_tokens(), 70_030);
633 assert_eq!(maps[&7].resolve(0), (1, 0));
634 assert_eq!(maps[&7].avg_len(), 5.0);
635 }
636
637 #[test]
638 fn merged_maps_offset_doc_ids_and_keep_ordinals() {
639 let first = build(&[(0, 0, 10), (1, 0, 11), (1, 1, 12)]);
640 let second = build(&[(0, 0, 20), (0, 1, 21)]);
641 let mut raw_first = Vec::new();
642 write_chunk_maps(&mut raw_first, &[(4, &first)], &[]).unwrap();
643 let mut raw_second = Vec::new();
644 write_chunk_maps(&mut raw_second, &[(4, &second)], &[]).unwrap();
645 let first = read_chunk_maps(OwnedBytes::new(raw_first))
646 .unwrap()
647 .chunk_maps;
648 let second = read_chunk_maps(OwnedBytes::new(raw_second))
649 .unwrap()
650 .chunk_maps;
651
652 let mut merged = Vec::new();
653 write_merged_chunk_maps(
654 &mut merged,
655 &[(
656 4,
657 vec![
658 ChunkMapSource {
659 map: &first[&4],
660 doc_offset: 0,
661 },
662 ChunkMapSource {
663 map: &second[&4],
664 doc_offset: 2,
665 },
666 ],
667 )],
668 &[],
669 )
670 .unwrap();
671 let merged = read_chunk_maps(OwnedBytes::new(merged)).unwrap().chunk_maps;
672 let map = &merged[&4];
673 assert_eq!(map.num_chunks(), 5);
674 assert_eq!(map.total_tokens(), 74);
675 assert_eq!(
676 (0..5).map(|v| map.resolve(v)).collect::<Vec<_>>(),
677 vec![(0, 0), (1, 0), (1, 1), (2, 0), (2, 1)]
678 );
679 assert_eq!(
680 (0..5).map(|v| map.length(v)).collect::<Vec<_>>(),
681 vec![10, 11, 12, 20, 21]
682 );
683 }
684
685 #[test]
686 fn rejects_foreign_or_truncated_files() {
687 assert!(read_chunk_maps(OwnedBytes::new(vec![0u8; 4])).is_err());
688 let mut bad_magic = Vec::new();
689 bad_magic.write_u32::<LittleEndian>(0xDEAD_BEEF).unwrap();
690 bad_magic.write_u32::<LittleEndian>(VERSION).unwrap();
691 bad_magic.write_u32::<LittleEndian>(0).unwrap();
692 assert!(read_chunk_maps(OwnedBytes::new(bad_magic)).is_err());
693
694 let a = build(&[(0, 0, 10)]);
695 let mut out = Vec::new();
696 write_chunk_maps(&mut out, &[(1, &a)], &[]).unwrap();
697 out.truncate(out.len() - 1);
698 assert!(read_chunk_maps(OwnedBytes::new(out)).is_err());
699 }
700
701 #[test]
702 fn doc_length_columns_round_trip_and_merge_with_zero_fill() {
703 let a = build(&[(0, 0, 10)]);
704 let column = [7u16, 0, 300];
705 let mut out = Vec::new();
706 write_chunk_maps(
707 &mut out,
708 &[(1, &a)],
709 &[DocLengthsColumn {
710 field_id: 5,
711 lengths: &column,
712 total_tokens: 307,
713 }],
714 )
715 .unwrap();
716 let file = read_chunk_maps(OwnedBytes::new(out)).unwrap();
717 assert_eq!(file.chunk_maps[&1].num_chunks(), 1);
718 let norms = &file.doc_lengths[&5];
719 assert_eq!(norms.num_docs(), 3);
720 assert_eq!(
721 (0..4).map(|d| norms.length(d)).collect::<Vec<_>>(),
722 vec![7, 0, 300, 0]
723 );
724 assert_eq!(norms.total_tokens(), 307);
725 assert!(
726 (norms.avg_len() - 153.5).abs() < 1e-3,
727 "{}",
728 norms.avg_len()
729 );
730
731 let mut merged = Vec::new();
733 write_merged_chunk_maps(
734 &mut merged,
735 &[],
736 &[(
737 5,
738 vec![
739 DocLengthsSource {
740 lengths: None,
741 num_docs: 2,
742 },
743 DocLengthsSource {
744 lengths: Some(norms),
745 num_docs: 3,
746 },
747 ],
748 )],
749 )
750 .unwrap();
751 let merged = read_chunk_maps(OwnedBytes::new(merged)).unwrap();
752 assert!(merged.chunk_maps.is_empty());
753 let norms = &merged.doc_lengths[&5];
754 assert_eq!(norms.num_docs(), 5);
755 assert_eq!(
756 (0..5).map(|d| norms.length(d)).collect::<Vec<_>>(),
757 vec![0, 0, 7, 0, 300]
758 );
759 assert_eq!(norms.total_tokens(), 307);
760 }
761
762 #[test]
763 fn version_one_files_still_read() {
764 let a = build(&[(0, 0, 10), (2, 0, 4)]);
765 let mut out = Vec::new();
766 out.write_u32::<LittleEndian>(MAGIC).unwrap();
767 out.write_u32::<LittleEndian>(1).unwrap();
768 out.write_u32::<LittleEndian>(1).unwrap();
769 out.write_u32::<LittleEndian>(9).unwrap();
770 out.write_u32::<LittleEndian>(2).unwrap();
771 out.write_u64::<LittleEndian>(14).unwrap();
772 out.write_u64::<LittleEndian>((HEADER_SIZE + TOC_ENTRY_SIZE_V1) as u64)
773 .unwrap();
774 for doc in &a.doc_ids {
775 out.write_u32::<LittleEndian>(*doc).unwrap();
776 }
777 for ord in &a.ordinals {
778 out.write_u16::<LittleEndian>(*ord).unwrap();
779 }
780 for len in &a.lengths {
781 out.write_u16::<LittleEndian>(*len).unwrap();
782 }
783 let file = read_chunk_maps(OwnedBytes::new(out)).unwrap();
784 assert!(file.doc_lengths.is_empty());
785 let map = &file.chunk_maps[&9];
786 assert_eq!(map.resolve(1), (2, 0));
787 assert_eq!(map.length(1), 4);
788 }
789}