Skip to main content

hermes_core/segment/
store.rs

1//! Document store with Zstd compression and lazy loading
2//!
3//! Optimized for static indexes:
4//! - Maximum compression level (22) for best compression ratio
5//! - Larger block sizes (256KB) for better compression efficiency
6//! - Optional trained dictionary support for even better compression
7//! - Parallel compression support for faster indexing
8//!
9//! Writer stores documents in compressed blocks.
10//! Reader only loads index into memory, blocks are loaded on-demand.
11
12use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
13use parking_lot::RwLock;
14use rustc_hash::FxHashMap;
15use std::io::{self, Write};
16use std::sync::Arc;
17
18use crate::DocId;
19use crate::compression::CompressionDict;
20#[cfg(feature = "native")]
21use crate::compression::CompressionLevel;
22use crate::directories::FileHandle;
23use crate::dsl::{Document, Schema};
24
25const STORE_MAGIC: u32 = 0x53544F52; // "STOR"
26const STORE_VERSION: u32 = 2; // Version 2 supports dictionaries
27
28/// Block size for document store (16KB).
29/// Smaller blocks reduce read amplification for single-doc fetches at the
30/// cost of slightly worse compression ratio. Zstd dictionary training
31/// recovers most of the compression loss.
32pub const STORE_BLOCK_SIZE: usize = 16 * 1024;
33
34/// Default dictionary size (4KB is a good balance)
35pub const DEFAULT_DICT_SIZE: usize = 4 * 1024;
36
37/// Hard safety bounds for individual on-disk store objects. Writers normally
38/// emit ~16 KiB blocks and 4 KiB dictionaries; these generous limits preserve
39/// large legacy documents while bounding corrupt compressed frames.
40const MAX_STORE_BLOCK_BYTES: usize = 64 * 1024 * 1024;
41const MAX_STORE_DICTIONARY_BYTES: u64 = 16 * 1024 * 1024;
42
43/// Default compression level for document store
44#[cfg(feature = "native")]
45const DEFAULT_COMPRESSION_LEVEL: CompressionLevel = CompressionLevel(3);
46
47/// Write block index + footer to a store file.
48///
49/// Shared by `EagerParallelStoreWriter::finish` and `StoreMerger::finish`.
50fn write_store_index_and_footer(
51    writer: &mut (impl Write + ?Sized),
52    index: &[StoreBlockIndex],
53    data_end_offset: u64,
54    dict_offset: u64,
55    num_docs: u32,
56    has_dict: bool,
57) -> io::Result<()> {
58    writer.write_u32::<LittleEndian>(u32::try_from(index.len()).map_err(|_| {
59        io::Error::new(
60            io::ErrorKind::InvalidInput,
61            "too many document store blocks",
62        )
63    })?)?;
64    for entry in index {
65        writer.write_u32::<LittleEndian>(entry.first_doc_id)?;
66        writer.write_u64::<LittleEndian>(entry.offset)?;
67        writer.write_u32::<LittleEndian>(entry.length)?;
68        writer.write_u32::<LittleEndian>(entry.num_docs)?;
69    }
70    writer.write_u64::<LittleEndian>(data_end_offset)?;
71    writer.write_u64::<LittleEndian>(dict_offset)?;
72    writer.write_u32::<LittleEndian>(num_docs)?;
73    writer.write_u32::<LittleEndian>(if has_dict { 1 } else { 0 })?;
74    writer.write_u32::<LittleEndian>(STORE_VERSION)?;
75    writer.write_u32::<LittleEndian>(STORE_MAGIC)?;
76    Ok(())
77}
78
79/// Binary document format:
80///   num_fields: u16
81///   per field: field_id: u16, type_tag: u8, value data
82///     0=Text:         len:u32 + utf8
83///     1=U64:          u64 LE
84///     2=I64:          i64 LE
85///     3=F64:          f64 LE
86///     4=Bytes:        len:u32 + raw
87///     5=SparseVector: count:u32 + count*(u32+f32)
88///     6=DenseVector:  count:u32 + count*f32
89///     7=Json:         len:u32 + json utf8
90pub fn serialize_document(doc: &Document, schema: &Schema) -> io::Result<Vec<u8>> {
91    let mut buf = Vec::with_capacity(256);
92    serialize_document_into(doc, schema, &mut buf)?;
93    Ok(buf)
94}
95
96/// Serialize a document into a reusable buffer (clears it first).
97/// Avoids per-document allocation when called in a loop.
98pub fn serialize_document_into(
99    doc: &Document,
100    schema: &Schema,
101    buf: &mut Vec<u8>,
102) -> io::Result<()> {
103    use crate::dsl::FieldValue;
104
105    buf.clear();
106
107    // Two-pass approach avoids allocating a Vec just to count + iterate stored fields.
108    let is_stored = |field: &crate::dsl::Field, value: &FieldValue| -> bool {
109        // Dense/binary vectors live in .vectors (LazyFlatVectorData), not in .store
110        if matches!(
111            value,
112            FieldValue::DenseVector(_) | FieldValue::BinaryDenseVector(_)
113        ) {
114            return false;
115        }
116        schema.get_field_entry(*field).is_some_and(|e| e.stored)
117    };
118
119    let stored_count = doc
120        .field_values()
121        .iter()
122        .filter(|(field, value)| is_stored(field, value))
123        .count();
124
125    buf.write_u16::<LittleEndian>(
126        u16::try_from(stored_count)
127            .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "too many stored fields"))?,
128    )?;
129
130    for (field, value) in doc.field_values().iter().filter(|(f, v)| is_stored(f, v)) {
131        buf.write_u16::<LittleEndian>(u16::try_from(field.0).map_err(|_| {
132            io::Error::new(io::ErrorKind::InvalidInput, "stored field id exceeds u16")
133        })?)?;
134        match value {
135            FieldValue::Text(s) => {
136                buf.push(0);
137                let bytes = s.as_bytes();
138                buf.write_u32::<LittleEndian>(u32::try_from(bytes.len()).map_err(|_| {
139                    io::Error::new(io::ErrorKind::InvalidInput, "stored text is too large")
140                })?)?;
141                buf.extend_from_slice(bytes);
142            }
143            FieldValue::U64(v) => {
144                buf.push(1);
145                buf.write_u64::<LittleEndian>(*v)?;
146            }
147            FieldValue::I64(v) => {
148                buf.push(2);
149                buf.write_i64::<LittleEndian>(*v)?;
150            }
151            FieldValue::F64(v) => {
152                buf.push(3);
153                buf.write_f64::<LittleEndian>(*v)?;
154            }
155            FieldValue::Bytes(b) => {
156                buf.push(4);
157                buf.write_u32::<LittleEndian>(u32::try_from(b.len()).map_err(|_| {
158                    io::Error::new(
159                        io::ErrorKind::InvalidInput,
160                        "stored byte field is too large",
161                    )
162                })?)?;
163                buf.extend_from_slice(b);
164            }
165            FieldValue::SparseVector(entries) => {
166                buf.push(5);
167                buf.write_u32::<LittleEndian>(u32::try_from(entries.len()).map_err(|_| {
168                    io::Error::new(
169                        io::ErrorKind::InvalidInput,
170                        "stored sparse vector is too large",
171                    )
172                })?)?;
173                for (idx, val) in entries {
174                    buf.write_u32::<LittleEndian>(*idx)?;
175                    buf.write_f32::<LittleEndian>(*val)?;
176                }
177            }
178            FieldValue::DenseVector(values) => {
179                buf.push(6);
180                buf.write_u32::<LittleEndian>(u32::try_from(values.len()).map_err(|_| {
181                    io::Error::new(
182                        io::ErrorKind::InvalidInput,
183                        "stored dense vector is too large",
184                    )
185                })?)?;
186                // Write raw f32 bytes directly
187                let byte_slice = unsafe {
188                    std::slice::from_raw_parts(values.as_ptr() as *const u8, values.len() * 4)
189                };
190                buf.extend_from_slice(byte_slice);
191            }
192            FieldValue::Json(v) => {
193                buf.push(7);
194                let json_bytes = serde_json::to_vec(v)
195                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
196                buf.write_u32::<LittleEndian>(u32::try_from(json_bytes.len()).map_err(|_| {
197                    io::Error::new(io::ErrorKind::InvalidInput, "stored JSON is too large")
198                })?)?;
199                buf.extend_from_slice(&json_bytes);
200            }
201            FieldValue::BinaryDenseVector(b) => {
202                buf.push(8);
203                buf.write_u32::<LittleEndian>(u32::try_from(b.len()).map_err(|_| {
204                    io::Error::new(
205                        io::ErrorKind::InvalidInput,
206                        "stored binary dense vector is too large",
207                    )
208                })?)?;
209                buf.extend_from_slice(b);
210            }
211        }
212    }
213
214    Ok(())
215}
216
217/// Compressed block result
218#[cfg(feature = "native")]
219struct CompressedBlock {
220    seq: usize,
221    first_doc_id: DocId,
222    num_docs: u32,
223    compressed: Vec<u8>,
224}
225
226/// Parallel document store writer - compresses blocks immediately when queued
227///
228/// Spawns compression tasks as soon as blocks are ready, overlapping document
229/// ingestion with compression to reduce total indexing time.
230///
231/// Uses background threads to compress blocks while the main thread continues
232/// accepting documents.
233#[cfg(feature = "native")]
234pub struct EagerParallelStoreWriter<'a> {
235    writer: &'a mut dyn Write,
236    block_buffer: Vec<u8>,
237    /// Reusable buffer for document serialization (avoids per-doc allocation)
238    serialize_buf: Vec<u8>,
239    /// Compressed blocks ready to be written (may arrive out of order)
240    compressed_blocks: Vec<CompressedBlock>,
241    /// Handles for in-flight compression tasks
242    pending_handles: Vec<std::thread::JoinHandle<CompressedBlock>>,
243    next_seq: usize,
244    next_doc_id: DocId,
245    block_first_doc: DocId,
246    dict: Option<Arc<CompressionDict>>,
247    compression_level: CompressionLevel,
248}
249
250#[cfg(feature = "native")]
251impl<'a> EagerParallelStoreWriter<'a> {
252    /// Create a new eager parallel store writer
253    pub fn new(writer: &'a mut dyn Write, _num_threads: usize) -> Self {
254        Self::with_compression_level(writer, _num_threads, DEFAULT_COMPRESSION_LEVEL)
255    }
256
257    /// Create with specific compression level
258    pub fn with_compression_level(
259        writer: &'a mut dyn Write,
260        _num_threads: usize,
261        compression_level: CompressionLevel,
262    ) -> Self {
263        Self {
264            writer,
265            block_buffer: Vec::with_capacity(STORE_BLOCK_SIZE),
266            serialize_buf: Vec::with_capacity(512),
267            compressed_blocks: Vec::new(),
268            pending_handles: Vec::new(),
269            next_seq: 0,
270            next_doc_id: 0,
271            block_first_doc: 0,
272            dict: None,
273            compression_level,
274        }
275    }
276
277    /// Create with dictionary
278    pub fn with_dict(
279        writer: &'a mut dyn Write,
280        dict: CompressionDict,
281        _num_threads: usize,
282    ) -> Self {
283        Self::with_dict_and_level(writer, dict, _num_threads, DEFAULT_COMPRESSION_LEVEL)
284    }
285
286    /// Create with dictionary and specific compression level
287    pub fn with_dict_and_level(
288        writer: &'a mut dyn Write,
289        dict: CompressionDict,
290        _num_threads: usize,
291        compression_level: CompressionLevel,
292    ) -> Self {
293        Self {
294            writer,
295            block_buffer: Vec::with_capacity(STORE_BLOCK_SIZE),
296            serialize_buf: Vec::with_capacity(512),
297            compressed_blocks: Vec::new(),
298            pending_handles: Vec::new(),
299            next_seq: 0,
300            next_doc_id: 0,
301            block_first_doc: 0,
302            dict: Some(Arc::new(dict)),
303            compression_level,
304        }
305    }
306
307    pub fn store(&mut self, doc: &Document, schema: &Schema) -> io::Result<DocId> {
308        serialize_document_into(doc, schema, &mut self.serialize_buf)?;
309        if self.serialize_buf.len() > MAX_STORE_BLOCK_BYTES.saturating_sub(4) {
310            return Err(io::Error::new(
311                io::ErrorKind::InvalidInput,
312                "serialized document exceeds store block limit",
313            ));
314        }
315        let doc_id = self.next_doc_id;
316        self.next_doc_id = self
317            .next_doc_id
318            .checked_add(1)
319            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "document id overflow"))?;
320        self.block_buffer
321            .write_u32::<LittleEndian>(self.serialize_buf.len() as u32)?;
322        self.block_buffer.extend_from_slice(&self.serialize_buf);
323        if self.block_buffer.len() >= STORE_BLOCK_SIZE {
324            self.spawn_compression();
325        }
326        Ok(doc_id)
327    }
328
329    /// Store pre-serialized document bytes directly (avoids deserialize+reserialize roundtrip).
330    pub fn store_raw(&mut self, doc_bytes: &[u8]) -> io::Result<DocId> {
331        if doc_bytes.len() > MAX_STORE_BLOCK_BYTES.saturating_sub(4) {
332            return Err(io::Error::new(
333                io::ErrorKind::InvalidInput,
334                "serialized document exceeds store block limit",
335            ));
336        }
337        let doc_id = self.next_doc_id;
338        self.next_doc_id = self
339            .next_doc_id
340            .checked_add(1)
341            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "document id overflow"))?;
342
343        self.block_buffer
344            .write_u32::<LittleEndian>(doc_bytes.len() as u32)?;
345        self.block_buffer.extend_from_slice(doc_bytes);
346
347        if self.block_buffer.len() >= STORE_BLOCK_SIZE {
348            self.spawn_compression();
349        }
350
351        Ok(doc_id)
352    }
353
354    /// Spawn compression for the current block immediately
355    fn spawn_compression(&mut self) {
356        if self.block_buffer.is_empty() {
357            return;
358        }
359
360        let num_docs = self.next_doc_id - self.block_first_doc;
361        let data = std::mem::replace(&mut self.block_buffer, Vec::with_capacity(STORE_BLOCK_SIZE));
362        let seq = self.next_seq;
363        let first_doc_id = self.block_first_doc;
364        let dict = self.dict.clone();
365
366        self.next_seq += 1;
367        self.block_first_doc = self.next_doc_id;
368
369        // Spawn compression task using thread
370        let level = self.compression_level;
371        let handle = std::thread::spawn(move || {
372            let compressed = if let Some(ref d) = dict {
373                crate::compression::compress_with_dict(&data, level, d).expect("compression failed")
374            } else {
375                crate::compression::compress(&data, level).expect("compression failed")
376            };
377
378            CompressedBlock {
379                seq,
380                first_doc_id,
381                num_docs,
382                compressed,
383            }
384        });
385
386        self.pending_handles.push(handle);
387    }
388
389    /// Collect any completed compression tasks
390    fn collect_completed(&mut self) {
391        let mut remaining = Vec::new();
392        for handle in self.pending_handles.drain(..) {
393            if handle.is_finished() {
394                match handle.join() {
395                    Ok(block) => self.compressed_blocks.push(block),
396                    Err(payload) => std::panic::resume_unwind(payload),
397                }
398            } else {
399                remaining.push(handle);
400            }
401        }
402        self.pending_handles = remaining;
403    }
404
405    pub fn finish(mut self) -> io::Result<u32> {
406        // Spawn compression for any remaining data
407        self.spawn_compression();
408
409        // Collect any already-completed tasks
410        self.collect_completed();
411
412        // Wait for all remaining compression tasks
413        for handle in self.pending_handles.drain(..) {
414            match handle.join() {
415                Ok(block) => self.compressed_blocks.push(block),
416                Err(payload) => std::panic::resume_unwind(payload),
417            }
418        }
419
420        if self.compressed_blocks.is_empty() {
421            write_store_index_and_footer(&mut self.writer, &[], 0, 0, 0, false)?;
422            return Ok(0);
423        }
424
425        // Sort by sequence to maintain order
426        self.compressed_blocks.sort_by_key(|b| b.seq);
427
428        // Write blocks in order and build index
429        let mut index = Vec::with_capacity(self.compressed_blocks.len());
430        let mut current_offset = 0u64;
431
432        for block in &self.compressed_blocks {
433            index.push(StoreBlockIndex {
434                first_doc_id: block.first_doc_id,
435                offset: current_offset,
436                length: u32::try_from(block.compressed.len()).map_err(|_| {
437                    io::Error::new(
438                        io::ErrorKind::InvalidData,
439                        "compressed store block too large",
440                    )
441                })?,
442                num_docs: block.num_docs,
443            });
444
445            self.writer.write_all(&block.compressed)?;
446            current_offset = current_offset
447                .checked_add(block.compressed.len() as u64)
448                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "store size overflow"))?;
449        }
450
451        let data_end_offset = current_offset;
452
453        // Write dictionary if present
454        let dict_offset = if let Some(ref dict) = self.dict {
455            let offset = current_offset;
456            let dict_bytes = dict.as_bytes();
457            self.writer
458                .write_u32::<LittleEndian>(dict_bytes.len() as u32)?;
459            self.writer.write_all(dict_bytes)?;
460            Some(offset)
461        } else {
462            None
463        };
464
465        // Write index + footer
466        write_store_index_and_footer(
467            &mut self.writer,
468            &index,
469            data_end_offset,
470            dict_offset.unwrap_or(0),
471            self.next_doc_id,
472            self.dict.is_some(),
473        )?;
474
475        Ok(self.next_doc_id)
476    }
477}
478
479/// Block index entry for document store
480#[derive(Debug, Clone)]
481pub(crate) struct StoreBlockIndex {
482    pub(crate) first_doc_id: DocId,
483    pub(crate) offset: u64,
484    pub(crate) length: u32,
485    pub(crate) num_docs: u32,
486}
487
488/// Async document store reader - loads blocks on demand
489pub struct AsyncStoreReader {
490    /// FileHandle for the data portion - fetches ranges on demand
491    data_slice: FileHandle,
492    /// Block index
493    index: Vec<StoreBlockIndex>,
494    num_docs: u32,
495    /// Optional compression dictionary
496    dict: Option<CompressionDict>,
497    /// Block cache
498    cache: RwLock<StoreBlockCache>,
499}
500
501/// Decompressed block with pre-built doc offset table.
502///
503/// The offset table is built once on decompression: `offsets[i]` is the byte
504/// position in `data` where doc `i`'s length prefix starts. This turns the
505/// O(n) linear scan per `get()` into O(1) direct indexing.
506struct CachedBlock {
507    data: Vec<u8>,
508    /// Byte offset of each doc's length prefix within `data`.
509    /// `offsets.len()` == number of docs in the block.
510    offsets: Vec<u32>,
511}
512
513impl CachedBlock {
514    fn build(data: Vec<u8>, num_docs: u32) -> io::Result<Self> {
515        if num_docs as usize > data.len() / 4 {
516            return Err(io::Error::new(
517                io::ErrorKind::InvalidData,
518                "store block document count exceeds block length",
519            ));
520        }
521        let mut offsets = Vec::new();
522        offsets.try_reserve_exact(num_docs as usize).map_err(|_| {
523            io::Error::new(
524                io::ErrorKind::InvalidData,
525                "store block has too many documents",
526            )
527        })?;
528        let mut pos = 0usize;
529        for _ in 0..num_docs {
530            let length_end = pos.checked_add(4).ok_or_else(|| {
531                io::Error::new(io::ErrorKind::InvalidData, "store block offset overflow")
532            })?;
533            if length_end > data.len() {
534                return Err(io::Error::new(
535                    io::ErrorKind::InvalidData,
536                    "truncated block while building offset table",
537                ));
538            }
539            offsets.push(u32::try_from(pos).map_err(|_| {
540                io::Error::new(io::ErrorKind::InvalidData, "store block offset exceeds u32")
541            })?);
542            let doc_len =
543                u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
544                    as usize;
545            pos = length_end.checked_add(doc_len).ok_or_else(|| {
546                io::Error::new(io::ErrorKind::InvalidData, "store document length overflow")
547            })?;
548            if pos > data.len() {
549                return Err(io::Error::new(
550                    io::ErrorKind::UnexpectedEof,
551                    "store document is truncated",
552                ));
553            }
554        }
555        if pos != data.len() {
556            return Err(io::Error::new(
557                io::ErrorKind::InvalidData,
558                "store block contains trailing data",
559            ));
560        }
561        Ok(Self { data, offsets })
562    }
563
564    /// Get doc bytes by index within the block (O(1))
565    fn doc_bytes(&self, doc_offset_in_block: u32) -> io::Result<&[u8]> {
566        let idx = doc_offset_in_block as usize;
567        if idx >= self.offsets.len() {
568            return Err(io::Error::new(
569                io::ErrorKind::InvalidData,
570                "doc offset out of range",
571            ));
572        }
573        let start = self.offsets[idx] as usize;
574        let data_start = start.checked_add(4).ok_or_else(|| {
575            io::Error::new(io::ErrorKind::InvalidData, "store document offset overflow")
576        })?;
577        if data_start > self.data.len() {
578            return Err(io::Error::new(
579                io::ErrorKind::InvalidData,
580                "truncated doc length",
581            ));
582        }
583        let doc_len = u32::from_le_bytes([
584            self.data[start],
585            self.data[start + 1],
586            self.data[start + 2],
587            self.data[start + 3],
588        ]) as usize;
589        let data_end = data_start.checked_add(doc_len).ok_or_else(|| {
590            io::Error::new(io::ErrorKind::InvalidData, "store document length overflow")
591        })?;
592        if data_end > self.data.len() {
593            return Err(io::Error::new(
594                io::ErrorKind::InvalidData,
595                "doc data overflow",
596            ));
597        }
598        Ok(&self.data[data_start..data_end])
599    }
600
601    #[inline]
602    fn retained_bytes(&self) -> usize {
603        self.data.capacity() + self.offsets.capacity() * std::mem::size_of::<u32>()
604    }
605}
606
607/// Bounded block cache with a contention-free read path.
608///
609/// Normal reads use [`StoreBlockCache::peek`] under a shared lock and do not
610/// promote hits, so normal eviction order is insertion order. `get()` promotes
611/// only on the exclusive race-resolution path.
612struct StoreBlockCache {
613    blocks: FxHashMap<DocId, Arc<CachedBlock>>,
614    lru_order: std::collections::VecDeque<DocId>,
615    max_blocks: usize,
616}
617
618impl StoreBlockCache {
619    fn new(max_blocks: usize) -> Self {
620        Self {
621            blocks: FxHashMap::default(),
622            lru_order: std::collections::VecDeque::with_capacity(max_blocks),
623            max_blocks,
624        }
625    }
626
627    /// Check cache without LRU promotion (safe for read-lock fast path)
628    fn peek(&self, first_doc_id: DocId) -> Option<Arc<CachedBlock>> {
629        self.blocks.get(&first_doc_id).map(Arc::clone)
630    }
631
632    fn get(&mut self, first_doc_id: DocId) -> Option<Arc<CachedBlock>> {
633        let block = self.blocks.get(&first_doc_id).map(Arc::clone)?;
634        self.promote(first_doc_id);
635        Some(block)
636    }
637
638    fn insert(&mut self, first_doc_id: DocId, block: Arc<CachedBlock>) {
639        if self.max_blocks == 0 {
640            return;
641        }
642        if self.blocks.contains_key(&first_doc_id) {
643            self.promote(first_doc_id);
644            return;
645        }
646        while self.blocks.len() >= self.max_blocks {
647            if let Some(evict) = self.lru_order.pop_front() {
648                self.blocks.remove(&evict);
649            } else {
650                break;
651            }
652        }
653        self.blocks.insert(first_doc_id, block);
654        self.lru_order.push_back(first_doc_id);
655    }
656
657    fn promote(&mut self, key: DocId) {
658        if let Some(pos) = self.lru_order.iter().position(|&k| k == key) {
659            self.lru_order.remove(pos);
660            self.lru_order.push_back(key);
661        }
662    }
663}
664
665impl AsyncStoreReader {
666    /// Open a document store from FileHandle
667    /// Only loads footer and index into memory, data blocks are fetched on-demand
668    pub async fn open(file_handle: FileHandle, cache_blocks: usize) -> io::Result<Self> {
669        let file_len = file_handle.len();
670        // Footer: data_end(8) + dict_offset(8) + num_docs(4) + has_dict(4) + version(4) + magic(4) = 32 bytes
671        if file_len < 32 {
672            return Err(io::Error::new(
673                io::ErrorKind::InvalidData,
674                "Store too small",
675            ));
676        }
677
678        // Read footer (32 bytes)
679        let footer = file_handle
680            .read_bytes_range(file_len - 32..file_len)
681            .await?;
682        let mut reader = footer.as_slice();
683        let data_end_offset = reader.read_u64::<LittleEndian>()?;
684        let dict_offset = reader.read_u64::<LittleEndian>()?;
685        let num_docs = reader.read_u32::<LittleEndian>()?;
686        let has_dict = reader.read_u32::<LittleEndian>()? != 0;
687        let version = reader.read_u32::<LittleEndian>()?;
688        let magic = reader.read_u32::<LittleEndian>()?;
689
690        if magic != STORE_MAGIC {
691            return Err(io::Error::new(
692                io::ErrorKind::InvalidData,
693                "Invalid store magic",
694            ));
695        }
696        if version != STORE_VERSION {
697            return Err(io::Error::new(
698                io::ErrorKind::InvalidData,
699                format!("Unsupported store version: {}", version),
700            ));
701        }
702
703        let index_end = file_len - 32;
704        if data_end_offset > index_end {
705            return Err(io::Error::new(
706                io::ErrorKind::InvalidData,
707                "store data section extends past its footer",
708            ));
709        }
710
711        // Load dictionary if present, and compute index_start in one pass
712        let (dict, index_start) = if has_dict {
713            if dict_offset < data_end_offset || dict_offset >= index_end {
714                return Err(io::Error::new(
715                    io::ErrorKind::InvalidData,
716                    "store dictionary offset is out of bounds",
717                ));
718            }
719            let dict_start = dict_offset;
720            let dict_header_end = dict_start.checked_add(4).ok_or_else(|| {
721                io::Error::new(
722                    io::ErrorKind::InvalidData,
723                    "store dictionary range overflow",
724                )
725            })?;
726            if dict_header_end > index_end {
727                return Err(io::Error::new(
728                    io::ErrorKind::UnexpectedEof,
729                    "store dictionary length is truncated",
730                ));
731            }
732            let dict_len_bytes = file_handle
733                .read_bytes_range(dict_start..dict_header_end)
734                .await?;
735            let dict_len = (&dict_len_bytes[..]).read_u32::<LittleEndian>()? as u64;
736            if dict_len > MAX_STORE_DICTIONARY_BYTES {
737                return Err(io::Error::new(
738                    io::ErrorKind::InvalidData,
739                    "store dictionary exceeds safety limit",
740                ));
741            }
742            let dict_end = dict_header_end.checked_add(dict_len).ok_or_else(|| {
743                io::Error::new(
744                    io::ErrorKind::InvalidData,
745                    "store dictionary range overflow",
746                )
747            })?;
748            if dict_end > index_end {
749                return Err(io::Error::new(
750                    io::ErrorKind::UnexpectedEof,
751                    "store dictionary is truncated",
752                ));
753            }
754            let dict_bytes = file_handle
755                .read_bytes_range(dict_header_end..dict_end)
756                .await?;
757            (
758                Some(CompressionDict::from_owned_bytes(dict_bytes)),
759                dict_end,
760            )
761        } else {
762            if dict_offset != 0 {
763                return Err(io::Error::new(
764                    io::ErrorKind::InvalidData,
765                    "store without a dictionary has a dictionary offset",
766                ));
767            }
768            (None, data_end_offset)
769        };
770
771        if index_start > index_end {
772            return Err(io::Error::new(
773                io::ErrorKind::InvalidData,
774                "store index offset is out of bounds",
775            ));
776        }
777
778        let index_bytes = file_handle.read_bytes_range(index_start..index_end).await?;
779        let mut reader = index_bytes.as_slice();
780
781        let num_blocks = reader.read_u32::<LittleEndian>()? as usize;
782        let required_index_bytes = num_blocks.checked_mul(20).ok_or_else(|| {
783            io::Error::new(io::ErrorKind::InvalidData, "store index size overflow")
784        })?;
785        if reader.len() != required_index_bytes {
786            return Err(io::Error::new(
787                io::ErrorKind::InvalidData,
788                "store index length is inconsistent",
789            ));
790        }
791        let mut index = Vec::new();
792        index
793            .try_reserve_exact(num_blocks)
794            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "too many store blocks"))?;
795
796        let mut expected_doc = 0u32;
797        let mut expected_offset = 0u64;
798
799        for _ in 0..num_blocks {
800            let first_doc_id = reader.read_u32::<LittleEndian>()?;
801            let offset = reader.read_u64::<LittleEndian>()?;
802            let length = reader.read_u32::<LittleEndian>()?;
803            let num_docs_in_block = reader.read_u32::<LittleEndian>()?;
804
805            let end = offset.checked_add(length as u64).ok_or_else(|| {
806                io::Error::new(io::ErrorKind::InvalidData, "store block range overflow")
807            })?;
808            if first_doc_id != expected_doc
809                || num_docs_in_block == 0
810                || offset != expected_offset
811                || end > data_end_offset
812            {
813                return Err(io::Error::new(
814                    io::ErrorKind::InvalidData,
815                    "store block index is inconsistent",
816                ));
817            }
818            expected_doc = expected_doc.checked_add(num_docs_in_block).ok_or_else(|| {
819                io::Error::new(io::ErrorKind::InvalidData, "store document count overflow")
820            })?;
821            expected_offset = end;
822
823            index.push(StoreBlockIndex {
824                first_doc_id,
825                offset,
826                length,
827                num_docs: num_docs_in_block,
828            });
829        }
830
831        if expected_doc != num_docs || expected_offset != data_end_offset {
832            return Err(io::Error::new(
833                io::ErrorKind::InvalidData,
834                "store footer totals do not match its block index",
835            ));
836        }
837
838        // Create lazy slice for data portion only
839        let data_slice = file_handle.slice(0..data_end_offset);
840
841        Ok(Self {
842            data_slice,
843            index,
844            num_docs,
845            dict,
846            cache: RwLock::new(StoreBlockCache::new(cache_blocks)),
847        })
848    }
849
850    /// Number of documents
851    pub fn num_docs(&self) -> u32 {
852        self.num_docs
853    }
854
855    /// Number of blocks currently in the cache
856    pub fn cached_blocks(&self) -> usize {
857        self.cache.read().blocks.len()
858    }
859
860    /// Heap bytes retained by decompressed blocks and their document offsets.
861    pub fn cached_bytes(&self) -> usize {
862        self.cache
863            .read()
864            .blocks
865            .values()
866            .map(|block| block.retained_bytes())
867            .sum()
868    }
869
870    /// Get a document by doc_id (async - may load block)
871    pub async fn get(&self, doc_id: DocId, schema: &Schema) -> io::Result<Option<Document>> {
872        if doc_id >= self.num_docs {
873            return Ok(None);
874        }
875
876        let t = crate::observe::Timer::start();
877        let (entry, block) = self.find_and_load_block(doc_id).await?;
878        let doc_bytes = block.doc_bytes(doc_id - entry.first_doc_id)?;
879        let result = deserialize_document(doc_bytes, schema).map(Some);
880        crate::observe::store_get(schema.index_label(), t.secs());
881        result
882    }
883
884    /// Get specific fields of a document by doc_id (async - may load block)
885    ///
886    /// Only deserializes the requested fields, skipping over unwanted data.
887    /// Much faster than `get()` when documents have large fields (text bodies,
888    /// vectors) that aren't needed for the response.
889    pub async fn get_fields(
890        &self,
891        doc_id: DocId,
892        schema: &Schema,
893        field_ids: &[u32],
894    ) -> io::Result<Option<Document>> {
895        if doc_id >= self.num_docs {
896            return Ok(None);
897        }
898
899        let t = crate::observe::Timer::start();
900        let (entry, block) = self.find_and_load_block(doc_id).await?;
901        let doc_bytes = block.doc_bytes(doc_id - entry.first_doc_id)?;
902        let result = deserialize_document_fields(doc_bytes, schema, field_ids).map(Some);
903        crate::observe::store_get(schema.index_label(), t.secs());
904        result
905    }
906
907    /// Find the block index entry and load/cache the block for a given doc_id
908    async fn find_and_load_block(
909        &self,
910        doc_id: DocId,
911    ) -> io::Result<(&StoreBlockIndex, Arc<CachedBlock>)> {
912        let block_idx = self
913            .index
914            .binary_search_by(|entry| {
915                if doc_id < entry.first_doc_id {
916                    std::cmp::Ordering::Greater
917                } else if doc_id >= entry.first_doc_id + entry.num_docs {
918                    std::cmp::Ordering::Less
919                } else {
920                    std::cmp::Ordering::Equal
921                }
922            })
923            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Doc not found in index"))?;
924
925        let entry = &self.index[block_idx];
926        let block = self.load_block(entry).await?;
927        Ok((entry, block))
928    }
929
930    async fn load_block(&self, entry: &StoreBlockIndex) -> io::Result<Arc<CachedBlock>> {
931        // Fast path: read lock to check cache (allows concurrent readers)
932        {
933            let cache = self.cache.read();
934            if let Some(block) = cache.peek(entry.first_doc_id) {
935                return Ok(block);
936            }
937        }
938        // Resolve a race where another reader inserted after our shared probe.
939        {
940            if let Some(block) = self.cache.write().get(entry.first_doc_id) {
941                return Ok(block);
942            }
943        }
944
945        // Load from FileSlice
946        let start = entry.offset;
947        let end = start.checked_add(entry.length as u64).ok_or_else(|| {
948            io::Error::new(io::ErrorKind::InvalidData, "store block range overflow")
949        })?;
950        let compressed = self.data_slice.read_bytes_range(start..end).await?;
951
952        // Use dictionary decompression if available
953        let decompressed = if let Some(ref dict) = self.dict {
954            crate::compression::decompress_with_dict_limited(
955                compressed.as_slice(),
956                dict,
957                MAX_STORE_BLOCK_BYTES,
958            )?
959        } else {
960            crate::compression::decompress_limited(compressed.as_slice(), MAX_STORE_BLOCK_BYTES)?
961        };
962
963        // Build offset table for O(1) doc lookup within the block
964        let cached = CachedBlock::build(decompressed, entry.num_docs)?;
965        let block = Arc::new(cached);
966
967        // Insert into cache
968        {
969            let mut cache = self.cache.write();
970            cache.insert(entry.first_doc_id, Arc::clone(&block));
971        }
972
973        Ok(block)
974    }
975}
976
977/// Deserialize only specific fields from document bytes.
978///
979/// Skips over unwanted fields without allocating their values — just advances
980/// the reader past their length-prefixed data. For large documents with many
981/// fields (e.g., full text body), this avoids allocating/copying data that
982/// the caller doesn't need.
983pub fn deserialize_document_fields(
984    data: &[u8],
985    schema: &Schema,
986    field_ids: &[u32],
987) -> io::Result<Document> {
988    deserialize_document_inner(data, schema, Some(field_ids))
989}
990
991/// Deserialize all fields from document bytes.
992///
993/// Delegates to the shared field-parsing core with no field filter.
994pub fn deserialize_document(data: &[u8], schema: &Schema) -> io::Result<Document> {
995    deserialize_document_inner(data, schema, None)
996}
997
998/// Shared deserialization core. `field_filter = None` means all fields wanted.
999fn deserialize_document_inner(
1000    data: &[u8],
1001    _schema: &Schema,
1002    field_filter: Option<&[u32]>,
1003) -> io::Result<Document> {
1004    use crate::dsl::Field;
1005
1006    let mut reader = data;
1007    let num_fields = reader.read_u16::<LittleEndian>()? as usize;
1008    let mut doc = Document::new();
1009
1010    for _ in 0..num_fields {
1011        let field_id = reader.read_u16::<LittleEndian>()?;
1012        let type_tag = reader.read_u8()?;
1013
1014        let wanted = field_filter.is_none_or(|ids| ids.contains(&(field_id as u32)));
1015
1016        match type_tag {
1017            0 => {
1018                // Text
1019                let len = reader.read_u32::<LittleEndian>()? as usize;
1020                let bytes = take_document_bytes(&mut reader, len, "text field")?;
1021                if wanted {
1022                    let s = std::str::from_utf8(bytes)
1023                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
1024                    doc.add_text(Field(field_id as u32), s);
1025                }
1026            }
1027            1 => {
1028                // U64
1029                let v = reader.read_u64::<LittleEndian>()?;
1030                if wanted {
1031                    doc.add_u64(Field(field_id as u32), v);
1032                }
1033            }
1034            2 => {
1035                // I64
1036                let v = reader.read_i64::<LittleEndian>()?;
1037                if wanted {
1038                    doc.add_i64(Field(field_id as u32), v);
1039                }
1040            }
1041            3 => {
1042                // F64
1043                let v = reader.read_f64::<LittleEndian>()?;
1044                if wanted {
1045                    doc.add_f64(Field(field_id as u32), v);
1046                }
1047            }
1048            4 => {
1049                // Bytes
1050                let len = reader.read_u32::<LittleEndian>()? as usize;
1051                let bytes = take_document_bytes(&mut reader, len, "byte field")?;
1052                if wanted {
1053                    doc.add_bytes(Field(field_id as u32), bytes.to_vec());
1054                }
1055            }
1056            5 => {
1057                // SparseVector
1058                let count = reader.read_u32::<LittleEndian>()? as usize;
1059                let byte_len = count.checked_mul(8).ok_or_else(|| {
1060                    io::Error::new(io::ErrorKind::InvalidData, "sparse vector size overflow")
1061                })?;
1062                let bytes = take_document_bytes(&mut reader, byte_len, "sparse vector")?;
1063                if wanted {
1064                    let mut entries = Vec::new();
1065                    entries.try_reserve_exact(count).map_err(|_| {
1066                        io::Error::new(io::ErrorKind::InvalidData, "sparse vector is too large")
1067                    })?;
1068                    let mut vector_reader = bytes;
1069                    for _ in 0..count {
1070                        let idx = vector_reader.read_u32::<LittleEndian>()?;
1071                        let val = vector_reader.read_f32::<LittleEndian>()?;
1072                        entries.push((idx, val));
1073                    }
1074                    doc.add_sparse_vector(Field(field_id as u32), entries);
1075                }
1076            }
1077            6 => {
1078                // DenseVector
1079                let count = reader.read_u32::<LittleEndian>()? as usize;
1080                let byte_len = count.checked_mul(4).ok_or_else(|| {
1081                    io::Error::new(io::ErrorKind::InvalidData, "dense vector size overflow")
1082                })?;
1083                let bytes = take_document_bytes(&mut reader, byte_len, "dense vector")?;
1084                if wanted {
1085                    let mut values = vec![0.0f32; count];
1086                    unsafe {
1087                        std::ptr::copy_nonoverlapping(
1088                            bytes.as_ptr(),
1089                            values.as_mut_ptr() as *mut u8,
1090                            byte_len,
1091                        );
1092                    }
1093                    doc.add_dense_vector(Field(field_id as u32), values);
1094                }
1095            }
1096            7 => {
1097                // Json
1098                let len = reader.read_u32::<LittleEndian>()? as usize;
1099                let bytes = take_document_bytes(&mut reader, len, "JSON field")?;
1100                if wanted {
1101                    let v: serde_json::Value = serde_json::from_slice(bytes)
1102                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
1103                    doc.add_json(Field(field_id as u32), v);
1104                }
1105            }
1106            8 => {
1107                // BinaryDenseVector
1108                let len = reader.read_u32::<LittleEndian>()? as usize;
1109                let bytes = take_document_bytes(&mut reader, len, "binary dense vector")?;
1110                if wanted {
1111                    doc.add_binary_dense_vector(Field(field_id as u32), bytes.to_vec());
1112                }
1113            }
1114            _ => {
1115                return Err(io::Error::new(
1116                    io::ErrorKind::InvalidData,
1117                    format!("Unknown field type tag: {}", type_tag),
1118                ));
1119            }
1120        }
1121    }
1122
1123    Ok(doc)
1124}
1125
1126fn take_document_bytes<'a>(reader: &mut &'a [u8], len: usize, field: &str) -> io::Result<&'a [u8]> {
1127    if len > reader.len() {
1128        return Err(io::Error::new(
1129            io::ErrorKind::UnexpectedEof,
1130            format!("{field} is truncated"),
1131        ));
1132    }
1133    let (value, remaining) = reader.split_at(len);
1134    *reader = remaining;
1135    Ok(value)
1136}
1137
1138/// Raw block info for store merging (without decompression)
1139#[derive(Debug, Clone)]
1140pub struct RawStoreBlock {
1141    pub first_doc_id: DocId,
1142    pub num_docs: u32,
1143    pub offset: u64,
1144    pub length: u32,
1145}
1146
1147/// Store merger - concatenates compressed blocks from multiple stores without recompression
1148///
1149/// This is much faster than rebuilding stores since it avoids:
1150/// - Decompressing blocks from source stores
1151/// - Re-serializing documents
1152/// - Re-compressing blocks at level 22
1153///
1154/// Limitations:
1155/// - All source stores must NOT use dictionaries (or use the same dictionary)
1156/// - Doc IDs are remapped sequentially
1157pub struct StoreMerger<'a, W: Write> {
1158    writer: &'a mut W,
1159    index: Vec<StoreBlockIndex>,
1160    current_offset: u64,
1161    next_doc_id: DocId,
1162}
1163
1164impl<'a, W: Write> StoreMerger<'a, W> {
1165    pub fn new(writer: &'a mut W) -> Self {
1166        Self {
1167            writer,
1168            index: Vec::new(),
1169            current_offset: 0,
1170            next_doc_id: 0,
1171        }
1172    }
1173
1174    /// Append raw compressed blocks from a store file
1175    ///
1176    /// `data_slice` should be the data portion of the store (before index/footer)
1177    /// `blocks` contains the block metadata from the source store
1178    pub async fn append_store(
1179        &mut self,
1180        data_slice: &FileHandle,
1181        blocks: &[RawStoreBlock],
1182    ) -> io::Result<()> {
1183        for block in blocks {
1184            // Read raw compressed block data
1185            let start = block.offset;
1186            let end = start.checked_add(block.length as u64).ok_or_else(|| {
1187                io::Error::new(io::ErrorKind::InvalidData, "store block range overflow")
1188            })?;
1189            if end > data_slice.len() {
1190                return Err(io::Error::new(
1191                    io::ErrorKind::UnexpectedEof,
1192                    "store block range is out of bounds",
1193                ));
1194            }
1195            let compressed_data = data_slice.read_bytes_range(start..end).await?;
1196
1197            // Write to output
1198            self.writer.write_all(compressed_data.as_slice())?;
1199
1200            // Add to index with remapped doc IDs
1201            self.index.push(StoreBlockIndex {
1202                first_doc_id: self.next_doc_id,
1203                offset: self.current_offset,
1204                length: block.length,
1205                num_docs: block.num_docs,
1206            });
1207
1208            self.current_offset = self
1209                .current_offset
1210                .checked_add(block.length as u64)
1211                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "store size overflow"))?;
1212            self.next_doc_id = self
1213                .next_doc_id
1214                .checked_add(block.num_docs)
1215                .ok_or_else(|| {
1216                    io::Error::new(io::ErrorKind::InvalidData, "store document count overflow")
1217                })?;
1218        }
1219
1220        Ok(())
1221    }
1222
1223    /// Append blocks from a dict-compressed store by decompressing and recompressing.
1224    ///
1225    /// For stores that use dictionary compression, raw blocks can't be stacked
1226    /// directly because the decompressor needs the original dictionary.
1227    /// This method decompresses each block with the source dict, then
1228    /// recompresses without a dictionary so the merged output is self-contained.
1229    pub async fn append_store_recompressing(&mut self, store: &AsyncStoreReader) -> io::Result<()> {
1230        let dict = store.dict();
1231        let data_slice = store.data_slice();
1232        let blocks = store.block_index();
1233
1234        for block in blocks {
1235            let start = block.offset;
1236            let end = start.checked_add(block.length as u64).ok_or_else(|| {
1237                io::Error::new(io::ErrorKind::InvalidData, "store block range overflow")
1238            })?;
1239            if end > data_slice.len() {
1240                return Err(io::Error::new(
1241                    io::ErrorKind::UnexpectedEof,
1242                    "store block range is out of bounds",
1243                ));
1244            }
1245            let compressed = data_slice.read_bytes_range(start..end).await?;
1246
1247            // Decompress with source dict (or without if no dict)
1248            let decompressed = if let Some(d) = dict {
1249                crate::compression::decompress_with_dict_limited(
1250                    compressed.as_slice(),
1251                    d,
1252                    MAX_STORE_BLOCK_BYTES,
1253                )?
1254            } else {
1255                crate::compression::decompress_limited(
1256                    compressed.as_slice(),
1257                    MAX_STORE_BLOCK_BYTES,
1258                )?
1259            };
1260
1261            // Recompress without dictionary
1262            let recompressed = crate::compression::compress(
1263                &decompressed,
1264                crate::compression::CompressionLevel::default(),
1265            )?;
1266
1267            self.writer.write_all(&recompressed)?;
1268
1269            self.index.push(StoreBlockIndex {
1270                first_doc_id: self.next_doc_id,
1271                offset: self.current_offset,
1272                length: u32::try_from(recompressed.len()).map_err(|_| {
1273                    io::Error::new(
1274                        io::ErrorKind::InvalidData,
1275                        "compressed store block too large",
1276                    )
1277                })?,
1278                num_docs: block.num_docs,
1279            });
1280
1281            self.current_offset = self
1282                .current_offset
1283                .checked_add(recompressed.len() as u64)
1284                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "store size overflow"))?;
1285            self.next_doc_id = self
1286                .next_doc_id
1287                .checked_add(block.num_docs)
1288                .ok_or_else(|| {
1289                    io::Error::new(io::ErrorKind::InvalidData, "store document count overflow")
1290                })?;
1291        }
1292
1293        Ok(())
1294    }
1295
1296    /// Finish writing the merged store
1297    pub fn finish(self) -> io::Result<u32> {
1298        let data_end_offset = self.current_offset;
1299
1300        // No dictionary support for merged stores (would need same dict across all sources)
1301        let dict_offset = 0u64;
1302
1303        // Write index + footer
1304        write_store_index_and_footer(
1305            self.writer,
1306            &self.index,
1307            data_end_offset,
1308            dict_offset,
1309            self.next_doc_id,
1310            false,
1311        )?;
1312
1313        Ok(self.next_doc_id)
1314    }
1315}
1316
1317impl AsyncStoreReader {
1318    /// Get raw block metadata for merging (without loading block data)
1319    pub fn raw_blocks(&self) -> Vec<RawStoreBlock> {
1320        self.index
1321            .iter()
1322            .map(|entry| RawStoreBlock {
1323                first_doc_id: entry.first_doc_id,
1324                num_docs: entry.num_docs,
1325                offset: entry.offset,
1326                length: entry.length,
1327            })
1328            .collect()
1329    }
1330
1331    /// Get the data slice for raw block access
1332    pub fn data_slice(&self) -> &FileHandle {
1333        &self.data_slice
1334    }
1335
1336    /// Check if this store uses a dictionary (incompatible with raw merging)
1337    pub fn has_dict(&self) -> bool {
1338        self.dict.is_some()
1339    }
1340
1341    /// Get the decompression dictionary (if any)
1342    pub fn dict(&self) -> Option<&CompressionDict> {
1343        self.dict.as_ref()
1344    }
1345
1346    /// Get block index for iteration
1347    pub(crate) fn block_index(&self) -> &[StoreBlockIndex] {
1348        &self.index
1349    }
1350}
1351
1352#[cfg(test)]
1353mod tests {
1354    use super::*;
1355
1356    #[test]
1357    fn cached_block_rejects_truncated_and_trailing_documents() {
1358        assert!(CachedBlock::build(vec![8, 0, 0, 0, 1], 1).is_err());
1359        assert!(CachedBlock::build(vec![0, 0, 0, 0, 1], 1).is_err());
1360    }
1361
1362    #[test]
1363    fn document_deserializer_rejects_length_prefixed_slice_overrun() {
1364        let schema = Schema::builder().build();
1365        let truncated_text = [1, 0, 0, 0, 0, 5, 0, 0, 0, b'x'];
1366        assert!(deserialize_document(&truncated_text, &schema).is_err());
1367
1368        let truncated_sparse = [1, 0, 0, 0, 5, 2, 0, 0, 0, 1, 0, 0, 0];
1369        assert!(deserialize_document(&truncated_sparse, &schema).is_err());
1370    }
1371}