1use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
13use lru::LruCache;
14use parking_lot::RwLock;
15use rustc_hash::FxHashMap;
16#[cfg(feature = "native")]
17use std::collections::{BTreeMap, HashMap};
18use std::io::{self, Write};
19use std::sync::Arc;
20#[cfg(feature = "native")]
21use std::sync::OnceLock;
22#[cfg(feature = "native")]
23use std::sync::atomic::{AtomicBool, Ordering};
24#[cfg(feature = "native")]
25use std::sync::mpsc::{Receiver, Sender, SyncSender};
26#[cfg(feature = "native")]
27use std::thread::JoinHandle;
28
29use crate::DocId;
30use crate::compression::CompressionDict;
31#[cfg(feature = "native")]
32use crate::compression::CompressionLevel;
33use crate::directories::FileHandle;
34use crate::dsl::{Document, Schema};
35
36const STORE_MAGIC: u32 = 0x53544F52; const STORE_VERSION: u32 = 2; pub const STORE_BLOCK_SIZE: usize = 16 * 1024;
44
45pub const DEFAULT_DICT_SIZE: usize = 4 * 1024;
47
48const MAX_STORE_BLOCK_BYTES: usize = 64 * 1024 * 1024;
52const MAX_STORE_DICTIONARY_BYTES: u64 = 16 * 1024 * 1024;
53
54#[cfg(feature = "native")]
56const DEFAULT_COMPRESSION_LEVEL: CompressionLevel = CompressionLevel(3);
57
58fn write_store_index_and_footer(
62 writer: &mut (impl Write + ?Sized),
63 index: &[StoreBlockIndex],
64 data_end_offset: u64,
65 dict_offset: u64,
66 num_docs: u32,
67 has_dict: bool,
68) -> io::Result<()> {
69 writer.write_u32::<LittleEndian>(u32::try_from(index.len()).map_err(|_| {
70 io::Error::new(
71 io::ErrorKind::InvalidInput,
72 "too many document store blocks",
73 )
74 })?)?;
75 for entry in index {
76 writer.write_u32::<LittleEndian>(entry.first_doc_id)?;
77 writer.write_u64::<LittleEndian>(entry.offset)?;
78 writer.write_u32::<LittleEndian>(entry.length)?;
79 writer.write_u32::<LittleEndian>(entry.num_docs)?;
80 }
81 writer.write_u64::<LittleEndian>(data_end_offset)?;
82 writer.write_u64::<LittleEndian>(dict_offset)?;
83 writer.write_u32::<LittleEndian>(num_docs)?;
84 writer.write_u32::<LittleEndian>(if has_dict { 1 } else { 0 })?;
85 writer.write_u32::<LittleEndian>(STORE_VERSION)?;
86 writer.write_u32::<LittleEndian>(STORE_MAGIC)?;
87 Ok(())
88}
89
90pub fn serialize_document(doc: &Document, schema: &Schema) -> io::Result<Vec<u8>> {
102 let mut buf = Vec::with_capacity(256);
103 serialize_document_into(doc, schema, &mut buf)?;
104 Ok(buf)
105}
106
107pub fn serialize_document_into(
110 doc: &Document,
111 schema: &Schema,
112 buf: &mut Vec<u8>,
113) -> io::Result<()> {
114 use crate::dsl::FieldValue;
115
116 buf.clear();
117
118 let is_stored = |field: &crate::dsl::Field, value: &FieldValue| -> bool {
120 if matches!(
122 value,
123 FieldValue::DenseVector(_) | FieldValue::BinaryDenseVector(_)
124 ) {
125 return false;
126 }
127 schema.get_field_entry(*field).is_some_and(|e| e.stored)
128 };
129
130 let stored_count = doc
131 .field_values()
132 .iter()
133 .filter(|(field, value)| is_stored(field, value))
134 .count();
135
136 buf.write_u16::<LittleEndian>(
137 u16::try_from(stored_count)
138 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "too many stored fields"))?,
139 )?;
140
141 for (field, value) in doc.field_values().iter().filter(|(f, v)| is_stored(f, v)) {
142 buf.write_u16::<LittleEndian>(u16::try_from(field.0).map_err(|_| {
143 io::Error::new(io::ErrorKind::InvalidInput, "stored field id exceeds u16")
144 })?)?;
145 match value {
146 FieldValue::Text(s) => {
147 buf.push(0);
148 let bytes = s.as_bytes();
149 buf.write_u32::<LittleEndian>(u32::try_from(bytes.len()).map_err(|_| {
150 io::Error::new(io::ErrorKind::InvalidInput, "stored text is too large")
151 })?)?;
152 buf.extend_from_slice(bytes);
153 }
154 FieldValue::U64(v) => {
155 buf.push(1);
156 buf.write_u64::<LittleEndian>(*v)?;
157 }
158 FieldValue::I64(v) => {
159 buf.push(2);
160 buf.write_i64::<LittleEndian>(*v)?;
161 }
162 FieldValue::F64(v) => {
163 buf.push(3);
164 buf.write_f64::<LittleEndian>(*v)?;
165 }
166 FieldValue::Bytes(b) => {
167 buf.push(4);
168 buf.write_u32::<LittleEndian>(u32::try_from(b.len()).map_err(|_| {
169 io::Error::new(
170 io::ErrorKind::InvalidInput,
171 "stored byte field is too large",
172 )
173 })?)?;
174 buf.extend_from_slice(b);
175 }
176 FieldValue::SparseVector(entries) => {
177 buf.push(5);
178 buf.write_u32::<LittleEndian>(u32::try_from(entries.len()).map_err(|_| {
179 io::Error::new(
180 io::ErrorKind::InvalidInput,
181 "stored sparse vector is too large",
182 )
183 })?)?;
184 for (idx, val) in entries {
185 buf.write_u32::<LittleEndian>(*idx)?;
186 buf.write_f32::<LittleEndian>(*val)?;
187 }
188 }
189 FieldValue::DenseVector(values) => {
190 buf.push(6);
191 buf.write_u32::<LittleEndian>(u32::try_from(values.len()).map_err(|_| {
192 io::Error::new(
193 io::ErrorKind::InvalidInput,
194 "stored dense vector is too large",
195 )
196 })?)?;
197 let byte_slice = unsafe {
199 std::slice::from_raw_parts(values.as_ptr() as *const u8, values.len() * 4)
200 };
201 buf.extend_from_slice(byte_slice);
202 }
203 FieldValue::Json(v) => {
204 buf.push(7);
205 let json_bytes = serde_json::to_vec(v)
206 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
207 buf.write_u32::<LittleEndian>(u32::try_from(json_bytes.len()).map_err(|_| {
208 io::Error::new(io::ErrorKind::InvalidInput, "stored JSON is too large")
209 })?)?;
210 buf.extend_from_slice(&json_bytes);
211 }
212 FieldValue::BinaryDenseVector(b) => {
213 buf.push(8);
214 buf.write_u32::<LittleEndian>(u32::try_from(b.len()).map_err(|_| {
215 io::Error::new(
216 io::ErrorKind::InvalidInput,
217 "stored binary dense vector is too large",
218 )
219 })?)?;
220 buf.extend_from_slice(b);
221 }
222 }
223 }
224
225 Ok(())
226}
227
228#[cfg(feature = "native")]
230struct CompressedBlock {
231 seq: usize,
232 first_doc_id: DocId,
233 num_docs: u32,
234 compressed: Vec<u8>,
235}
236
237#[cfg(feature = "native")]
238struct CompressionJob {
239 seq: usize,
240 first_doc_id: DocId,
241 num_docs: u32,
242 data: Vec<u8>,
243}
244
245#[cfg(feature = "native")]
246struct CompressionRequest {
247 job: CompressionJob,
248 dict: Option<Arc<CompressionDict>>,
249 compression_level: CompressionLevel,
250 results: Sender<io::Result<CompressedBlock>>,
251 cancelled: Arc<AtomicBool>,
252}
253
254#[cfg(feature = "native")]
260struct StoreCompressionExecutor {
261 jobs: Option<SyncSender<CompressionRequest>>,
262 workers: Vec<JoinHandle<()>>,
263 num_threads: usize,
264}
265
266#[cfg(feature = "native")]
267impl StoreCompressionExecutor {
268 fn new(num_threads: usize) -> Self {
269 let (job_sender, job_receiver) =
270 std::sync::mpsc::sync_channel::<CompressionRequest>(num_threads);
271 let job_receiver = Arc::new(std::sync::Mutex::new(job_receiver));
272 let mut workers = Vec::with_capacity(num_threads);
273
274 for worker_id in 0..num_threads {
275 let jobs = Arc::clone(&job_receiver);
276 let worker = std::thread::Builder::new()
277 .name(format!("hermes-store-compress-{num_threads}-{worker_id}"))
278 .spawn(move || {
279 loop {
280 let request = {
284 let receiver = jobs
285 .lock()
286 .unwrap_or_else(std::sync::PoisonError::into_inner);
287 receiver.recv()
288 };
289 let Ok(request) = request else {
290 break;
291 };
292 let CompressionRequest {
293 job,
294 dict,
295 compression_level,
296 results,
297 cancelled,
298 } = request;
299 if cancelled.load(Ordering::Acquire) {
300 continue;
301 }
302
303 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
308 let compressed = if let Some(ref dict) = dict {
309 crate::compression::compress_with_dict(
310 &job.data,
311 compression_level,
312 dict,
313 )
314 } else {
315 crate::compression::compress(&job.data, compression_level)
316 }?;
317 Ok(CompressedBlock {
318 seq: job.seq,
319 first_doc_id: job.first_doc_id,
320 num_docs: job.num_docs,
321 compressed,
322 })
323 }))
324 .unwrap_or_else(|_| {
325 Err(io::Error::other(
326 "document-store compression worker panicked",
327 ))
328 });
329
330 if !cancelled.load(Ordering::Acquire) {
334 let _ = results.send(result);
335 }
336 }
337 })
338 .expect("failed to spawn document-store compression worker");
339 workers.push(worker);
340 }
341
342 Self {
343 jobs: Some(job_sender),
344 workers,
345 num_threads,
346 }
347 }
348}
349
350#[cfg(feature = "native")]
351impl Drop for StoreCompressionExecutor {
352 fn drop(&mut self) {
353 self.jobs.take();
354 for worker in self.workers.drain(..) {
355 let _ = worker.join();
356 }
357 }
358}
359
360#[cfg(feature = "native")]
361static STORE_COMPRESSION_EXECUTORS: OnceLock<
362 parking_lot::Mutex<HashMap<usize, Arc<StoreCompressionExecutor>>>,
363> = OnceLock::new();
364
365#[cfg(feature = "native")]
366fn shared_store_compression_executor(num_threads: usize) -> Arc<StoreCompressionExecutor> {
367 let num_threads = num_threads.max(1);
368 let mut executors = STORE_COMPRESSION_EXECUTORS
369 .get_or_init(|| parking_lot::Mutex::new(HashMap::new()))
370 .lock();
371 if let Some(executor) = executors.get(&num_threads) {
372 return Arc::clone(executor);
373 }
374
375 let executor = Arc::new(StoreCompressionExecutor::new(num_threads));
376 executors.insert(num_threads, Arc::clone(&executor));
380 log::info!(
381 "[store] process-wide compression pool: {} thread(s)",
382 num_threads
383 );
384 executor
385}
386
387#[cfg(feature = "native")]
394struct StoreCompressionPool {
395 executor: Arc<StoreCompressionExecutor>,
396 results: Receiver<io::Result<CompressedBlock>>,
397 result_sender: Sender<io::Result<CompressedBlock>>,
398 dict: Option<Arc<CompressionDict>>,
399 compression_level: CompressionLevel,
400 cancelled: Arc<AtomicBool>,
401}
402
403#[cfg(feature = "native")]
404impl StoreCompressionPool {
405 fn new(
406 num_threads: usize,
407 dict: Option<Arc<CompressionDict>>,
408 compression_level: CompressionLevel,
409 ) -> Self {
410 let num_threads = num_threads.max(1);
414 let (result_sender, result_receiver) = std::sync::mpsc::channel();
415
416 Self {
417 executor: shared_store_compression_executor(num_threads),
418 results: result_receiver,
419 result_sender,
420 dict,
421 compression_level,
422 cancelled: Arc::new(AtomicBool::new(false)),
423 }
424 }
425
426 #[inline]
427 fn num_threads(&self) -> usize {
428 self.executor.num_threads
429 }
430
431 fn submit(&self, job: CompressionJob) -> io::Result<()> {
432 self.executor
433 .jobs
434 .as_ref()
435 .ok_or_else(|| io::Error::new(io::ErrorKind::BrokenPipe, "compression pool is closed"))?
436 .send(CompressionRequest {
437 job,
438 dict: self.dict.clone(),
439 compression_level: self.compression_level,
440 results: self.result_sender.clone(),
441 cancelled: Arc::clone(&self.cancelled),
442 })
443 .map_err(|_| {
444 io::Error::new(
445 io::ErrorKind::BrokenPipe,
446 "document-store compression workers stopped",
447 )
448 })
449 }
450
451 fn receive(&self) -> io::Result<CompressedBlock> {
452 self.results.recv().map_err(|_| {
453 io::Error::new(
454 io::ErrorKind::BrokenPipe,
455 "document-store compression result channel closed",
456 )
457 })?
458 }
459
460 fn shutdown(&mut self) -> io::Result<()> {
461 self.cancelled.store(true, Ordering::Release);
465 Ok(())
466 }
467}
468
469#[cfg(feature = "native")]
470impl Drop for StoreCompressionPool {
471 fn drop(&mut self) {
472 self.cancelled.store(true, Ordering::Release);
476 }
477}
478
479#[cfg(feature = "native")]
485pub struct EagerParallelStoreWriter<'a> {
486 writer: &'a mut dyn Write,
487 block_buffer: Vec<u8>,
488 serialize_buf: Vec<u8>,
490 workers: StoreCompressionPool,
491 ready_blocks: BTreeMap<usize, CompressedBlock>,
493 pending_results: usize,
495 max_in_flight: usize,
497 next_seq: usize,
498 next_write_seq: usize,
499 next_doc_id: DocId,
500 block_first_doc: DocId,
501 index: Vec<StoreBlockIndex>,
502 current_offset: u64,
503 dict: Option<Arc<CompressionDict>>,
504}
505
506#[cfg(feature = "native")]
507impl<'a> EagerParallelStoreWriter<'a> {
508 pub fn new(writer: &'a mut dyn Write, num_threads: usize) -> Self {
510 Self::with_compression_level(writer, num_threads, DEFAULT_COMPRESSION_LEVEL)
511 }
512
513 pub fn with_compression_level(
515 writer: &'a mut dyn Write,
516 num_threads: usize,
517 compression_level: CompressionLevel,
518 ) -> Self {
519 Self::with_optional_dict(writer, None, num_threads, compression_level)
520 }
521
522 pub fn with_dict(writer: &'a mut dyn Write, dict: CompressionDict, num_threads: usize) -> Self {
524 Self::with_dict_and_level(writer, dict, num_threads, DEFAULT_COMPRESSION_LEVEL)
525 }
526
527 pub fn with_dict_and_level(
529 writer: &'a mut dyn Write,
530 dict: CompressionDict,
531 num_threads: usize,
532 compression_level: CompressionLevel,
533 ) -> Self {
534 Self::with_optional_dict(writer, Some(Arc::new(dict)), num_threads, compression_level)
535 }
536
537 fn with_optional_dict(
538 writer: &'a mut dyn Write,
539 dict: Option<Arc<CompressionDict>>,
540 num_threads: usize,
541 compression_level: CompressionLevel,
542 ) -> Self {
543 let workers = StoreCompressionPool::new(num_threads, dict.clone(), compression_level);
544 let max_in_flight = workers.num_threads().saturating_mul(2).max(1);
547 Self {
548 writer,
549 block_buffer: Vec::with_capacity(STORE_BLOCK_SIZE),
550 serialize_buf: Vec::with_capacity(512),
551 workers,
552 ready_blocks: BTreeMap::new(),
553 pending_results: 0,
554 max_in_flight,
555 next_seq: 0,
556 next_write_seq: 0,
557 next_doc_id: 0,
558 block_first_doc: 0,
559 index: Vec::new(),
560 current_offset: 0,
561 dict,
562 }
563 }
564
565 pub fn store(&mut self, doc: &Document, schema: &Schema) -> io::Result<DocId> {
566 serialize_document_into(doc, schema, &mut self.serialize_buf)?;
567 if self.serialize_buf.len() > MAX_STORE_BLOCK_BYTES.saturating_sub(4) {
568 return Err(io::Error::new(
569 io::ErrorKind::InvalidInput,
570 "serialized document exceeds store block limit",
571 ));
572 }
573 let doc_id = self.next_doc_id;
574 self.next_doc_id = self
575 .next_doc_id
576 .checked_add(1)
577 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "document id overflow"))?;
578 self.block_buffer
579 .write_u32::<LittleEndian>(self.serialize_buf.len() as u32)?;
580 self.block_buffer.extend_from_slice(&self.serialize_buf);
581 if self.block_buffer.len() >= STORE_BLOCK_SIZE {
582 self.queue_compression()?;
583 }
584 Ok(doc_id)
585 }
586
587 pub fn store_raw(&mut self, doc_bytes: &[u8]) -> io::Result<DocId> {
589 if doc_bytes.len() > MAX_STORE_BLOCK_BYTES.saturating_sub(4) {
590 return Err(io::Error::new(
591 io::ErrorKind::InvalidInput,
592 "serialized document exceeds store block limit",
593 ));
594 }
595 let doc_id = self.next_doc_id;
596 self.next_doc_id = self
597 .next_doc_id
598 .checked_add(1)
599 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "document id overflow"))?;
600
601 self.block_buffer
602 .write_u32::<LittleEndian>(doc_bytes.len() as u32)?;
603 self.block_buffer.extend_from_slice(doc_bytes);
604
605 if self.block_buffer.len() >= STORE_BLOCK_SIZE {
606 self.queue_compression()?;
607 }
608
609 Ok(doc_id)
610 }
611
612 fn queue_compression(&mut self) -> io::Result<()> {
615 if self.block_buffer.is_empty() {
616 return Ok(());
617 }
618
619 let num_docs = self.next_doc_id - self.block_first_doc;
620 let data = std::mem::replace(&mut self.block_buffer, Vec::with_capacity(STORE_BLOCK_SIZE));
621 let seq = self.next_seq;
622 let first_doc_id = self.block_first_doc;
623
624 self.workers.submit(CompressionJob {
625 seq,
626 first_doc_id,
627 num_docs,
628 data,
629 })?;
630 self.next_seq = self
631 .next_seq
632 .checked_add(1)
633 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "store block overflow"))?;
634 self.pending_results += 1;
635 self.block_first_doc = self.next_doc_id;
636
637 while self.outstanding_blocks() >= self.max_in_flight {
638 self.receive_and_write_ready()?;
639 }
640
641 Ok(())
642 }
643
644 #[inline]
645 fn outstanding_blocks(&self) -> usize {
646 self.next_seq - self.next_write_seq
647 }
648
649 fn receive_and_write_ready(&mut self) -> io::Result<()> {
650 if self.pending_results == 0 {
651 return Err(io::Error::new(
652 io::ErrorKind::BrokenPipe,
653 "missing document-store compression result",
654 ));
655 }
656 let block = self.workers.receive()?;
657 self.pending_results -= 1;
658 if block.seq < self.next_write_seq || self.ready_blocks.insert(block.seq, block).is_some() {
659 return Err(io::Error::new(
660 io::ErrorKind::InvalidData,
661 "duplicate document-store compression result",
662 ));
663 }
664 self.write_ready_blocks()
665 }
666
667 fn write_ready_blocks(&mut self) -> io::Result<()> {
668 while let Some(block) = self.ready_blocks.remove(&self.next_write_seq) {
669 let length = u32::try_from(block.compressed.len()).map_err(|_| {
670 io::Error::new(
671 io::ErrorKind::InvalidData,
672 "compressed store block too large",
673 )
674 })?;
675 self.writer.write_all(&block.compressed)?;
676 self.index.push(StoreBlockIndex {
677 first_doc_id: block.first_doc_id,
678 offset: self.current_offset,
679 length,
680 num_docs: block.num_docs,
681 });
682 self.current_offset = self
683 .current_offset
684 .checked_add(u64::from(length))
685 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "store size overflow"))?;
686 self.next_write_seq += 1;
687 }
688 Ok(())
689 }
690
691 pub fn finish(mut self) -> io::Result<u32> {
692 self.queue_compression()?;
695 while self.next_write_seq < self.next_seq {
696 self.receive_and_write_ready()?;
697 }
698 debug_assert_eq!(self.pending_results, 0);
699 debug_assert!(self.ready_blocks.is_empty());
700 self.workers.shutdown()?;
701
702 if self.index.is_empty() {
703 write_store_index_and_footer(&mut self.writer, &[], 0, 0, 0, false)?;
704 return Ok(0);
705 }
706
707 let dict_offset = if let Some(ref dict) = self.dict {
709 let offset = self.current_offset;
710 let dict_bytes = dict.as_bytes();
711 self.writer
712 .write_u32::<LittleEndian>(dict_bytes.len() as u32)?;
713 self.writer.write_all(dict_bytes)?;
714 Some(offset)
715 } else {
716 None
717 };
718
719 write_store_index_and_footer(
721 &mut self.writer,
722 &self.index,
723 self.current_offset,
724 dict_offset.unwrap_or(0),
725 self.next_doc_id,
726 self.dict.is_some(),
727 )?;
728
729 Ok(self.next_doc_id)
730 }
731}
732
733#[derive(Debug, Clone)]
735pub(crate) struct StoreBlockIndex {
736 pub(crate) first_doc_id: DocId,
737 pub(crate) offset: u64,
738 pub(crate) length: u32,
739 pub(crate) num_docs: u32,
740}
741
742pub struct AsyncStoreReader {
744 data_slice: FileHandle,
746 index: Vec<StoreBlockIndex>,
748 num_docs: u32,
749 dict: Option<CompressionDict>,
751 cache: Arc<SharedStoreCache>,
753 cache_namespace: StoreCacheNamespace,
755}
756
757struct CachedBlock {
763 data: Vec<u8>,
764 offsets: Vec<u32>,
767}
768
769impl CachedBlock {
770 fn build(data: Vec<u8>, num_docs: u32) -> io::Result<Self> {
771 if num_docs as usize > data.len() / 4 {
772 return Err(io::Error::new(
773 io::ErrorKind::InvalidData,
774 "store block document count exceeds block length",
775 ));
776 }
777 let mut offsets = Vec::new();
778 offsets.try_reserve_exact(num_docs as usize).map_err(|_| {
779 io::Error::new(
780 io::ErrorKind::InvalidData,
781 "store block has too many documents",
782 )
783 })?;
784 let mut pos = 0usize;
785 for _ in 0..num_docs {
786 let length_end = pos.checked_add(4).ok_or_else(|| {
787 io::Error::new(io::ErrorKind::InvalidData, "store block offset overflow")
788 })?;
789 if length_end > data.len() {
790 return Err(io::Error::new(
791 io::ErrorKind::InvalidData,
792 "truncated block while building offset table",
793 ));
794 }
795 offsets.push(u32::try_from(pos).map_err(|_| {
796 io::Error::new(io::ErrorKind::InvalidData, "store block offset exceeds u32")
797 })?);
798 let doc_len =
799 u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
800 as usize;
801 pos = length_end.checked_add(doc_len).ok_or_else(|| {
802 io::Error::new(io::ErrorKind::InvalidData, "store document length overflow")
803 })?;
804 if pos > data.len() {
805 return Err(io::Error::new(
806 io::ErrorKind::UnexpectedEof,
807 "store document is truncated",
808 ));
809 }
810 }
811 if pos != data.len() {
812 return Err(io::Error::new(
813 io::ErrorKind::InvalidData,
814 "store block contains trailing data",
815 ));
816 }
817 Ok(Self { data, offsets })
818 }
819
820 fn doc_bytes(&self, doc_offset_in_block: u32) -> io::Result<&[u8]> {
822 let idx = doc_offset_in_block as usize;
823 if idx >= self.offsets.len() {
824 return Err(io::Error::new(
825 io::ErrorKind::InvalidData,
826 "doc offset out of range",
827 ));
828 }
829 let start = self.offsets[idx] as usize;
830 let data_start = start.checked_add(4).ok_or_else(|| {
831 io::Error::new(io::ErrorKind::InvalidData, "store document offset overflow")
832 })?;
833 if data_start > self.data.len() {
834 return Err(io::Error::new(
835 io::ErrorKind::InvalidData,
836 "truncated doc length",
837 ));
838 }
839 let doc_len = u32::from_le_bytes([
840 self.data[start],
841 self.data[start + 1],
842 self.data[start + 2],
843 self.data[start + 3],
844 ]) as usize;
845 let data_end = data_start.checked_add(doc_len).ok_or_else(|| {
846 io::Error::new(io::ErrorKind::InvalidData, "store document length overflow")
847 })?;
848 if data_end > self.data.len() {
849 return Err(io::Error::new(
850 io::ErrorKind::InvalidData,
851 "doc data overflow",
852 ));
853 }
854 Ok(&self.data[data_start..data_end])
855 }
856
857 #[inline]
858 fn retained_bytes(&self) -> usize {
859 self.data.capacity() + self.offsets.capacity() * std::mem::size_of::<u32>()
860 }
861}
862
863#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
864struct StoreCacheNamespace {
865 directory: usize,
866 segment: u128,
867}
868
869#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
870struct StoreCacheKey {
871 namespace: StoreCacheNamespace,
872 first_doc_id: DocId,
873}
874
875struct SharedStoreCacheState {
876 blocks: LruCache<StoreCacheKey, Arc<CachedBlock>>,
877 retained_bytes: usize,
878 namespace_bytes: FxHashMap<StoreCacheNamespace, usize>,
879 namespace_readers: FxHashMap<StoreCacheNamespace, usize>,
880}
881
882pub(crate) struct SharedStoreCache {
891 state: RwLock<SharedStoreCacheState>,
892 max_bytes: usize,
893 max_entry_bytes: usize,
897}
898
899impl std::fmt::Debug for SharedStoreCache {
900 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
901 formatter
902 .debug_struct("SharedStoreCache")
903 .field("max_bytes", &self.max_bytes)
904 .field("max_entry_bytes", &self.max_entry_bytes)
905 .field("retained_bytes", &self.total_bytes())
906 .finish()
907 }
908}
909
910impl SharedStoreCache {
911 const MAX_ADMITTED_ENTRY_BYTES: usize = 8 * 1024 * 1024;
912
913 pub(crate) fn new(max_bytes: usize) -> Self {
914 Self::with_limits(max_bytes, max_bytes.min(Self::MAX_ADMITTED_ENTRY_BYTES))
915 }
916
917 fn with_limits(max_bytes: usize, max_entry_bytes: usize) -> Self {
918 Self {
919 state: RwLock::new(SharedStoreCacheState {
920 blocks: LruCache::unbounded(),
921 retained_bytes: 0,
922 namespace_bytes: FxHashMap::default(),
923 namespace_readers: FxHashMap::default(),
924 }),
925 max_bytes,
926 max_entry_bytes: max_entry_bytes.min(max_bytes),
927 }
928 }
929
930 fn register(&self, namespace: StoreCacheNamespace) {
931 if self.max_bytes == 0 {
932 return;
933 }
934 let mut state = self.state.write();
935 *state.namespace_readers.entry(namespace).or_default() += 1;
936 }
937
938 fn unregister(&self, namespace: StoreCacheNamespace) {
939 if self.max_bytes == 0 {
940 return;
941 }
942 let mut state = self.state.write();
943 let Some(readers) = state.namespace_readers.get_mut(&namespace) else {
944 return;
945 };
946 *readers -= 1;
947 if *readers > 0 {
948 return;
949 }
950 state.namespace_readers.remove(&namespace);
951
952 let keys: Vec<_> = state
956 .blocks
957 .iter()
958 .filter_map(|(key, _)| (key.namespace == namespace).then_some(*key))
959 .collect();
960 for key in keys {
961 if let Some(block) = state.blocks.pop(&key) {
962 state.retained_bytes = state.retained_bytes.saturating_sub(block.retained_bytes());
963 }
964 }
965 state.namespace_bytes.remove(&namespace);
966 }
967
968 fn get(&self, key: StoreCacheKey) -> Option<Arc<CachedBlock>> {
969 self.state.read().blocks.peek(&key).map(Arc::clone)
973 }
974
975 fn insert(&self, key: StoreCacheKey, block: Arc<CachedBlock>) -> Arc<CachedBlock> {
978 let bytes = block.retained_bytes();
979 if self.max_bytes == 0 || bytes == 0 || bytes > self.max_entry_bytes {
980 return block;
981 }
982
983 let mut state = self.state.write();
984 if let Some(existing) = state.blocks.get(&key) {
985 return Arc::clone(existing);
986 }
987
988 state.retained_bytes = state.retained_bytes.saturating_add(bytes);
989 *state.namespace_bytes.entry(key.namespace).or_default() = state
990 .namespace_bytes
991 .get(&key.namespace)
992 .copied()
993 .unwrap_or(0)
994 .saturating_add(bytes);
995 state.blocks.put(key, Arc::clone(&block));
996
997 while state.retained_bytes > self.max_bytes {
998 let Some((evicted_key, evicted)) = state.blocks.pop_lru() else {
999 state.retained_bytes = 0;
1000 state.namespace_bytes.clear();
1001 break;
1002 };
1003 let evicted_bytes = evicted.retained_bytes();
1004 state.retained_bytes = state.retained_bytes.saturating_sub(evicted_bytes);
1005 if let Some(namespace_bytes) = state.namespace_bytes.get_mut(&evicted_key.namespace) {
1006 *namespace_bytes = namespace_bytes.saturating_sub(evicted_bytes);
1007 if *namespace_bytes == 0 {
1008 state.namespace_bytes.remove(&evicted_key.namespace);
1009 }
1010 }
1011 }
1012 block
1013 }
1014
1015 pub(crate) fn total_bytes(&self) -> usize {
1016 self.state.read().retained_bytes
1017 }
1018
1019 pub(crate) fn total_blocks(&self) -> usize {
1020 self.state.read().blocks.len()
1021 }
1022
1023 fn namespace_bytes(&self, namespace: StoreCacheNamespace) -> usize {
1024 self.state
1025 .read()
1026 .namespace_bytes
1027 .get(&namespace)
1028 .copied()
1029 .unwrap_or(0)
1030 }
1031
1032 fn namespace_blocks(&self, namespace: StoreCacheNamespace) -> usize {
1033 self.state
1034 .read()
1035 .blocks
1036 .iter()
1037 .filter(|(key, _)| key.namespace == namespace)
1038 .count()
1039 }
1040}
1041
1042impl Drop for AsyncStoreReader {
1043 fn drop(&mut self) {
1044 self.cache.unregister(self.cache_namespace);
1045 }
1046}
1047
1048impl AsyncStoreReader {
1049 pub(crate) async fn open(
1052 file_handle: FileHandle,
1053 directory_namespace: usize,
1054 segment_namespace: u128,
1055 cache: Arc<SharedStoreCache>,
1056 ) -> io::Result<Self> {
1057 let file_len = file_handle.len();
1058 if file_len < 32 {
1060 return Err(io::Error::new(
1061 io::ErrorKind::InvalidData,
1062 "Store too small",
1063 ));
1064 }
1065
1066 let footer = file_handle
1068 .read_bytes_range(file_len - 32..file_len)
1069 .await?;
1070 let mut reader = footer.as_slice();
1071 let data_end_offset = reader.read_u64::<LittleEndian>()?;
1072 let dict_offset = reader.read_u64::<LittleEndian>()?;
1073 let num_docs = reader.read_u32::<LittleEndian>()?;
1074 let has_dict = reader.read_u32::<LittleEndian>()? != 0;
1075 let version = reader.read_u32::<LittleEndian>()?;
1076 let magic = reader.read_u32::<LittleEndian>()?;
1077
1078 if magic != STORE_MAGIC {
1079 return Err(io::Error::new(
1080 io::ErrorKind::InvalidData,
1081 "Invalid store magic",
1082 ));
1083 }
1084 if version != STORE_VERSION {
1085 return Err(io::Error::new(
1086 io::ErrorKind::InvalidData,
1087 format!("Unsupported store version: {}", version),
1088 ));
1089 }
1090
1091 let index_end = file_len - 32;
1092 if data_end_offset > index_end {
1093 return Err(io::Error::new(
1094 io::ErrorKind::InvalidData,
1095 "store data section extends past its footer",
1096 ));
1097 }
1098
1099 let (dict, index_start) = if has_dict {
1101 if dict_offset < data_end_offset || dict_offset >= index_end {
1102 return Err(io::Error::new(
1103 io::ErrorKind::InvalidData,
1104 "store dictionary offset is out of bounds",
1105 ));
1106 }
1107 let dict_start = dict_offset;
1108 let dict_header_end = dict_start.checked_add(4).ok_or_else(|| {
1109 io::Error::new(
1110 io::ErrorKind::InvalidData,
1111 "store dictionary range overflow",
1112 )
1113 })?;
1114 if dict_header_end > index_end {
1115 return Err(io::Error::new(
1116 io::ErrorKind::UnexpectedEof,
1117 "store dictionary length is truncated",
1118 ));
1119 }
1120 let dict_len_bytes = file_handle
1121 .read_bytes_range(dict_start..dict_header_end)
1122 .await?;
1123 let dict_len = (&dict_len_bytes[..]).read_u32::<LittleEndian>()? as u64;
1124 if dict_len > MAX_STORE_DICTIONARY_BYTES {
1125 return Err(io::Error::new(
1126 io::ErrorKind::InvalidData,
1127 "store dictionary exceeds safety limit",
1128 ));
1129 }
1130 let dict_end = dict_header_end.checked_add(dict_len).ok_or_else(|| {
1131 io::Error::new(
1132 io::ErrorKind::InvalidData,
1133 "store dictionary range overflow",
1134 )
1135 })?;
1136 if dict_end > index_end {
1137 return Err(io::Error::new(
1138 io::ErrorKind::UnexpectedEof,
1139 "store dictionary is truncated",
1140 ));
1141 }
1142 let dict_bytes = file_handle
1143 .read_bytes_range(dict_header_end..dict_end)
1144 .await?;
1145 (
1146 Some(CompressionDict::from_owned_bytes(dict_bytes)),
1147 dict_end,
1148 )
1149 } else {
1150 if dict_offset != 0 {
1151 return Err(io::Error::new(
1152 io::ErrorKind::InvalidData,
1153 "store without a dictionary has a dictionary offset",
1154 ));
1155 }
1156 (None, data_end_offset)
1157 };
1158
1159 if index_start > index_end {
1160 return Err(io::Error::new(
1161 io::ErrorKind::InvalidData,
1162 "store index offset is out of bounds",
1163 ));
1164 }
1165
1166 let index_bytes = file_handle.read_bytes_range(index_start..index_end).await?;
1167 let mut reader = index_bytes.as_slice();
1168
1169 let num_blocks = reader.read_u32::<LittleEndian>()? as usize;
1170 let required_index_bytes = num_blocks.checked_mul(20).ok_or_else(|| {
1171 io::Error::new(io::ErrorKind::InvalidData, "store index size overflow")
1172 })?;
1173 if reader.len() != required_index_bytes {
1174 return Err(io::Error::new(
1175 io::ErrorKind::InvalidData,
1176 "store index length is inconsistent",
1177 ));
1178 }
1179 let mut index = Vec::new();
1180 index
1181 .try_reserve_exact(num_blocks)
1182 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "too many store blocks"))?;
1183
1184 let mut expected_doc = 0u32;
1185 let mut expected_offset = 0u64;
1186
1187 for _ in 0..num_blocks {
1188 let first_doc_id = reader.read_u32::<LittleEndian>()?;
1189 let offset = reader.read_u64::<LittleEndian>()?;
1190 let length = reader.read_u32::<LittleEndian>()?;
1191 let num_docs_in_block = reader.read_u32::<LittleEndian>()?;
1192
1193 let end = offset.checked_add(length as u64).ok_or_else(|| {
1194 io::Error::new(io::ErrorKind::InvalidData, "store block range overflow")
1195 })?;
1196 if first_doc_id != expected_doc
1197 || num_docs_in_block == 0
1198 || offset != expected_offset
1199 || end > data_end_offset
1200 {
1201 return Err(io::Error::new(
1202 io::ErrorKind::InvalidData,
1203 "store block index is inconsistent",
1204 ));
1205 }
1206 expected_doc = expected_doc.checked_add(num_docs_in_block).ok_or_else(|| {
1207 io::Error::new(io::ErrorKind::InvalidData, "store document count overflow")
1208 })?;
1209 expected_offset = end;
1210
1211 index.push(StoreBlockIndex {
1212 first_doc_id,
1213 offset,
1214 length,
1215 num_docs: num_docs_in_block,
1216 });
1217 }
1218
1219 if expected_doc != num_docs || expected_offset != data_end_offset {
1220 return Err(io::Error::new(
1221 io::ErrorKind::InvalidData,
1222 "store footer totals do not match its block index",
1223 ));
1224 }
1225
1226 let data_slice = file_handle.slice(0..data_end_offset);
1228
1229 let cache_namespace = StoreCacheNamespace {
1230 directory: directory_namespace,
1231 segment: segment_namespace,
1232 };
1233 cache.register(cache_namespace);
1234 Ok(Self {
1235 data_slice,
1236 index,
1237 num_docs,
1238 dict,
1239 cache,
1240 cache_namespace,
1241 })
1242 }
1243
1244 pub fn num_docs(&self) -> u32 {
1246 self.num_docs
1247 }
1248
1249 pub fn cached_blocks(&self) -> usize {
1251 self.cache.namespace_blocks(self.cache_namespace)
1252 }
1253
1254 pub fn cached_bytes(&self) -> usize {
1256 self.cache.namespace_bytes(self.cache_namespace)
1257 }
1258
1259 pub async fn get(&self, doc_id: DocId, schema: &Schema) -> io::Result<Option<Document>> {
1261 if doc_id >= self.num_docs {
1262 return Ok(None);
1263 }
1264
1265 let t = crate::observe::Timer::start();
1266 let (entry, block) = self.find_and_load_block(doc_id).await?;
1267 let doc_bytes = block.doc_bytes(doc_id - entry.first_doc_id)?;
1268 let result = deserialize_document(doc_bytes, schema).map(Some);
1269 crate::observe::store_get(schema.index_label(), t.secs());
1270 result
1271 }
1272
1273 pub async fn get_fields(
1279 &self,
1280 doc_id: DocId,
1281 schema: &Schema,
1282 field_ids: &[u32],
1283 ) -> io::Result<Option<Document>> {
1284 if doc_id >= self.num_docs {
1285 return Ok(None);
1286 }
1287
1288 let t = crate::observe::Timer::start();
1289 let (entry, block) = self.find_and_load_block(doc_id).await?;
1290 let doc_bytes = block.doc_bytes(doc_id - entry.first_doc_id)?;
1291 let result = deserialize_document_fields(doc_bytes, schema, field_ids).map(Some);
1292 crate::observe::store_get(schema.index_label(), t.secs());
1293 result
1294 }
1295
1296 async fn find_and_load_block(
1298 &self,
1299 doc_id: DocId,
1300 ) -> io::Result<(&StoreBlockIndex, Arc<CachedBlock>)> {
1301 let block_idx = self
1302 .index
1303 .binary_search_by(|entry| {
1304 if doc_id < entry.first_doc_id {
1305 std::cmp::Ordering::Greater
1306 } else if doc_id >= entry.first_doc_id + entry.num_docs {
1307 std::cmp::Ordering::Less
1308 } else {
1309 std::cmp::Ordering::Equal
1310 }
1311 })
1312 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Doc not found in index"))?;
1313
1314 let entry = &self.index[block_idx];
1315 let block = self.load_block(entry).await?;
1316 Ok((entry, block))
1317 }
1318
1319 async fn load_block(&self, entry: &StoreBlockIndex) -> io::Result<Arc<CachedBlock>> {
1320 let key = StoreCacheKey {
1321 namespace: self.cache_namespace,
1322 first_doc_id: entry.first_doc_id,
1323 };
1324 if let Some(block) = self.cache.get(key) {
1325 return Ok(block);
1326 }
1327
1328 let start = entry.offset;
1330 let end = start.checked_add(entry.length as u64).ok_or_else(|| {
1331 io::Error::new(io::ErrorKind::InvalidData, "store block range overflow")
1332 })?;
1333 let compressed = self.data_slice.read_bytes_range(start..end).await?;
1334
1335 let decompressed = if let Some(ref dict) = self.dict {
1337 crate::compression::decompress_with_dict_limited(
1338 compressed.as_slice(),
1339 dict,
1340 MAX_STORE_BLOCK_BYTES,
1341 )?
1342 } else {
1343 crate::compression::decompress_limited(compressed.as_slice(), MAX_STORE_BLOCK_BYTES)?
1344 };
1345
1346 let cached = CachedBlock::build(decompressed, entry.num_docs)?;
1348 Ok(self.cache.insert(key, Arc::new(cached)))
1349 }
1350}
1351
1352pub fn deserialize_document_fields(
1359 data: &[u8],
1360 schema: &Schema,
1361 field_ids: &[u32],
1362) -> io::Result<Document> {
1363 deserialize_document_inner(data, schema, Some(field_ids))
1364}
1365
1366pub fn deserialize_document(data: &[u8], schema: &Schema) -> io::Result<Document> {
1370 deserialize_document_inner(data, schema, None)
1371}
1372
1373fn deserialize_document_inner(
1375 data: &[u8],
1376 _schema: &Schema,
1377 field_filter: Option<&[u32]>,
1378) -> io::Result<Document> {
1379 use crate::dsl::Field;
1380
1381 let mut reader = data;
1382 let num_fields = reader.read_u16::<LittleEndian>()? as usize;
1383 let mut doc = Document::new();
1384
1385 for _ in 0..num_fields {
1386 let field_id = reader.read_u16::<LittleEndian>()?;
1387 let type_tag = reader.read_u8()?;
1388
1389 let wanted = field_filter.is_none_or(|ids| ids.contains(&(field_id as u32)));
1390
1391 match type_tag {
1392 0 => {
1393 let len = reader.read_u32::<LittleEndian>()? as usize;
1395 let bytes = take_document_bytes(&mut reader, len, "text field")?;
1396 if wanted {
1397 let s = std::str::from_utf8(bytes)
1398 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
1399 doc.add_text(Field(field_id as u32), s);
1400 }
1401 }
1402 1 => {
1403 let v = reader.read_u64::<LittleEndian>()?;
1405 if wanted {
1406 doc.add_u64(Field(field_id as u32), v);
1407 }
1408 }
1409 2 => {
1410 let v = reader.read_i64::<LittleEndian>()?;
1412 if wanted {
1413 doc.add_i64(Field(field_id as u32), v);
1414 }
1415 }
1416 3 => {
1417 let v = reader.read_f64::<LittleEndian>()?;
1419 if wanted {
1420 doc.add_f64(Field(field_id as u32), v);
1421 }
1422 }
1423 4 => {
1424 let len = reader.read_u32::<LittleEndian>()? as usize;
1426 let bytes = take_document_bytes(&mut reader, len, "byte field")?;
1427 if wanted {
1428 doc.add_bytes(Field(field_id as u32), bytes.to_vec());
1429 }
1430 }
1431 5 => {
1432 let count = reader.read_u32::<LittleEndian>()? as usize;
1434 let byte_len = count.checked_mul(8).ok_or_else(|| {
1435 io::Error::new(io::ErrorKind::InvalidData, "sparse vector size overflow")
1436 })?;
1437 let bytes = take_document_bytes(&mut reader, byte_len, "sparse vector")?;
1438 if wanted {
1439 let mut entries = Vec::new();
1440 entries.try_reserve_exact(count).map_err(|_| {
1441 io::Error::new(io::ErrorKind::InvalidData, "sparse vector is too large")
1442 })?;
1443 let mut vector_reader = bytes;
1444 for _ in 0..count {
1445 let idx = vector_reader.read_u32::<LittleEndian>()?;
1446 let val = vector_reader.read_f32::<LittleEndian>()?;
1447 entries.push((idx, val));
1448 }
1449 doc.add_sparse_vector(Field(field_id as u32), entries);
1450 }
1451 }
1452 6 => {
1453 let count = reader.read_u32::<LittleEndian>()? as usize;
1455 let byte_len = count.checked_mul(4).ok_or_else(|| {
1456 io::Error::new(io::ErrorKind::InvalidData, "dense vector size overflow")
1457 })?;
1458 let bytes = take_document_bytes(&mut reader, byte_len, "dense vector")?;
1459 if wanted {
1460 let mut values = vec![0.0f32; count];
1461 unsafe {
1462 std::ptr::copy_nonoverlapping(
1463 bytes.as_ptr(),
1464 values.as_mut_ptr() as *mut u8,
1465 byte_len,
1466 );
1467 }
1468 doc.add_dense_vector(Field(field_id as u32), values);
1469 }
1470 }
1471 7 => {
1472 let len = reader.read_u32::<LittleEndian>()? as usize;
1474 let bytes = take_document_bytes(&mut reader, len, "JSON field")?;
1475 if wanted {
1476 let v: serde_json::Value = serde_json::from_slice(bytes)
1477 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
1478 doc.add_json(Field(field_id as u32), v);
1479 }
1480 }
1481 8 => {
1482 let len = reader.read_u32::<LittleEndian>()? as usize;
1484 let bytes = take_document_bytes(&mut reader, len, "binary dense vector")?;
1485 if wanted {
1486 doc.add_binary_dense_vector(Field(field_id as u32), bytes.to_vec());
1487 }
1488 }
1489 _ => {
1490 return Err(io::Error::new(
1491 io::ErrorKind::InvalidData,
1492 format!("Unknown field type tag: {}", type_tag),
1493 ));
1494 }
1495 }
1496 }
1497
1498 Ok(doc)
1499}
1500
1501fn take_document_bytes<'a>(reader: &mut &'a [u8], len: usize, field: &str) -> io::Result<&'a [u8]> {
1502 if len > reader.len() {
1503 return Err(io::Error::new(
1504 io::ErrorKind::UnexpectedEof,
1505 format!("{field} is truncated"),
1506 ));
1507 }
1508 let (value, remaining) = reader.split_at(len);
1509 *reader = remaining;
1510 Ok(value)
1511}
1512
1513#[derive(Debug, Clone)]
1515pub struct RawStoreBlock {
1516 pub first_doc_id: DocId,
1517 pub num_docs: u32,
1518 pub offset: u64,
1519 pub length: u32,
1520}
1521
1522pub struct StoreMerger<'a, W: Write> {
1533 writer: &'a mut W,
1534 index: Vec<StoreBlockIndex>,
1535 current_offset: u64,
1536 next_doc_id: DocId,
1537}
1538
1539impl<'a, W: Write> StoreMerger<'a, W> {
1540 pub fn new(writer: &'a mut W) -> Self {
1541 Self {
1542 writer,
1543 index: Vec::new(),
1544 current_offset: 0,
1545 next_doc_id: 0,
1546 }
1547 }
1548
1549 pub async fn append_store(
1554 &mut self,
1555 data_slice: &FileHandle,
1556 blocks: &[RawStoreBlock],
1557 cancellation: Option<&std::sync::atomic::AtomicBool>,
1558 ) -> io::Result<()> {
1559 for block in blocks {
1560 if cancellation
1561 .is_some_and(|cancelled| cancelled.load(std::sync::atomic::Ordering::Relaxed))
1562 {
1563 return Err(io::Error::new(
1564 io::ErrorKind::Interrupted,
1565 "store merge cancelled",
1566 ));
1567 }
1568 let start = block.offset;
1570 let end = start.checked_add(block.length as u64).ok_or_else(|| {
1571 io::Error::new(io::ErrorKind::InvalidData, "store block range overflow")
1572 })?;
1573 if end > data_slice.len() {
1574 return Err(io::Error::new(
1575 io::ErrorKind::UnexpectedEof,
1576 "store block range is out of bounds",
1577 ));
1578 }
1579 let compressed_data = data_slice.read_bytes_range(start..end).await?;
1580
1581 self.writer.write_all(compressed_data.as_slice())?;
1583
1584 self.index.push(StoreBlockIndex {
1586 first_doc_id: self.next_doc_id,
1587 offset: self.current_offset,
1588 length: block.length,
1589 num_docs: block.num_docs,
1590 });
1591
1592 self.current_offset = self
1593 .current_offset
1594 .checked_add(block.length as u64)
1595 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "store size overflow"))?;
1596 self.next_doc_id = self
1597 .next_doc_id
1598 .checked_add(block.num_docs)
1599 .ok_or_else(|| {
1600 io::Error::new(io::ErrorKind::InvalidData, "store document count overflow")
1601 })?;
1602 }
1603
1604 Ok(())
1605 }
1606
1607 pub async fn append_store_recompressing(
1614 &mut self,
1615 store: &AsyncStoreReader,
1616 cancellation: Option<&std::sync::atomic::AtomicBool>,
1617 ) -> io::Result<()> {
1618 let dict = store.dict();
1619 let data_slice = store.data_slice();
1620 let blocks = store.block_index();
1621
1622 for block in blocks {
1623 if cancellation
1624 .is_some_and(|cancelled| cancelled.load(std::sync::atomic::Ordering::Relaxed))
1625 {
1626 return Err(io::Error::new(
1627 io::ErrorKind::Interrupted,
1628 "store merge cancelled",
1629 ));
1630 }
1631 let start = block.offset;
1632 let end = start.checked_add(block.length as u64).ok_or_else(|| {
1633 io::Error::new(io::ErrorKind::InvalidData, "store block range overflow")
1634 })?;
1635 if end > data_slice.len() {
1636 return Err(io::Error::new(
1637 io::ErrorKind::UnexpectedEof,
1638 "store block range is out of bounds",
1639 ));
1640 }
1641 let compressed = data_slice.read_bytes_range(start..end).await?;
1642
1643 let decompressed = if let Some(d) = dict {
1645 crate::compression::decompress_with_dict_limited(
1646 compressed.as_slice(),
1647 d,
1648 MAX_STORE_BLOCK_BYTES,
1649 )?
1650 } else {
1651 crate::compression::decompress_limited(
1652 compressed.as_slice(),
1653 MAX_STORE_BLOCK_BYTES,
1654 )?
1655 };
1656
1657 let recompressed = crate::compression::compress(
1659 &decompressed,
1660 crate::compression::CompressionLevel::default(),
1661 )?;
1662
1663 self.writer.write_all(&recompressed)?;
1664
1665 self.index.push(StoreBlockIndex {
1666 first_doc_id: self.next_doc_id,
1667 offset: self.current_offset,
1668 length: u32::try_from(recompressed.len()).map_err(|_| {
1669 io::Error::new(
1670 io::ErrorKind::InvalidData,
1671 "compressed store block too large",
1672 )
1673 })?,
1674 num_docs: block.num_docs,
1675 });
1676
1677 self.current_offset = self
1678 .current_offset
1679 .checked_add(recompressed.len() as u64)
1680 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "store size overflow"))?;
1681 self.next_doc_id = self
1682 .next_doc_id
1683 .checked_add(block.num_docs)
1684 .ok_or_else(|| {
1685 io::Error::new(io::ErrorKind::InvalidData, "store document count overflow")
1686 })?;
1687 }
1688
1689 Ok(())
1690 }
1691
1692 pub fn finish(self) -> io::Result<u32> {
1694 let data_end_offset = self.current_offset;
1695
1696 let dict_offset = 0u64;
1698
1699 write_store_index_and_footer(
1701 self.writer,
1702 &self.index,
1703 data_end_offset,
1704 dict_offset,
1705 self.next_doc_id,
1706 false,
1707 )?;
1708
1709 Ok(self.next_doc_id)
1710 }
1711}
1712
1713impl AsyncStoreReader {
1714 pub fn raw_blocks(&self) -> Vec<RawStoreBlock> {
1716 self.index
1717 .iter()
1718 .map(|entry| RawStoreBlock {
1719 first_doc_id: entry.first_doc_id,
1720 num_docs: entry.num_docs,
1721 offset: entry.offset,
1722 length: entry.length,
1723 })
1724 .collect()
1725 }
1726
1727 pub fn data_slice(&self) -> &FileHandle {
1729 &self.data_slice
1730 }
1731
1732 pub fn has_dict(&self) -> bool {
1734 self.dict.is_some()
1735 }
1736
1737 pub fn dict(&self) -> Option<&CompressionDict> {
1739 self.dict.as_ref()
1740 }
1741
1742 pub(crate) fn block_index(&self) -> &[StoreBlockIndex] {
1744 &self.index
1745 }
1746}
1747
1748#[cfg(test)]
1749mod tests {
1750 use super::*;
1751
1752 #[cfg(feature = "native")]
1753 fn raw_store_bytes(
1754 num_threads: usize,
1755 compression_level: CompressionLevel,
1756 documents: &[Vec<u8>],
1757 ) -> Vec<u8> {
1758 let mut output = Vec::new();
1759 let mut store = EagerParallelStoreWriter::with_compression_level(
1760 &mut output,
1761 num_threads,
1762 compression_level,
1763 );
1764 for document in documents {
1765 store.store_raw(document).unwrap();
1766 }
1767 assert_eq!(store.finish().unwrap() as usize, documents.len());
1768 output
1769 }
1770
1771 fn cached_test_block(byte: u8) -> Arc<CachedBlock> {
1772 Arc::new(CachedBlock::build(vec![4, 0, 0, 0, byte, byte, byte, byte], 1).unwrap())
1773 }
1774
1775 #[cfg(feature = "native")]
1776 #[test]
1777 fn parallel_store_is_byte_identical_across_worker_counts() {
1778 let documents: Vec<Vec<u8>> = (0..64usize)
1781 .map(|doc| {
1782 let len = 1_000 + (doc * 7_919 % 40_000);
1783 (0..len)
1784 .map(|offset| ((doc * 17 + offset * 31 + offset / 7) % 251) as u8)
1785 .collect()
1786 })
1787 .collect();
1788
1789 let single_worker = raw_store_bytes(1, CompressionLevel::FAST, &documents);
1790 let four_workers = raw_store_bytes(4, CompressionLevel::FAST, &documents);
1791 assert_eq!(four_workers, single_worker);
1792 }
1793
1794 #[cfg(feature = "native")]
1795 #[test]
1796 fn parallel_stores_share_same_width_compression_executor() {
1797 let first_documents: Vec<Vec<u8>> = (0..12)
1798 .map(|doc| vec![(doc * 17 + 3) as u8; STORE_BLOCK_SIZE + doc * 97])
1799 .collect();
1800 let second_documents: Vec<Vec<u8>> = (0..12)
1801 .map(|doc| vec![(doc * 29 + 7) as u8; STORE_BLOCK_SIZE + doc * 131])
1802 .collect();
1803 let expected_first = raw_store_bytes(1, CompressionLevel::FAST, &first_documents);
1804 let expected_second = raw_store_bytes(1, CompressionLevel::BETTER, &second_documents);
1805
1806 let mut first_output = Vec::new();
1807 let mut second_output = Vec::new();
1808 let mut first = EagerParallelStoreWriter::with_compression_level(
1809 &mut first_output,
1810 3,
1811 CompressionLevel::FAST,
1812 );
1813 let mut second = EagerParallelStoreWriter::with_compression_level(
1814 &mut second_output,
1815 3,
1816 CompressionLevel::BETTER,
1817 );
1818
1819 assert!(Arc::ptr_eq(
1820 &first.workers.executor,
1821 &second.workers.executor
1822 ));
1823 assert_eq!(first.workers.num_threads(), 3);
1824 assert_eq!(second.workers.num_threads(), 3);
1825
1826 for (first_document, second_document) in first_documents.iter().zip(&second_documents) {
1827 first.store_raw(first_document).unwrap();
1828 second.store_raw(second_document).unwrap();
1829 }
1830 assert_eq!(first.finish().unwrap(), first_documents.len() as u32);
1831 assert_eq!(second.finish().unwrap(), second_documents.len() as u32);
1832 assert_eq!(first_output, expected_first);
1833 assert_eq!(second_output, expected_second);
1834 }
1835
1836 #[cfg(feature = "native")]
1837 #[test]
1838 fn parallel_store_bounds_in_flight_blocks_and_streams_before_finish() {
1839 use std::sync::atomic::{AtomicUsize, Ordering};
1840
1841 struct CountingWriter(Arc<AtomicUsize>);
1842 impl Write for CountingWriter {
1843 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
1844 self.0.fetch_add(bytes.len(), Ordering::Relaxed);
1845 Ok(bytes.len())
1846 }
1847
1848 fn flush(&mut self) -> io::Result<()> {
1849 Ok(())
1850 }
1851 }
1852
1853 let bytes_written = Arc::new(AtomicUsize::new(0));
1854 let mut output = CountingWriter(Arc::clone(&bytes_written));
1855 let mut store = EagerParallelStoreWriter::new(&mut output, 3);
1856 assert_eq!(store.workers.num_threads(), 3);
1857 assert_eq!(store.max_in_flight, 6);
1858
1859 let document = vec![7u8; STORE_BLOCK_SIZE];
1860 for _ in 0..24 {
1861 store.store_raw(&document).unwrap();
1862 assert!(store.outstanding_blocks() < store.max_in_flight);
1863 assert_eq!(
1864 store.outstanding_blocks(),
1865 store.pending_results + store.ready_blocks.len()
1866 );
1867 }
1868
1869 assert!(bytes_written.load(Ordering::Relaxed) > 0);
1872 assert_eq!(store.finish().unwrap(), 24);
1873 }
1874
1875 #[cfg(feature = "native")]
1876 #[test]
1877 fn parallel_store_zero_threads_falls_back_to_one_worker() {
1878 let mut output = Vec::new();
1879 let store = EagerParallelStoreWriter::new(&mut output, 0);
1880 assert_eq!(store.workers.num_threads(), 1);
1881 assert_eq!(store.max_in_flight, 2);
1882 assert_eq!(store.finish().unwrap(), 0);
1883 assert_eq!(output.len(), 36);
1885 }
1886
1887 #[test]
1888 fn cached_block_rejects_truncated_and_trailing_documents() {
1889 assert!(CachedBlock::build(vec![8, 0, 0, 0, 1], 1).is_err());
1890 assert!(CachedBlock::build(vec![0, 0, 0, 0, 1], 1).is_err());
1891 }
1892
1893 #[test]
1894 fn document_deserializer_rejects_length_prefixed_slice_overrun() {
1895 let schema = Schema::builder().build();
1896 let truncated_text = [1, 0, 0, 0, 0, 5, 0, 0, 0, b'x'];
1897 assert!(deserialize_document(&truncated_text, &schema).is_err());
1898
1899 let truncated_sparse = [1, 0, 0, 0, 5, 2, 0, 0, 0, 1, 0, 0, 0];
1900 assert!(deserialize_document(&truncated_sparse, &schema).is_err());
1901 }
1902
1903 #[test]
1904 fn shared_store_cache_is_byte_bounded_and_read_concurrent() {
1905 let block_bytes = cached_test_block(1).retained_bytes();
1906 let cache = SharedStoreCache::with_limits(block_bytes * 2, block_bytes);
1907 let key = |first_doc_id| StoreCacheKey {
1908 namespace: StoreCacheNamespace {
1909 directory: 1,
1910 segment: 7,
1911 },
1912 first_doc_id,
1913 };
1914
1915 cache.insert(key(1), cached_test_block(1));
1916 cache.insert(key(2), cached_test_block(2));
1917 assert!(cache.get(key(1)).is_some());
1918 cache.insert(key(3), cached_test_block(3));
1919
1920 assert!(cache.get(key(1)).is_none());
1921 assert!(cache.get(key(2)).is_some());
1922 assert!(cache.get(key(3)).is_some());
1923 assert!(cache.total_bytes() <= block_bytes * 2);
1924 }
1925
1926 #[test]
1927 fn shared_store_cache_bypasses_oversized_entries() {
1928 let block = cached_test_block(1);
1929 let cache = SharedStoreCache::with_limits(1024, block.retained_bytes() - 1);
1930 let key = StoreCacheKey {
1931 namespace: StoreCacheNamespace {
1932 directory: 1,
1933 segment: 9,
1934 },
1935 first_doc_id: 0,
1936 };
1937 cache.insert(key, block);
1938 assert_eq!(cache.total_bytes(), 0);
1939 assert!(cache.get(key).is_none());
1940 }
1941
1942 #[test]
1943 fn shared_store_cache_purges_closed_segment_namespace() {
1944 let block = cached_test_block(1);
1945 let cache = SharedStoreCache::with_limits(1024, 1024);
1946 let key = StoreCacheKey {
1947 namespace: StoreCacheNamespace {
1948 directory: 1,
1949 segment: 11,
1950 },
1951 first_doc_id: 0,
1952 };
1953 cache.register(key.namespace);
1954 cache.insert(key, block);
1955 assert!(cache.total_bytes() > 0);
1956 cache.unregister(key.namespace);
1957 assert_eq!(cache.total_bytes(), 0);
1958 assert!(cache.get(key).is_none());
1959 }
1960
1961 #[test]
1962 fn shared_store_cache_isolates_equal_segment_ids_across_directories() {
1963 let cache = SharedStoreCache::with_limits(1024, 1024);
1964 let key = |directory| StoreCacheKey {
1965 namespace: StoreCacheNamespace {
1966 directory,
1967 segment: 42,
1968 },
1969 first_doc_id: 0,
1970 };
1971 let left = cache.insert(key(1), cached_test_block(1));
1972 let right = cache.insert(key(2), cached_test_block(2));
1973
1974 assert!(!Arc::ptr_eq(&left, &right));
1975 assert_eq!(cache.total_blocks(), 2);
1976 }
1977}