1mod budget;
4mod cancel;
5mod cascade;
6mod clock;
7mod close;
8pub mod durability;
9mod expansion;
10pub(crate) mod graph_cache;
11mod group_count;
12mod hybrid;
13pub mod lock;
14pub(crate) mod materialize;
15pub(crate) mod namespace_batch;
16pub use cascade::CascadeRule;
17#[cfg(any(test, feature = "test-seams"))]
18pub use namespace_batch::namespace_delete_cascade_with_steps;
19pub use namespace_batch::{namespace_declare_cascade, namespace_delete_cascade};
20#[cfg(feature = "graph-cypher")]
21mod enable_graph;
22#[cfg(feature = "graph-cypher")]
23pub(crate) mod native_graph;
24#[cfg(feature = "graph-cypher")]
25mod pinned_search;
26mod pool;
27pub(crate) mod prepared;
28mod prepared_lexical;
29mod reindex;
30pub(crate) mod rescored_scan;
31mod snapshot;
32mod snapshot_copy;
33mod snapshot_view;
34pub use namespace_batch::{
35 LiveNamespaceMutation, NamespaceMutation, NamespaceRootId, namespace_batch,
36 namespace_batch_live, namespace_relocate,
37};
38#[cfg(any(test, feature = "test-seams"))]
39#[doc(hidden)]
40pub use namespace_batch::{
41 namespace_batch_live_on_vfs, namespace_batch_live_with_steps, namespace_batch_with_steps,
42 namespace_reclaim, namespace_reclaim_on_vfs, namespace_relocate_with_steps,
43};
44pub(crate) mod stats;
45
46use crate::diag::{timing_elapsed, timing_start};
47pub use cancel::{
48 CancelToken, Deadline, DeadlineError, QueryCancellation, QueryControl, QueryError,
49};
50#[cfg(any(test, feature = "test-seams"))]
51pub use clock::ManualMonotonicClock;
52pub use clock::{MonotonicClock, SystemMonotonicClock};
53#[cfg(any(test, feature = "test-seams"))]
54pub use hybrid::{
55 HybridScoreTestObservations, begin_hybrid_fresh_round_test_observations,
56 begin_hybrid_score_test_observations, take_hybrid_score_test_observations,
57};
58#[cfg(any(test, feature = "test-seams"))]
59pub use materialize::MaterializationTestCounters;
60pub use materialize::{
61 HybridPreparationError, MaterializationError, MaterializedRow, QueryMaterializer,
62};
63
64pub use group_count::{
65 DocumentGroup, DocumentGroupCounts, DocumentGroupValue, MAX_DOCUMENT_GROUP_LIMIT,
66};
67pub use rescored_scan::ScanRescoreOptions;
68pub use snapshot::{
69 InMemorySegment, InMemorySegmentFactors, PreparedSegment, PublishedSnapshot, SnapshotLease,
70};
71pub use snapshot_copy::SnapshotTargetReason;
72pub use stats::Stats;
73
74use std::collections::HashSet;
75#[cfg(any(test, feature = "test-seams"))]
76use std::io::IoSlice;
77use std::path::{Path, PathBuf};
78use std::sync::atomic::{AtomicU64, Ordering};
79use std::sync::{Arc, Condvar, Mutex, RwLock};
80use std::time::Duration;
81
82use durability::{
83 CommitTier, DurabilityMode, DurabilityPolicy, DurabilityPolicyError, SyncRequirement,
84};
85use lock::{StoreLock, StoreLockError};
86
87use self::close::BackgroundThread;
88
89#[cfg(any(test, feature = "test-seams"))]
91#[derive(Clone)]
92pub struct StoreTestDependencies {
93 pub(crate) vfs: Arc<dyn crate::vfs::Vfs>,
94 pub(crate) clock: Arc<dyn MonotonicClock>,
95 hybrid_leg_fault: Option<HybridLegTestFault>,
96 storage_fault_controller: Option<StorageFaultController>,
97 ingest_retention_fault_controller: Option<crate::ingest::IngestRetentionFaultController>,
98 metadata_test_controller: Option<Arc<crate::planner::MetadataTestController>>,
99 pub(crate) vector_fault_controller: Option<crate::scan::vector_fault::VectorFaultController>,
100 pub(crate) kernel_fault_controller: Option<crate::kernels::vector_fault::KernelFaultController>,
101 vector_seal_scheme: Option<crate::quant::QuantScheme>,
102}
103
104#[cfg(any(test, feature = "test-seams"))]
105impl StoreTestDependencies {
106 #[must_use]
109 pub fn new(vfs: Arc<dyn crate::vfs::Vfs>, clock: Arc<dyn MonotonicClock>) -> Self {
110 Self {
111 vfs,
112 clock,
113 hybrid_leg_fault: None,
114 storage_fault_controller: None,
115 ingest_retention_fault_controller: None,
116 metadata_test_controller: None,
117 vector_fault_controller: None,
118 kernel_fault_controller: None,
119 vector_seal_scheme: None,
120 }
121 }
122
123 #[must_use]
125 pub const fn with_hybrid_leg_fault(mut self, fault: HybridLegTestFault) -> Self {
126 self.hybrid_leg_fault = Some(fault);
127 self
128 }
129
130 #[must_use]
132 pub fn with_storage_fault_controller(mut self, controller: StorageFaultController) -> Self {
133 self.storage_fault_controller = Some(controller);
134 self
135 }
136
137 #[must_use]
139 pub fn with_ingest_retention_fault_controller(
140 mut self,
141 controller: crate::ingest::IngestRetentionFaultController,
142 ) -> Self {
143 self.ingest_retention_fault_controller = Some(controller);
144 self
145 }
146
147 #[must_use]
150 pub fn with_metadata_test_controller(
151 mut self,
152 controller: Arc<crate::planner::MetadataTestController>,
153 ) -> Self {
154 self.metadata_test_controller = Some(controller);
155 self
156 }
157
158 #[must_use]
160 pub fn with_vector_fault_controller(
161 mut self,
162 controller: crate::scan::vector_fault::VectorFaultController,
163 ) -> Self {
164 self.vector_fault_controller = Some(controller);
165 self
166 }
167
168 #[must_use]
170 pub fn with_kernel_fault_controller(
171 mut self,
172 controller: crate::kernels::vector_fault::KernelFaultController,
173 ) -> Self {
174 self.kernel_fault_controller = Some(controller);
175 self
176 }
177
178 #[must_use]
183 pub const fn with_vector_seal_scheme(mut self, scheme: crate::quant::QuantScheme) -> Self {
184 self.vector_seal_scheme = Some(scheme);
185 self
186 }
187}
188
189#[cfg(any(test, feature = "test-seams"))]
191#[derive(Clone, Debug, Eq, PartialEq)]
192#[doc(hidden)]
193pub enum StorageTestFault {
194 TornWalHeader,
195 TornWalBody,
196 TornWalChecksum,
197 PostCommitError,
198 ManifestPreRename,
199 ManifestPostRename,
200 CorruptSegmentRegion,
201 WrongManifestObject,
202 WrongSegmentObject,
203 ListOmission { file_name: String },
204 DeleteOmission { file_name: String },
205}
206
207#[cfg(any(test, feature = "test-seams"))]
208impl StorageTestFault {
209 const fn operation(&self) -> &'static str {
210 match self {
211 Self::TornWalHeader | Self::TornWalBody | Self::TornWalChecksum => "wal-prefix",
212 Self::PostCommitError => "retry",
213 Self::ManifestPreRename | Self::ManifestPostRename => "publication",
214 Self::CorruptSegmentRegion | Self::WrongManifestObject | Self::WrongSegmentObject => {
215 "format-check"
216 }
217 Self::ListOmission { .. } | Self::DeleteOmission { .. } => "orphan-cleanup",
218 }
219 }
220
221 const fn key(&self) -> &'static str {
222 match self {
223 Self::TornWalHeader => "torn-wal-header",
224 Self::TornWalBody => "torn-wal-body",
225 Self::TornWalChecksum => "torn-wal-checksum",
226 Self::PostCommitError => "post-commit-error",
227 Self::ManifestPreRename => "manifest-pre-rename-crash",
228 Self::ManifestPostRename => "manifest-post-rename-crash",
229 Self::CorruptSegmentRegion => "corrupt-segment-region",
230 Self::WrongManifestObject => "wrong-manifest-object",
231 Self::WrongSegmentObject => "wrong-segment-object",
232 Self::ListOmission { .. } | Self::DeleteOmission { .. } => "list-delete-omission",
233 }
234 }
235}
236
237#[cfg(any(test, feature = "test-seams"))]
239#[derive(Clone, Debug, Eq, PartialEq)]
240#[doc(hidden)]
241pub struct StorageFaultPlan {
242 op_index: u32,
243 artifact: String,
244 offset: Option<u64>,
245 segment: Option<crate::segment::SegmentId>,
246 region_kind: Option<u16>,
247 chunk: Option<u32>,
248}
249
250#[cfg(any(test, feature = "test-seams"))]
251impl StorageFaultPlan {
252 #[must_use]
254 pub fn new(op_index: u32, artifact: impl Into<String>) -> Self {
255 Self {
256 op_index,
257 artifact: artifact.into(),
258 offset: None,
259 segment: None,
260 region_kind: None,
261 chunk: None,
262 }
263 }
264
265 #[must_use]
267 pub const fn with_offset(mut self, offset: u64) -> Self {
268 self.offset = Some(offset);
269 self
270 }
271
272 #[must_use]
274 pub const fn with_segment_region(
275 mut self,
276 segment: crate::segment::SegmentId,
277 region_kind: u16,
278 chunk: u32,
279 ) -> Self {
280 self.segment = Some(segment);
281 self.region_kind = Some(region_kind);
282 self.chunk = Some(chunk);
283 self
284 }
285
286 #[must_use]
288 pub const fn with_segment(mut self, segment: crate::segment::SegmentId) -> Self {
289 self.segment = Some(segment);
290 self
291 }
292
293 #[must_use]
295 pub const fn op_index(&self) -> u32 {
296 self.op_index
297 }
298
299 #[must_use]
301 pub fn artifact(&self) -> &str {
302 &self.artifact
303 }
304
305 #[must_use]
307 pub const fn offset(&self) -> Option<u64> {
308 self.offset
309 }
310
311 #[must_use]
313 pub const fn segment(&self) -> Option<crate::segment::SegmentId> {
314 self.segment
315 }
316
317 #[must_use]
319 pub const fn region_kind(&self) -> Option<u16> {
320 self.region_kind
321 }
322
323 #[must_use]
325 pub const fn chunk(&self) -> Option<u32> {
326 self.chunk
327 }
328}
329
330#[cfg(any(test, feature = "test-seams"))]
332#[derive(Clone, Copy, Debug, Eq, PartialEq)]
333#[doc(hidden)]
334pub enum StorageReceiptSite {
335 WalOpenHeaderValidation,
336 WalOpenRecordValidation,
337 WalOpenRecordChecksum,
338 WalCommitAppendAfterInnerSuccess,
339 ManifestCommitBeforeRename,
340 ManifestCommitAfterRename,
341 SegmentReadRegionChecksum,
342 ManifestOpenFamilyValidation,
343 SegmentOpenFamilyValidation,
344 SegmentOpenObjectIdentity,
345 OrphanCleanupList,
346 OrphanCleanupDelete,
347}
348
349#[cfg(any(test, feature = "test-seams"))]
350impl StorageReceiptSite {
351 const fn key(self) -> &'static str {
352 match self {
353 Self::WalOpenHeaderValidation => "WalOpen.HeaderValidation",
354 Self::WalOpenRecordValidation => "WalOpen.RecordValidation",
355 Self::WalOpenRecordChecksum => "WalOpen.RecordChecksum",
356 Self::WalCommitAppendAfterInnerSuccess => "WalCommit.AppendAfterInnerSuccess",
357 Self::ManifestCommitBeforeRename => "ManifestCommit.BeforeRename",
358 Self::ManifestCommitAfterRename => "ManifestCommit.AfterRename",
359 Self::SegmentReadRegionChecksum => "SegmentRead.RegionChecksum",
360 Self::ManifestOpenFamilyValidation => "ManifestOpen.FamilyValidation",
361 Self::SegmentOpenFamilyValidation => "SegmentOpen.FamilyValidation",
362 Self::SegmentOpenObjectIdentity => "SegmentOpen.ObjectIdentity",
363 Self::OrphanCleanupList => "OrphanCleanup.List",
364 Self::OrphanCleanupDelete => "OrphanCleanup.Delete",
365 }
366 }
367}
368
369#[cfg(any(test, feature = "test-seams"))]
371#[derive(Clone, Debug, Eq, PartialEq)]
372#[doc(hidden)]
373pub enum StorageReceiptObserved {
374 WalHeader {
375 artifact: String,
376 reason: crate::wal::header::WalHeaderError,
377 },
378 WalRecord {
379 artifact: String,
380 offset: u64,
381 reason: crate::wal::replay::CorruptionReason,
382 },
383 WalAppend {
384 artifact: String,
385 encoded_len: u64,
386 first_seq: u64,
387 last_seq: u64,
388 inner_append_completed: bool,
389 caller_saw_error: bool,
390 },
391 ManifestRename {
392 temporary: String,
393 committed: String,
394 rename_performed: bool,
395 new_segment_final: bool,
396 directory_sync_returned: bool,
397 },
398 SegmentChecksum {
399 artifact: String,
400 segment: crate::segment::SegmentId,
401 region_kind: u16,
402 chunk: u32,
403 expected_checksum: u64,
404 actual_checksum: u64,
405 },
406 Format {
407 artifact: String,
408 check: crate::format::frame::FormatCheck,
409 expected_family: Option<u16>,
410 actual_family: Option<u16>,
411 expected_id: Option<crate::segment::SegmentId>,
412 actual_id: Option<crate::segment::SegmentId>,
413 },
414 Omission {
415 artifact: String,
416 deletion_observed: bool,
417 },
418}
419
420#[cfg(any(test, feature = "test-seams"))]
422#[derive(Clone, Debug, Eq, PartialEq)]
423#[doc(hidden)]
424pub struct StorageFaultReceipt {
425 operation: &'static str,
426 fault: &'static str,
427 site: StorageReceiptSite,
428 plan: StorageFaultPlan,
429 observed: StorageReceiptObserved,
430 cardinality: u32,
431}
432
433#[cfg(any(test, feature = "test-seams"))]
434impl StorageFaultReceipt {
435 #[must_use]
436 pub const fn campaign(&self) -> &'static str {
437 "storage-durability"
438 }
439
440 #[must_use]
441 pub const fn operation(&self) -> &'static str {
442 self.operation
443 }
444
445 #[must_use]
446 pub const fn fault(&self) -> &'static str {
447 self.fault
448 }
449
450 #[must_use]
451 pub const fn site(&self) -> &'static str {
452 self.site.key()
453 }
454
455 #[must_use]
456 pub const fn typed_site(&self) -> StorageReceiptSite {
457 self.site
458 }
459
460 #[must_use]
461 pub const fn cardinality(&self) -> u32 {
462 self.cardinality
463 }
464
465 #[must_use]
466 pub const fn plan(&self) -> &StorageFaultPlan {
467 &self.plan
468 }
469
470 #[must_use]
471 pub const fn observed(&self) -> &StorageReceiptObserved {
472 &self.observed
473 }
474
475 pub fn from_manifest_abort_acknowledgment(acknowledgment: &str) -> Result<Self, String> {
481 let body = acknowledgment
482 .strip_suffix('\n')
483 .filter(|body| !body.contains('\n'))
484 .ok_or_else(|| {
485 "manifest abort acknowledgment must end in exactly one newline".to_owned()
486 })?;
487 let fields = body
488 .split('|')
489 .map(|field| {
490 field.split_once('=').ok_or_else(|| {
491 format!("manifest abort acknowledgment field lacks '=': {field}")
492 })
493 })
494 .collect::<Result<Vec<_>, _>>()?;
495 let expected_keys = [
496 "campaign",
497 "operation",
498 "fault",
499 "site",
500 "op_index",
501 "cardinality",
502 "artifact",
503 "temporary",
504 "committed",
505 "rename_performed",
506 "new_segment_final",
507 "directory_sync_returned",
508 ];
509 if fields.len() != expected_keys.len()
510 || fields
511 .iter()
512 .map(|(key, _)| *key)
513 .ne(expected_keys.iter().copied())
514 {
515 return Err("manifest abort acknowledgment field inventory differs".to_owned());
516 }
517 let mut values = fields.into_iter().map(|(_, value)| value);
518 let campaign = values
519 .next()
520 .ok_or_else(|| "manifest abort acknowledgment lacks campaign".to_owned())?;
521 let operation = values
522 .next()
523 .ok_or_else(|| "manifest abort acknowledgment lacks operation".to_owned())?;
524 let fault = values
525 .next()
526 .ok_or_else(|| "manifest abort acknowledgment lacks fault".to_owned())?;
527 let site = values
528 .next()
529 .ok_or_else(|| "manifest abort acknowledgment lacks site".to_owned())?;
530 let op_index = values
531 .next()
532 .ok_or_else(|| "manifest abort acknowledgment lacks op index".to_owned())?
533 .parse::<u32>()
534 .map_err(|error| format!("manifest abort op index is invalid: {error}"))?;
535 let cardinality = values
536 .next()
537 .ok_or_else(|| "manifest abort acknowledgment lacks cardinality".to_owned())?
538 .parse::<u32>()
539 .map_err(|error| format!("manifest abort cardinality is invalid: {error}"))?;
540 let artifact = values
541 .next()
542 .ok_or_else(|| "manifest abort acknowledgment lacks artifact".to_owned())?;
543 let temporary = values
544 .next()
545 .ok_or_else(|| "manifest abort acknowledgment lacks temporary path".to_owned())?;
546 let committed = values
547 .next()
548 .ok_or_else(|| "manifest abort acknowledgment lacks committed path".to_owned())?;
549 let parse_bool = |field: &str, value: &str| match value {
550 "true" => Ok(true),
551 "false" => Ok(false),
552 _ => Err(format!("manifest abort {field} is not a bool: {value}")),
553 };
554 let rename_performed = parse_bool(
555 "rename_performed",
556 values
557 .next()
558 .ok_or_else(|| "manifest abort acknowledgment lacks rename result".to_owned())?,
559 )?;
560 let new_segment_final = parse_bool(
561 "new_segment_final",
562 values.next().ok_or_else(|| {
563 "manifest abort acknowledgment lacks final-segment result".to_owned()
564 })?,
565 )?;
566 let directory_sync_returned = parse_bool(
567 "directory_sync_returned",
568 values.next().ok_or_else(|| {
569 "manifest abort acknowledgment lacks directory-sync result".to_owned()
570 })?,
571 )?;
572 if campaign != "storage-durability"
573 || operation != "publication"
574 || cardinality != 1
575 || artifact != crate::manifest::io::MANIFEST_FILE
576 || temporary != crate::manifest::io::MANIFEST_TEMP_FILE
577 || committed != crate::manifest::io::MANIFEST_FILE
578 || !new_segment_final
579 || directory_sync_returned
580 {
581 return Err("manifest abort acknowledgment fixed facts differ".to_owned());
582 }
583 let (fault, site, expected_rename) = match (fault, site) {
584 ("manifest-pre-rename-crash", "ManifestCommit.BeforeRename") => (
585 "manifest-pre-rename-crash",
586 StorageReceiptSite::ManifestCommitBeforeRename,
587 false,
588 ),
589 ("manifest-post-rename-crash", "ManifestCommit.AfterRename") => (
590 "manifest-post-rename-crash",
591 StorageReceiptSite::ManifestCommitAfterRename,
592 true,
593 ),
594 _ => return Err("manifest abort fault and production site differ".to_owned()),
595 };
596 if rename_performed != expected_rename {
597 return Err("manifest abort rename result differs from production site".to_owned());
598 }
599 Ok(Self {
600 operation: "publication",
601 fault,
602 site,
603 plan: StorageFaultPlan::new(op_index, artifact),
604 observed: StorageReceiptObserved::ManifestRename {
605 temporary: temporary.to_owned(),
606 committed: committed.to_owned(),
607 rename_performed,
608 new_segment_final,
609 directory_sync_returned,
610 },
611 cardinality,
612 })
613 }
614}
615
616#[cfg(any(test, feature = "test-seams"))]
617#[derive(Debug)]
618struct StorageFaultState {
619 fault: StorageTestFault,
620 plan: StorageFaultPlan,
621 consumed: bool,
622 receipt: Option<StorageFaultReceipt>,
623 cleanup_report: Option<StorageCleanupReport>,
624 #[cfg(unix)]
625 child_abort_ack: Option<std::os::unix::net::UnixStream>,
626}
627
628#[cfg(any(test, feature = "test-seams"))]
630#[derive(Clone, Debug, Eq, PartialEq)]
631#[doc(hidden)]
632pub struct StorageCleanupReport {
633 reclaimed_bytes: u64,
634 deleted_paths: Vec<String>,
635 retained_eligible_paths: Vec<String>,
636 directory_synced: bool,
637}
638
639#[cfg(any(test, feature = "test-seams"))]
640impl StorageCleanupReport {
641 #[must_use]
643 pub const fn reclaimed_bytes(&self) -> u64 {
644 self.reclaimed_bytes
645 }
646
647 #[must_use]
649 pub fn deleted_paths(&self) -> &[String] {
650 &self.deleted_paths
651 }
652
653 #[must_use]
655 pub fn retained_eligible_paths(&self) -> &[String] {
656 &self.retained_eligible_paths
657 }
658
659 #[must_use]
661 pub const fn directory_synced(&self) -> bool {
662 self.directory_synced
663 }
664}
665
666#[cfg(any(test, feature = "test-seams"))]
668#[derive(Clone, Debug)]
669#[doc(hidden)]
670pub struct StorageFaultController {
671 state: Arc<Mutex<StorageFaultState>>,
672}
673
674#[cfg(any(test, feature = "test-seams"))]
675impl StorageFaultController {
676 #[must_use]
678 pub fn new(fault: StorageTestFault, plan: StorageFaultPlan) -> Self {
679 Self {
680 state: Arc::new(Mutex::new(StorageFaultState {
681 fault,
682 plan,
683 consumed: false,
684 receipt: None,
685 cleanup_report: None,
686 #[cfg(unix)]
687 child_abort_ack: None,
688 })),
689 }
690 }
691
692 #[cfg(unix)]
694 #[must_use]
695 pub fn with_child_abort_ack(self, acknowledgment: std::os::unix::net::UnixStream) -> Self {
696 if let Ok(mut state) = self.state.lock() {
697 state.child_abort_ack = Some(acknowledgment);
698 }
699 self
700 }
701
702 pub fn take_receipt(&self) -> Option<StorageFaultReceipt> {
704 self.state
705 .lock()
706 .ok()
707 .and_then(|mut state| state.receipt.take())
708 }
709
710 pub fn take_cleanup_report(&self) -> Option<StorageCleanupReport> {
712 self.state
713 .lock()
714 .ok()
715 .and_then(|mut state| state.cleanup_report.take())
716 }
717
718 fn record_cleanup_report(&self, report: crate::manifest::io::OrphanCleanupReport) {
719 let normalize = |paths: Vec<PathBuf>| {
720 paths
721 .into_iter()
722 .filter_map(|path| {
723 path.file_name()
724 .and_then(|name| name.to_str())
725 .map(str::to_owned)
726 })
727 .collect::<Vec<_>>()
728 };
729 if let Ok(mut state) = self.state.lock() {
730 state.cleanup_report = Some(StorageCleanupReport {
731 reclaimed_bytes: report.reclaimed_bytes,
732 deleted_paths: normalize(report.deleted_paths),
733 retained_eligible_paths: normalize(report.retained_eligible_paths),
734 directory_synced: report.directory_synced,
735 });
736 }
737 }
738
739 fn armed_fault(&self) -> Option<StorageTestFault> {
740 self.state
741 .lock()
742 .ok()
743 .and_then(|state| (!state.consumed).then(|| state.fault.clone()))
744 }
745
746 fn planned_segment(&self) -> Option<crate::segment::SegmentId> {
747 self.state
748 .lock()
749 .ok()
750 .and_then(|state| state.plan.segment())
751 }
752
753 fn emit(&self, site: StorageReceiptSite, observed: StorageReceiptObserved) -> bool {
754 let Ok(mut state) = self.state.lock() else {
755 return false;
756 };
757 if state.consumed {
758 return false;
759 }
760 state.consumed = true;
761 state.receipt = Some(StorageFaultReceipt {
762 operation: state.fault.operation(),
763 fault: state.fault.key(),
764 site,
765 plan: state.plan.clone(),
766 observed,
767 cardinality: 1,
768 });
769 true
770 }
771
772 fn abort_after_manifest_receipt(&self) -> ! {
773 #[cfg(unix)]
774 if let Ok(mut state) = self.state.lock() {
775 let wire_receipt = state
776 .receipt
777 .as_ref()
778 .and_then(storage_manifest_abort_wire_receipt);
779 if let (Some(wire_receipt), Some(acknowledgment)) =
780 (wire_receipt, state.child_abort_ack.as_mut())
781 {
782 use std::io::Write as _;
783 let _ = acknowledgment.write_all(wire_receipt.as_bytes());
784 let _ = acknowledgment.flush();
785 }
786 }
787 std::process::abort()
788 }
789}
790
791#[cfg(any(test, feature = "test-seams"))]
792thread_local! {
793 static STORAGE_OPEN_CONTROLLER: std::cell::RefCell<
794 Option<std::sync::Weak<Mutex<StorageFaultState>>>
795 > = const { std::cell::RefCell::new(None) };
796}
797
798#[cfg(any(test, feature = "test-seams"))]
799fn with_storage_open_controller<R>(
800 controller: Option<&StorageFaultController>,
801 operation: impl FnOnce() -> R,
802) -> R {
803 STORAGE_OPEN_CONTROLLER.with(|slot| {
804 let previous = slot.replace(controller.map(|controller| Arc::downgrade(&controller.state)));
805 let result = operation();
806 let _ = slot.replace(previous);
807 result
808 })
809}
810
811#[cfg(any(test, feature = "test-seams"))]
812fn storage_open_controller() -> Option<StorageFaultController> {
813 STORAGE_OPEN_CONTROLLER.with(|slot| {
814 slot.borrow()
815 .as_ref()
816 .and_then(std::sync::Weak::upgrade)
817 .map(|state| StorageFaultController { state })
818 })
819}
820
821#[cfg(any(test, feature = "test-seams"))]
822pub(crate) fn record_storage_wal_recovery_fault(error: &crate::wal::WalRecoveryError) {
823 let Some(controller) = storage_open_controller() else {
824 return;
825 };
826 match (controller.armed_fault(), error) {
827 (
828 Some(StorageTestFault::TornWalHeader),
829 crate::wal::WalRecoveryError::InvalidHeader(reason),
830 ) => {
831 let _ = controller.emit(
832 StorageReceiptSite::WalOpenHeaderValidation,
833 StorageReceiptObserved::WalHeader {
834 artifact: "wal.ze".to_owned(),
835 reason: *reason,
836 },
837 );
838 }
839 (
840 Some(StorageTestFault::TornWalBody),
841 crate::wal::WalRecoveryError::CorruptAt {
842 offset,
843 reason:
844 reason @ crate::wal::replay::CorruptionReason::Record {
845 error:
846 crate::wal::record::RecordError::HeaderTruncated { .. }
847 | crate::wal::record::RecordError::BodyTruncated { .. }
848 | crate::wal::record::RecordError::LengthOverflow { .. },
849 ..
850 },
851 },
852 ) => record_storage_wal_record_fault(
853 &controller,
854 StorageReceiptSite::WalOpenRecordValidation,
855 *offset,
856 *reason,
857 ),
858 (
859 Some(StorageTestFault::TornWalChecksum),
860 crate::wal::WalRecoveryError::CorruptAt {
861 offset,
862 reason:
863 reason @ crate::wal::replay::CorruptionReason::Record {
864 error: crate::wal::record::RecordError::ChecksumMismatch { .. },
865 ..
866 },
867 },
868 ) => record_storage_wal_record_fault(
869 &controller,
870 StorageReceiptSite::WalOpenRecordChecksum,
871 *offset,
872 *reason,
873 ),
874 _ => {}
875 }
876}
877
878#[cfg(any(test, feature = "test-seams"))]
879fn record_storage_wal_record_fault(
880 controller: &StorageFaultController,
881 site: StorageReceiptSite,
882 offset: usize,
883 reason: crate::wal::replay::CorruptionReason,
884) {
885 let Ok(offset) = u64::try_from(offset) else {
886 return;
887 };
888 let _ = controller.emit(
889 site,
890 StorageReceiptObserved::WalRecord {
891 artifact: "wal.ze".to_owned(),
892 offset,
893 reason,
894 },
895 );
896}
897
898#[cfg(any(test, feature = "test-seams"))]
899pub(crate) fn record_storage_manifest_format_fault(
900 error: &crate::format::frame::FormatError,
901 actual_family: Option<u16>,
902) {
903 let Some(controller) = storage_open_controller() else {
904 return;
905 };
906 if matches!(
907 controller.armed_fault(),
908 Some(StorageTestFault::WrongManifestObject)
909 ) && error.check() == crate::format::frame::FormatCheck::Family
910 {
911 let _ = controller.emit(
912 StorageReceiptSite::ManifestOpenFamilyValidation,
913 StorageReceiptObserved::Format {
914 artifact: storage_artifact_name(error.artifact()).to_owned(),
915 check: error.check(),
916 expected_family: Some(crate::format::FormatFamily::Manifest.id()),
917 actual_family,
918 expected_id: None,
919 actual_id: None,
920 },
921 );
922 }
923}
924
925#[cfg(any(test, feature = "test-seams"))]
926pub(crate) fn record_storage_segment_format_fault(
927 error: &crate::format::frame::FormatError,
928 actual_family: Option<u16>,
929 expected_id: crate::segment::SegmentId,
930) {
931 let Some(controller) = storage_open_controller() else {
932 return;
933 };
934 if matches!(
935 controller.armed_fault(),
936 Some(StorageTestFault::WrongSegmentObject)
937 ) && error.check() == crate::format::frame::FormatCheck::Family
938 {
939 let _ = controller.emit(
940 StorageReceiptSite::SegmentOpenFamilyValidation,
941 StorageReceiptObserved::Format {
942 artifact: storage_artifact_name(error.artifact()).to_owned(),
943 check: error.check(),
944 expected_family: Some(crate::format::FormatFamily::Segment.id()),
945 actual_family,
946 expected_id: Some(expected_id),
947 actual_id: None,
948 },
949 );
950 }
951}
952
953#[cfg(any(test, feature = "test-seams"))]
954pub(crate) fn record_storage_segment_identity_fault(
955 artifact: &str,
956 expected: crate::segment::SegmentId,
957 actual: crate::segment::SegmentId,
958) {
959 let Some(controller) = storage_open_controller() else {
960 return;
961 };
962 if matches!(
963 controller.armed_fault(),
964 Some(StorageTestFault::WrongSegmentObject)
965 ) {
966 let _ = controller.emit(
967 StorageReceiptSite::SegmentOpenObjectIdentity,
968 StorageReceiptObserved::Format {
969 artifact: storage_artifact_name(artifact).to_owned(),
970 check: crate::format::frame::FormatCheck::ObjectIdentity,
971 expected_family: None,
972 actual_family: None,
973 expected_id: Some(expected),
974 actual_id: Some(actual),
975 },
976 );
977 }
978}
979
980#[cfg(unix)]
988#[cfg(any(test, feature = "test-seams"))]
989fn storage_manifest_abort_wire_receipt(receipt: &StorageFaultReceipt) -> Option<String> {
990 let StorageReceiptObserved::ManifestRename {
991 temporary,
992 committed,
993 rename_performed,
994 new_segment_final,
995 directory_sync_returned,
996 } = &receipt.observed
997 else {
998 return None;
999 };
1000 Some(format!(
1001 "campaign={}|operation={}|fault={}|site={}|op_index={}|cardinality={}|artifact={}|temporary={temporary}|committed={committed}|rename_performed={rename_performed}|new_segment_final={new_segment_final}|directory_sync_returned={directory_sync_returned}\n",
1002 receipt.campaign(),
1003 receipt.operation,
1004 receipt.fault,
1005 receipt.site.key(),
1006 receipt.plan.op_index,
1007 receipt.cardinality,
1008 receipt.plan.artifact,
1009 ))
1010}
1011
1012#[cfg(any(test, feature = "test-seams"))]
1013fn storage_artifact_name(artifact: &str) -> &str {
1014 Path::new(artifact)
1015 .file_name()
1016 .and_then(|name| name.to_str())
1017 .unwrap_or(artifact)
1018}
1019
1020#[cfg(any(test, feature = "test-seams"))]
1021type StorageSegmentControllerRegistry = Mutex<
1022 Vec<(
1023 PathBuf,
1024 crate::segment::SegmentId,
1025 std::sync::Weak<Mutex<StorageFaultState>>,
1026 )>,
1027>;
1028
1029#[cfg(any(test, feature = "test-seams"))]
1030static STORAGE_SEGMENT_CONTROLLERS: std::sync::OnceLock<StorageSegmentControllerRegistry> =
1031 std::sync::OnceLock::new();
1032
1033#[cfg(any(test, feature = "test-seams"))]
1034fn register_storage_segment_controller(
1035 store_directory: &Path,
1036 segment: crate::segment::SegmentId,
1037 controller: &StorageFaultController,
1038) {
1039 let registry = STORAGE_SEGMENT_CONTROLLERS.get_or_init(|| Mutex::new(Vec::new()));
1040 if let Ok(mut entries) = registry.lock() {
1041 entries.retain(|(_, _, state)| state.strong_count() != 0);
1042 entries.push((
1043 store_directory.to_path_buf(),
1044 segment,
1045 Arc::downgrade(&controller.state),
1046 ));
1047 }
1048}
1049
1050#[cfg(any(test, feature = "test-seams"))]
1051pub(crate) fn record_storage_segment_checksum_fault(
1052 store_directory: &Path,
1053 segment: crate::segment::SegmentId,
1054 kind: crate::segment::layout::RegionKind,
1055 chunk: u32,
1056 expected: u64,
1057 actual: u64,
1058) {
1059 let Some(registry) = STORAGE_SEGMENT_CONTROLLERS.get() else {
1060 return;
1061 };
1062 let states = match registry.lock() {
1063 Ok(mut entries) => {
1064 entries.retain(|(_, _, state)| state.strong_count() != 0);
1065 entries
1066 .iter()
1067 .filter(|(directory, registered, _)| {
1068 directory == store_directory && *registered == segment
1069 })
1070 .filter_map(|(_, _, state)| state.upgrade())
1071 .collect::<Vec<_>>()
1072 }
1073 Err(_) => return,
1074 };
1075 for state in states {
1076 let controller = StorageFaultController { state };
1077 if matches!(
1078 controller.armed_fault(),
1079 Some(StorageTestFault::CorruptSegmentRegion)
1080 ) {
1081 let _ = controller.emit(
1082 StorageReceiptSite::SegmentReadRegionChecksum,
1083 StorageReceiptObserved::SegmentChecksum {
1084 artifact: segment.file_name(),
1085 segment,
1086 region_kind: kind.id(),
1087 chunk,
1088 expected_checksum: expected,
1089 actual_checksum: actual,
1090 },
1091 );
1092 }
1093 }
1094}
1095
1096#[cfg(any(test, feature = "test-seams"))]
1097struct StorageFaultVfs {
1098 inner: Arc<dyn crate::vfs::Vfs>,
1099 controller: StorageFaultController,
1100}
1101
1102#[cfg(any(test, feature = "test-seams"))]
1103struct StorageFaultFile {
1104 inner: Box<dyn crate::vfs::VfsFile>,
1105 path: PathBuf,
1106 controller: StorageFaultController,
1107}
1108
1109#[cfg(any(test, feature = "test-seams"))]
1110impl StorageFaultFile {
1111 fn after_append(&self, encoded_len: u64, first_seq: u64, last_seq: u64) -> std::io::Result<()> {
1112 if matches!(
1113 self.controller.armed_fault(),
1114 Some(StorageTestFault::PostCommitError)
1115 ) && self.path.file_name().is_some_and(|name| name == "wal.ze")
1116 && self.controller.emit(
1117 StorageReceiptSite::WalCommitAppendAfterInnerSuccess,
1118 StorageReceiptObserved::WalAppend {
1119 artifact: "wal.ze".to_owned(),
1120 encoded_len,
1121 first_seq,
1122 last_seq,
1123 inner_append_completed: true,
1124 caller_saw_error: true,
1125 },
1126 )
1127 {
1128 return Err(std::io::Error::other(
1129 "scheduled post-commit error after inner append",
1130 ));
1131 }
1132 Ok(())
1133 }
1134}
1135
1136#[cfg(any(test, feature = "test-seams"))]
1137impl crate::vfs::VfsFile for StorageFaultFile {
1138 fn append(&mut self, bytes: &[u8]) -> std::io::Result<()> {
1139 let record_facts = storage_wal_append_facts(bytes)?;
1140 self.inner.append(bytes)?;
1141 record_facts.map_or(Ok(()), |(encoded_len, first_seq, last_seq)| {
1142 self.after_append(encoded_len, first_seq, last_seq)
1143 })
1144 }
1145
1146 fn append_vectored(&mut self, buffers: &mut [IoSlice<'_>]) -> std::io::Result<()> {
1147 let mut aggregate = None::<(u64, u64, u64)>;
1148 for bytes in buffers.iter() {
1149 if let Some((encoded_len, first_seq, last_seq)) = storage_wal_append_facts(bytes)? {
1150 aggregate = Some(match aggregate {
1151 None => (encoded_len, first_seq, last_seq),
1152 Some((total, first, _)) => (
1153 total.checked_add(encoded_len).ok_or_else(|| {
1154 std::io::Error::other("WAL record append length overflow")
1155 })?,
1156 first,
1157 last_seq,
1158 ),
1159 });
1160 }
1161 }
1162 self.inner.append_vectored(buffers)?;
1163 aggregate.map_or(Ok(()), |(encoded_len, first_seq, last_seq)| {
1164 self.after_append(encoded_len, first_seq, last_seq)
1165 })
1166 }
1167
1168 fn sync(&self, kind: crate::vfs::SyncKind) -> std::io::Result<()> {
1169 self.inner.sync(kind)
1170 }
1171}
1172
1173#[cfg(any(test, feature = "test-seams"))]
1174impl crate::vfs::Vfs for StorageFaultVfs {
1175 fn segment_data_read_counter(&self) -> Option<Arc<AtomicU64>> {
1176 self.inner.segment_data_read_counter()
1177 }
1178
1179 fn ensure_directory(&self, path: &Path, create: bool) -> std::io::Result<bool> {
1180 self.inner.ensure_directory(path, create)
1181 }
1182
1183 fn open(&self, path: &Path) -> std::io::Result<u64> {
1184 self.inner.open(path)
1185 }
1186
1187 fn open_for_map(&self, path: &Path) -> std::io::Result<std::fs::File> {
1188 self.inner.open_for_map(path)
1189 }
1190
1191 fn read(&self, path: &Path) -> std::io::Result<Vec<u8>> {
1192 self.inner.read(path)
1193 }
1194
1195 fn read_range(&self, path: &Path, offset: u64, length: usize) -> std::io::Result<Vec<u8>> {
1196 self.inner.read_range(path, offset, length)
1197 }
1198
1199 fn write(&self, path: &Path, bytes: &[u8]) -> std::io::Result<()> {
1200 self.inner.write(path, bytes)
1201 }
1202
1203 fn open_append(&self, path: &Path) -> std::io::Result<Box<dyn crate::vfs::VfsFile>> {
1204 Ok(Box::new(StorageFaultFile {
1205 inner: self.inner.open_append(path)?,
1206 path: path.to_path_buf(),
1207 controller: self.controller.clone(),
1208 }))
1209 }
1210
1211 fn rename(&self, from: &Path, to: &Path) -> std::io::Result<()> {
1212 let is_manifest = from
1213 .file_name()
1214 .is_some_and(|name| name == crate::manifest::io::MANIFEST_TEMP_FILE)
1215 && to
1216 .file_name()
1217 .is_some_and(|name| name == crate::manifest::io::MANIFEST_FILE);
1218 if is_manifest
1219 && matches!(
1220 self.controller.armed_fault(),
1221 Some(StorageTestFault::ManifestPreRename)
1222 )
1223 && self.controller.emit(
1224 StorageReceiptSite::ManifestCommitBeforeRename,
1225 StorageReceiptObserved::ManifestRename {
1226 temporary: crate::manifest::io::MANIFEST_TEMP_FILE.to_owned(),
1227 committed: crate::manifest::io::MANIFEST_FILE.to_owned(),
1228 rename_performed: false,
1229 new_segment_final: storage_planned_segment_is_final(
1230 &*self.inner,
1231 from,
1232 self.controller.planned_segment(),
1233 ),
1234 directory_sync_returned: false,
1235 },
1236 )
1237 {
1238 self.controller.abort_after_manifest_receipt();
1239 }
1240 self.inner.rename(from, to)?;
1241 if is_manifest
1242 && matches!(
1243 self.controller.armed_fault(),
1244 Some(StorageTestFault::ManifestPostRename)
1245 )
1246 && self.controller.emit(
1247 StorageReceiptSite::ManifestCommitAfterRename,
1248 StorageReceiptObserved::ManifestRename {
1249 temporary: crate::manifest::io::MANIFEST_TEMP_FILE.to_owned(),
1250 committed: crate::manifest::io::MANIFEST_FILE.to_owned(),
1251 rename_performed: true,
1252 new_segment_final: storage_planned_segment_is_final(
1253 &*self.inner,
1254 to,
1255 self.controller.planned_segment(),
1256 ),
1257 directory_sync_returned: false,
1258 },
1259 )
1260 {
1261 self.controller.abort_after_manifest_receipt();
1262 }
1263 Ok(())
1264 }
1265
1266 fn sync(&self, path: &Path, kind: crate::vfs::SyncKind) -> std::io::Result<()> {
1267 self.inner.sync(path, kind)
1268 }
1269
1270 fn list(&self, directory: &Path) -> std::io::Result<Vec<PathBuf>> {
1271 let mut paths = self.inner.list(directory)?;
1272 if let Some(StorageTestFault::ListOmission { file_name }) = self.controller.armed_fault()
1273 && let Some(position) = paths.iter().position(|path| {
1274 path.file_name()
1275 .is_some_and(|name| name == file_name.as_str())
1276 })
1277 {
1278 let _ = paths.remove(position);
1279 let _ = self.controller.emit(
1280 StorageReceiptSite::OrphanCleanupList,
1281 StorageReceiptObserved::Omission {
1282 artifact: file_name,
1283 deletion_observed: false,
1284 },
1285 );
1286 }
1287 Ok(paths)
1288 }
1289
1290 fn for_each_direct_child(
1291 &self,
1292 directory: &Path,
1293 visitor: &mut dyn FnMut(&Path) -> std::io::Result<()>,
1294 ) -> std::io::Result<()> {
1295 self.inner.for_each_direct_child(directory, visitor)
1298 }
1299
1300 fn delete(&self, path: &Path) -> std::io::Result<()> {
1301 if let Some(StorageTestFault::DeleteOmission { file_name }) = self.controller.armed_fault()
1302 && path
1303 .file_name()
1304 .is_some_and(|name| name == file_name.as_str())
1305 && self.controller.emit(
1306 StorageReceiptSite::OrphanCleanupDelete,
1307 StorageReceiptObserved::Omission {
1308 artifact: file_name,
1309 deletion_observed: false,
1310 },
1311 )
1312 {
1313 return Ok(());
1314 }
1315 self.inner.delete(path)
1316 }
1317}
1318
1319#[cfg(any(test, feature = "test-seams"))]
1320fn storage_wal_append_facts(bytes: &[u8]) -> std::io::Result<Option<(u64, u64, u64)>> {
1321 const RECORD_HEADER_LEN: usize = 14;
1322 const RECORD_CHECKSUM_LEN: usize = 8;
1323 let has_wal_header = bytes
1324 .get(..8)
1325 .is_some_and(|magic| magic == crate::format::frame::FILE_MAGIC)
1326 && bytes
1327 .get(8..10)
1328 .is_some_and(|family| family == crate::format::FormatFamily::Wal.id().to_le_bytes());
1329 let mut offset = if has_wal_header {
1330 crate::wal::header::WAL_HEADER_LEN
1331 } else {
1332 0
1333 };
1334 if offset == bytes.len() {
1335 return Ok(None);
1336 }
1337 let mut encoded_len = 0_u64;
1338 let mut first_seq = None;
1339 let mut last_seq = None;
1340 while offset < bytes.len() {
1341 let header_end = offset
1342 .checked_add(RECORD_HEADER_LEN)
1343 .ok_or_else(|| std::io::Error::other("WAL record header offset overflow"))?;
1344 let header = bytes
1345 .get(offset..header_end)
1346 .ok_or_else(|| std::io::Error::other("WAL append has a torn record header"))?;
1347 let payload_len = usize::try_from(u32::from_le_bytes(
1348 header
1349 .get(..4)
1350 .ok_or_else(|| std::io::Error::other("WAL payload length is absent"))?
1351 .try_into()
1352 .map_err(|_| std::io::Error::other("WAL payload length width changed"))?,
1353 ))
1354 .map_err(|_| std::io::Error::other("WAL payload length does not fit usize"))?;
1355 let seq = u64::from_le_bytes(
1356 header
1357 .get(4..12)
1358 .ok_or_else(|| std::io::Error::other("WAL sequence is absent"))?
1359 .try_into()
1360 .map_err(|_| std::io::Error::other("WAL sequence width changed"))?,
1361 );
1362 let record_len = RECORD_HEADER_LEN
1363 .checked_add(payload_len)
1364 .and_then(|length| length.checked_add(RECORD_CHECKSUM_LEN))
1365 .ok_or_else(|| std::io::Error::other("WAL record length overflow"))?;
1366 offset = offset
1367 .checked_add(record_len)
1368 .ok_or_else(|| std::io::Error::other("WAL record end overflow"))?;
1369 if offset > bytes.len() {
1370 return Err(std::io::Error::other("WAL append has a torn record body"));
1371 }
1372 encoded_len = encoded_len
1373 .checked_add(
1374 u64::try_from(record_len)
1375 .map_err(|_| std::io::Error::other("WAL record length does not fit u64"))?,
1376 )
1377 .ok_or_else(|| std::io::Error::other("WAL record append length overflow"))?;
1378 first_seq.get_or_insert(seq);
1379 last_seq = Some(seq);
1380 }
1381 match (first_seq, last_seq) {
1382 (Some(first), Some(last)) => Ok(Some((encoded_len, first, last))),
1383 _ => Ok(None),
1384 }
1385}
1386
1387#[cfg(any(test, feature = "test-seams"))]
1388fn storage_planned_segment_is_final(
1389 vfs: &dyn crate::vfs::Vfs,
1390 artifact: &Path,
1391 planned: Option<crate::segment::SegmentId>,
1392) -> bool {
1393 let Some(file_name) = planned.map(crate::segment::SegmentId::file_name) else {
1394 return false;
1395 };
1396 artifact.parent().is_some_and(|directory| {
1397 vfs.list(directory).is_ok_and(|paths| {
1398 paths.iter().any(|path| {
1399 path.file_name()
1400 .and_then(|name| name.to_str())
1401 .is_some_and(|name| name == file_name)
1402 })
1403 })
1404 })
1405}
1406
1407#[cfg(any(test, feature = "test-seams"))]
1409#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1410#[doc(hidden)]
1411pub enum HybridLegTestFault {
1412 Panic(crate::fusion::FusionLeg),
1414}
1415
1416#[allow(clippy::panic)]
1417fn maybe_trigger_hybrid_leg_panic(armed: bool, detail: &'static str) {
1418 #[cfg(any(test, feature = "test-seams"))]
1419 if armed {
1420 std::panic::panic_any(detail);
1421 }
1422 #[cfg(not(any(test, feature = "test-seams")))]
1423 let _ = (armed, detail);
1424}
1425
1426#[cfg(any(test, feature = "test-seams"))]
1428#[derive(Clone, Debug, Eq, PartialEq)]
1429#[doc(hidden)]
1430pub struct HybridExecutionReceipt {
1431 pub vector_thread: std::thread::ThreadId,
1433 pub lexical_thread_name: Option<String>,
1435 pub vector_completed: bool,
1437 pub lexical_completed: bool,
1439}
1440
1441pub const DEFAULT_READER_DRAIN_TIMEOUT: Duration = Duration::from_millis(250);
1443
1444#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1446pub enum StoreState {
1447 Open,
1449 Closing,
1451 Closed,
1453}
1454
1455#[derive(Clone, Debug, Eq, PartialEq)]
1457pub struct OpenOptions {
1458 inspection: bool,
1459 access_mode: AccessMode,
1460 durability_mode: DurabilityMode,
1461 commit_tier: CommitTier,
1462 reader_drain_timeout: Duration,
1463 max_resident_bytes: u64,
1464 max_temp_bytes: u64,
1465 epoch: Option<crate::epoch::StoreEpoch>,
1466 tokenizer: Option<crate::fts::tokenizer::TokenizerConfig>,
1467 schema: Option<crate::meta::Schema>,
1468 #[cfg(feature = "graph-cypher")]
1469 graph_document: Option<crate::epoch::EmbeddingTower>,
1470}
1471
1472#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1474pub enum AccessMode {
1475 ReadWrite,
1477 ReadOnly,
1479}
1480
1481impl OpenOptions {
1482 #[must_use]
1484 pub const fn new() -> Self {
1485 Self {
1486 inspection: false,
1487 access_mode: AccessMode::ReadWrite,
1488 durability_mode: DurabilityMode::Derived,
1489 commit_tier: CommitTier::Ordered,
1490 reader_drain_timeout: DEFAULT_READER_DRAIN_TIMEOUT,
1491 max_resident_bytes: u64::MAX,
1492 max_temp_bytes: u64::MAX,
1493 epoch: None,
1494 tokenizer: None,
1495 schema: None,
1496 #[cfg(feature = "graph-cypher")]
1497 graph_document: None,
1498 }
1499 }
1500
1501 #[must_use]
1503 pub const fn read_only() -> Self {
1504 Self {
1505 inspection: false,
1506 access_mode: AccessMode::ReadOnly,
1507 durability_mode: DurabilityMode::Derived,
1508 commit_tier: CommitTier::Ordered,
1509 reader_drain_timeout: DEFAULT_READER_DRAIN_TIMEOUT,
1510 max_resident_bytes: u64::MAX,
1511 max_temp_bytes: u64::MAX,
1512 epoch: None,
1513 tokenizer: None,
1514 schema: None,
1515 #[cfg(feature = "graph-cypher")]
1516 graph_document: None,
1517 }
1518 }
1519
1520 #[must_use]
1522 pub const fn with_durability(mut self, mode: DurabilityMode, tier: CommitTier) -> Self {
1523 self.durability_mode = mode;
1524 self.commit_tier = tier;
1525 self
1526 }
1527
1528 #[cfg(feature = "graph-cypher")]
1531 #[must_use]
1532 pub(crate) const fn with_access_mode(mut self, access_mode: AccessMode) -> Self {
1533 self.access_mode = access_mode;
1534 self
1535 }
1536
1537 #[must_use]
1539 pub const fn with_reader_drain_timeout(mut self, timeout: Duration) -> Self {
1540 self.reader_drain_timeout = timeout;
1541 self
1542 }
1543
1544 #[must_use]
1546 pub const fn with_max_resident_bytes(mut self, bytes: u64) -> Self {
1547 self.max_resident_bytes = bytes;
1548 self
1549 }
1550
1551 #[must_use]
1559 pub fn with_schema(mut self, schema: crate::meta::Schema) -> Self {
1560 self.schema = Some(schema);
1561 self
1562 }
1563
1564 #[must_use]
1566 pub const fn with_max_temp_bytes(mut self, bytes: u64) -> Self {
1567 self.max_temp_bytes = bytes;
1568 self
1569 }
1570
1571 #[must_use]
1577 pub fn with_epoch(mut self, epoch: crate::epoch::StoreEpoch) -> Self {
1578 self.epoch = Some(epoch);
1579 self
1580 }
1581
1582 #[must_use]
1587 pub fn with_tokenizer(mut self, tokenizer: crate::fts::tokenizer::TokenizerConfig) -> Self {
1588 self.tokenizer = Some(tokenizer);
1589 self
1590 }
1591}
1592
1593impl Default for OpenOptions {
1594 fn default() -> Self {
1595 Self::new()
1596 }
1597}
1598
1599#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1601pub struct GraphSearchOptions {
1602 profile: crate::graph::search::GraphSearchProfile,
1603 ef: Option<usize>,
1604 seed: u64,
1605 #[cfg(any(test, feature = "test-seams"))]
1606 cancel_after_hops: Option<usize>,
1607}
1608
1609impl GraphSearchOptions {
1610 #[must_use]
1612 pub const fn new(profile: crate::graph::search::GraphSearchProfile) -> Self {
1613 Self {
1614 profile,
1615 ef: None,
1616 seed: 0,
1617 #[cfg(any(test, feature = "test-seams"))]
1618 cancel_after_hops: None,
1619 }
1620 }
1621
1622 #[must_use]
1624 pub const fn with_ef(mut self, ef: usize) -> Self {
1625 self.ef = Some(ef);
1626 self
1627 }
1628
1629 #[must_use]
1631 pub const fn with_seed(mut self, seed: u64) -> Self {
1632 self.seed = seed;
1633 self
1634 }
1635
1636 #[cfg(any(test, feature = "test-seams"))]
1638 #[must_use]
1639 pub const fn with_cancel_after_hops(mut self, hops: usize) -> Self {
1640 self.cancel_after_hops = Some(hops);
1641 self
1642 }
1643
1644 pub(crate) const fn profile(self) -> crate::graph::search::GraphSearchProfile {
1645 self.profile
1646 }
1647
1648 pub(crate) const fn ef(self) -> Option<usize> {
1649 self.ef
1650 }
1651
1652 pub(crate) const fn seed(self) -> u64 {
1653 self.seed
1654 }
1655
1656 #[cfg(any(test, feature = "test-seams"))]
1657 pub(crate) const fn cancel_after_hops(self) -> Option<usize> {
1658 self.cancel_after_hops
1659 }
1660}
1661
1662impl Default for GraphSearchOptions {
1663 fn default() -> Self {
1664 Self::new(crate::graph::search::GraphSearchProfile::SiftClass)
1665 }
1666}
1667
1668pub(crate) fn auto_graph_search_options(
1669 snapshot: &PublishedSnapshot,
1670) -> Result<GraphSearchOptions, QueryError> {
1671 snapshot
1672 .graph_profile()
1673 .map(|profile| GraphSearchOptions::new(profile.search_profile()))
1674 .map_err(crate::graph::search::GraphSearchError::Profile)
1675 .map_err(QueryError::Graph)
1676}
1677
1678#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1680pub enum SearchTier {
1681 #[default]
1683 Auto,
1684 Exact,
1688 Scan,
1690 Graph(GraphSearchOptions),
1692}
1693
1694#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1696pub struct SearchOptions {
1697 scan: crate::scan::ScanOptions,
1698 tier: Option<SearchTier>,
1699 scan_rescore: Option<ScanRescoreOptions>,
1700}
1701
1702#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1703enum GraphBoundMode {
1704 Shared,
1705 #[cfg(test)]
1706 Independent,
1707}
1708
1709impl SearchOptions {
1710 #[must_use]
1712 pub const fn new(scan: crate::scan::ScanOptions) -> Self {
1713 Self {
1714 scan,
1715 tier: None,
1716 scan_rescore: None,
1717 }
1718 }
1719
1720 #[must_use]
1722 pub const fn with_tier(mut self, tier: SearchTier) -> Self {
1723 self.tier = Some(tier);
1724 self.scan_rescore = None;
1725 self
1726 }
1727
1728 #[must_use]
1732 pub const fn with_scan_rescore(mut self, options: ScanRescoreOptions) -> Self {
1733 self.tier = Some(SearchTier::Scan);
1734 self.scan_rescore = Some(options);
1735 self
1736 }
1737
1738 #[must_use]
1740 pub const fn scan_rescore(self) -> Option<ScanRescoreOptions> {
1741 self.scan_rescore
1742 }
1743
1744 #[must_use]
1746 pub const fn scan(self) -> crate::scan::ScanOptions {
1747 self.scan
1748 }
1749
1750 #[must_use]
1753 pub const fn tier(self) -> SearchTier {
1754 match self.tier {
1755 Some(tier) => tier,
1756 None => SearchTier::Auto,
1757 }
1758 }
1759
1760 pub(crate) const fn explicit_tier(self) -> Option<SearchTier> {
1761 self.tier
1762 }
1763}
1764
1765impl From<crate::scan::ScanOptions> for SearchOptions {
1766 fn from(scan: crate::scan::ScanOptions) -> Self {
1767 Self::new(scan)
1768 }
1769}
1770
1771#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1773pub struct DocumentFields(u8);
1774
1775impl DocumentFields {
1776 pub const NONE: Self = Self(0);
1778 pub const VECTOR: Self = Self(1 << 0);
1780 pub const TEXT: Self = Self(1 << 1);
1782 pub const METADATA: Self = Self(1 << 2);
1784 pub const ATTRIBUTES: Self = Self(1 << 3);
1786 pub const ALL: Self =
1788 Self(Self::VECTOR.0 | Self::TEXT.0 | Self::METADATA.0 | Self::ATTRIBUTES.0);
1789
1790 #[must_use]
1792 pub const fn contains(self, fields: Self) -> bool {
1793 self.0 & fields.0 == fields.0
1794 }
1795}
1796
1797impl std::ops::BitOr for DocumentFields {
1798 type Output = Self;
1799
1800 fn bitor(self, rhs: Self) -> Self::Output {
1801 Self(self.0 | rhs.0)
1802 }
1803}
1804
1805#[derive(Clone, Debug, PartialEq)]
1807pub struct StoredDocument {
1808 pub doc_id: crate::ingest::DocId,
1810 pub revision: crate::ingest::Revision,
1812 pub timestamp: i64,
1814 pub vector: Option<Vec<f32>>,
1816 pub text: Option<String>,
1818 pub metadata: Option<Vec<u8>>,
1820 pub attributes: Option<Vec<(crate::meta::ColumnId, crate::meta::PredicateValue)>>,
1822}
1823
1824#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1826pub enum ScanOrder {
1827 #[default]
1830 Storage,
1831 TimestampAscending,
1833 TimestampDescending,
1835 Attribute {
1841 column: crate::meta::ColumnId,
1843 direction: ScanDirection,
1845 },
1846}
1847
1848#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1850pub enum ScanDirection {
1851 Ascending,
1853 Descending,
1855}
1856
1857#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1859pub enum DocumentScanSource {
1860 Sealed(crate::segment::SegmentId),
1862 Active,
1864}
1865
1866#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1868pub struct DocumentScanCursor {
1869 pub generation: u64,
1871 pub order: ScanOrder,
1874 pub source: DocumentScanSource,
1876 pub next_row: u32,
1878}
1879
1880#[derive(Clone, Debug)]
1882pub struct DocumentScanRequest<'a> {
1883 limit: usize,
1884 fields: DocumentFields,
1885 order: ScanOrder,
1886 cursor: Option<DocumentScanCursor>,
1887 timestamp_range: Option<(i64, i64)>,
1888 predicate: Option<&'a crate::meta::Predicate>,
1889 control: QueryControl,
1890}
1891
1892impl<'a> DocumentScanRequest<'a> {
1893 #[must_use]
1895 pub fn new(limit: usize, fields: DocumentFields, control: QueryControl) -> Self {
1896 Self {
1897 limit,
1898 fields,
1899 order: ScanOrder::Storage,
1900 cursor: None,
1901 timestamp_range: None,
1902 predicate: None,
1903 control,
1904 }
1905 }
1906
1907 #[must_use]
1909 pub const fn with_order(mut self, order: ScanOrder) -> Self {
1910 self.order = order;
1911 self
1912 }
1913
1914 #[must_use]
1916 pub const fn with_cursor(mut self, cursor: DocumentScanCursor) -> Self {
1917 self.cursor = Some(cursor);
1918 self
1919 }
1920
1921 #[must_use]
1923 pub const fn with_timestamp_range(mut self, start: i64, end: i64) -> Self {
1924 self.timestamp_range = Some((start, end));
1925 self
1926 }
1927
1928 #[must_use]
1930 pub const fn with_predicate(mut self, predicate: &'a crate::meta::Predicate) -> Self {
1931 self.predicate = Some(predicate);
1932 self
1933 }
1934}
1935
1936#[derive(Clone, Debug, PartialEq)]
1938pub struct DocumentScanPage {
1939 pub documents: Vec<StoredDocument>,
1941 pub generation: u64,
1943 pub continuation: Option<DocumentScanCursor>,
1945}
1946
1947#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1949pub struct DocumentCount {
1950 pub count: u64,
1952 pub generation: u64,
1954}
1955
1956pub const MAX_DOCUMENT_SCAN_LIMIT: usize = 1 << 20;
1958
1959#[derive(Debug)]
1961pub enum StoreError {
1962 #[cfg(feature = "graph-cypher")]
1964 DocumentMutation(Box<crate::ingest::IngestError>),
1965 CascadeCycle {
1967 cycle: Vec<String>,
1969 },
1970 Io {
1972 path: PathBuf,
1974 source: std::io::Error,
1976 },
1977 NotDirectory {
1979 path: PathBuf,
1981 },
1982 StoreBusy {
1984 path: PathBuf,
1986 },
1987 Lock(StoreLockError),
1989 Durability(DurabilityPolicyError),
1991 Kernel(crate::kernels::KernelInitError),
1993 Manifest(crate::manifest::ManifestError),
1995 Tokenizer(crate::fts::tokenizer::TokenizerError),
1997 EpochMismatch(crate::epoch::EpochMismatch),
1999 EpochUndeclared,
2001 EpochUnstamped,
2003 SchemaMismatch {
2005 persisted: crate::meta::Schema,
2007 declared: crate::meta::Schema,
2009 },
2010 ScanStale {
2012 cursor_generation: u64,
2014 current_generation: u64,
2016 },
2017 InvalidScan {
2019 detail: String,
2021 },
2022 GroupLimitExceeded {
2024 limit: usize,
2026 },
2027 Segment(crate::segment::SegmentError),
2029 DuplicateLiveDocument {
2031 doc_id: crate::ingest::DocId,
2033 },
2034 GraphUnavailable {
2036 segment_id: crate::segment::SegmentId,
2038 },
2039 Wal(crate::wal::WalReadError),
2041 WalRecovery(crate::wal::WalRecoveryError),
2043 WalRecord {
2045 seq: crate::wal::LogSeq,
2047 source: crate::wal::VisibleRecordError,
2049 },
2050 WalMutation {
2052 seq: crate::wal::LogSeq,
2054 op: u16,
2056 source: crate::ingest::wal_payload::PayloadError,
2058 },
2059 WalRevisionOrder {
2061 seq: crate::wal::LogSeq,
2063 doc_id: crate::ingest::DocId,
2065 current: crate::ingest::Revision,
2067 attempted: crate::ingest::Revision,
2069 },
2070 UnsupportedWalMutation {
2072 seq: crate::wal::LogSeq,
2074 op: u16,
2076 },
2077 WalVector {
2079 seq: crate::wal::LogSeq,
2081 source: crate::quant::QuantError,
2083 },
2084 WalWrite(crate::wal::WalWriteError),
2086 WalRetire(crate::wal::WalRetireError),
2088 Statistics {
2090 component: &'static str,
2092 source: std::io::Error,
2094 },
2095 BudgetExceeded {
2097 needed: u64,
2099 budget: u64,
2101 component: &'static str,
2103 },
2104 AllocationFailed {
2106 needed: u64,
2108 component: &'static str,
2110 },
2111 DimensionMismatch {
2113 expected: usize,
2115 actual: usize,
2117 },
2118 ActiveRowOverflow,
2120 EmptyActiveSegment,
2122 SealCancelled,
2124 ReadOnly,
2126 SealedTombstoneRecoveryRequired,
2128 ForeignPreparedSegment,
2130 GenerationOverflow,
2132 ConcurrentActiveMutation {
2134 expected_generation: u64,
2136 actual_generation: u64,
2138 },
2139 PartitionBytesOverflow,
2141 PurgeRecovery {
2143 detail: String,
2145 },
2146 Closing,
2148 Closed,
2150 ReadCancelled,
2152 BackgroundStart {
2154 source: std::io::Error,
2156 },
2157 BackgroundHandshake,
2159 BackgroundThreadPanicked,
2161 QueryPoolStart {
2163 source: std::io::Error,
2165 },
2166 QueryPoolHandshake,
2168 QueryPoolThreadPanicked,
2170 QueryPoolCapacity {
2172 source: String,
2174 },
2175 Synchronization {
2177 component: &'static str,
2179 },
2180 NativeGraphDirectory {
2183 path: PathBuf,
2185 },
2186 SnapshotTarget {
2188 path: PathBuf,
2190 reason: SnapshotTargetReason,
2192 },
2193 SnapshotPin {
2196 detail: &'static str,
2198 },
2199 SnapshotPurgePending,
2202 SnapshotViewsOpen,
2204}
2205
2206impl std::fmt::Display for StoreError {
2207 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2208 match self {
2209 #[cfg(feature = "graph-cypher")]
2210 Self::DocumentMutation(error) => error.fmt(formatter),
2211 Self::CascadeCycle { cycle } => {
2212 write!(formatter, "cascade cycle: {}", cycle.join(" -> "))
2213 }
2214 Self::Io { path, source } => {
2215 write!(formatter, "store I/O {}: {source}", path.display())
2216 }
2217 Self::NotDirectory { path } => {
2218 write!(
2219 formatter,
2220 "store path is not a directory: {}",
2221 path.display()
2222 )
2223 }
2224 Self::SnapshotViewsOpen => {
2225 formatter.write_str("close open snapshots before physical removal")
2226 }
2227 Self::StoreBusy { path } => {
2228 write!(formatter, "store already has a writer: {}", path.display())
2229 }
2230 Self::Lock(error) => error.fmt(formatter),
2231 Self::Durability(error) => error.fmt(formatter),
2232 Self::Kernel(error) => error.fmt(formatter),
2233 Self::Manifest(error) => error.fmt(formatter),
2234 Self::Tokenizer(error) => write!(formatter, "store tokenizer: {error}"),
2235 Self::EpochMismatch(error) => error.fmt(formatter),
2236 Self::EpochUndeclared => {
2237 formatter.write_str("store epoch is persisted but the caller declared none")
2238 }
2239 Self::EpochUnstamped => formatter
2240 .write_str("store manifest predates epoch identity and cannot adopt a declaration"),
2241 Self::SchemaMismatch {
2242 persisted,
2243 declared,
2244 } => match persisted.additive_evolution(declared) {
2245 Err(conflict) => write!(
2246 formatter,
2247 "declared store schema does not match the persisted schema: {conflict}"
2248 ),
2249 Ok(Some(_)) => {
2252 formatter.write_str(
2253 "declared store schema adds attributes that a read-only open \
2254 cannot persist:",
2255 )?;
2256 for added in declared
2257 .columns()
2258 .iter()
2259 .filter(|column| persisted.column(column.id()).is_none())
2260 {
2261 write!(formatter, " '{}' (id {})", added.name(), added.id().get())?;
2262 }
2263 Ok(())
2264 }
2265 Ok(None) => write!(
2266 formatter,
2267 "declared store schema {declared:?} does not match persisted schema \
2268 {persisted:?}"
2269 ),
2270 },
2271 Self::ScanStale {
2272 cursor_generation,
2273 current_generation,
2274 } => write!(
2275 formatter,
2276 "scan cursor generation {cursor_generation} differs from current generation {current_generation}"
2277 ),
2278 Self::InvalidScan { detail } => write!(formatter, "invalid document scan: {detail}"),
2279 Self::GroupLimitExceeded { limit } => write!(
2280 formatter,
2281 "grouped count found more than {limit} distinct values; raise the group limit"
2282 ),
2283 Self::Segment(error) => error.fmt(formatter),
2284 Self::DuplicateLiveDocument { doc_id } => {
2285 write!(
2286 formatter,
2287 "document {} has multiple live rows",
2288 doc_id.get()
2289 )
2290 }
2291 Self::GraphUnavailable { segment_id } => {
2292 write!(formatter, "sealed segment {segment_id} has no graph region")
2293 }
2294 Self::Wal(error) => error.fmt(formatter),
2295 Self::WalRecovery(error) => error.fmt(formatter),
2296 Self::WalRecord { seq, source } => {
2297 write!(
2298 formatter,
2299 "WAL sequence {} retained record: {source}",
2300 seq.get()
2301 )
2302 }
2303 Self::WalMutation { seq, op, source } => write!(
2304 formatter,
2305 "WAL sequence {} operation {op} payload: {source}",
2306 seq.get()
2307 ),
2308 Self::WalRevisionOrder {
2309 seq,
2310 doc_id,
2311 current,
2312 attempted,
2313 } => write!(
2314 formatter,
2315 "WAL sequence {} document {} revision {} does not follow {}",
2316 seq.get(),
2317 doc_id.get(),
2318 attempted.get(),
2319 current.get()
2320 ),
2321 Self::UnsupportedWalMutation { seq, op } => write!(
2322 formatter,
2323 "WAL sequence {} operation {op} has no active-segment recovery path",
2324 seq.get()
2325 ),
2326 Self::WalVector { seq, source } => {
2327 write!(formatter, "WAL sequence {} vector: {source}", seq.get())
2328 }
2329 Self::WalWrite(error) => error.fmt(formatter),
2330 Self::WalRetire(error) => error.fmt(formatter),
2331 Self::Statistics { component, source } => {
2332 write!(formatter, "store statistics {component}: {source}")
2333 }
2334 Self::BudgetExceeded {
2335 needed,
2336 budget,
2337 component,
2338 } => write!(
2339 formatter,
2340 "store {component} allocation needs {needed} bytes, budget is {budget} bytes"
2341 ),
2342 Self::AllocationFailed { needed, component } => write!(
2343 formatter,
2344 "store {component} allocator rejected {needed} bytes"
2345 ),
2346 Self::DimensionMismatch { expected, actual } => write!(
2347 formatter,
2348 "active vector dimension {actual} does not match {expected}"
2349 ),
2350 Self::ActiveRowOverflow => {
2351 formatter.write_str("active segment row or byte geometry overflow")
2352 }
2353 Self::EmptyActiveSegment => formatter.write_str("active segment is empty"),
2354 Self::SealCancelled => formatter.write_str("seal was cancelled before commit"),
2355 Self::ReadOnly => formatter.write_str("store handle is read-only"),
2356 Self::SealedTombstoneRecoveryRequired => formatter.write_str(
2357 "store requires writable sealed-tombstone recovery before reads are safe",
2358 ),
2359 Self::ForeignPreparedSegment => {
2360 formatter.write_str("prepared segment belongs to another store")
2361 }
2362 Self::GenerationOverflow => formatter.write_str("store snapshot generation overflow"),
2363 Self::ConcurrentActiveMutation {
2364 expected_generation,
2365 actual_generation,
2366 } => write!(
2367 formatter,
2368 "active generation changed from {expected_generation} to {actual_generation} while preparing a commit"
2369 ),
2370 Self::PartitionBytesOverflow => {
2371 formatter.write_str("partition reclaimed-byte count overflow")
2372 }
2373 Self::PurgeRecovery { detail } => {
2374 write!(formatter, "physical purge recovery failed: {detail}")
2375 }
2376 Self::Closing => formatter.write_str("store is closing"),
2377 Self::Closed => formatter.write_str("store is closed"),
2378 Self::ReadCancelled => formatter.write_str("store close cancelled the admitted read"),
2379 Self::BackgroundStart { source } => {
2380 write!(
2381 formatter,
2382 "store lifecycle thread could not start: {source}"
2383 )
2384 }
2385 Self::BackgroundHandshake => {
2386 formatter.write_str("store lifecycle thread startup handshake failed")
2387 }
2388 Self::BackgroundThreadPanicked => {
2389 formatter.write_str("store lifecycle thread panicked")
2390 }
2391 Self::QueryPoolStart { source } => {
2392 write!(formatter, "store query worker could not start: {source}")
2393 }
2394 Self::QueryPoolHandshake => {
2395 formatter.write_str("store query worker startup handshake failed")
2396 }
2397 Self::QueryPoolThreadPanicked => formatter.write_str("store query worker panicked"),
2398 Self::QueryPoolCapacity { source } => {
2399 write!(formatter, "store query worker capacity failed: {source}")
2400 }
2401 Self::Synchronization { component } => {
2402 write!(
2403 formatter,
2404 "store lifecycle synchronization poisoned: {component}"
2405 )
2406 }
2407 Self::NativeGraphDirectory { path } => write!(
2408 formatter,
2409 "native graph directory cannot be opened as a legacy store: {}",
2410 path.display()
2411 ),
2412 Self::SnapshotTarget { path, reason } => {
2413 write!(formatter, "snapshot target {} {reason}", path.display())
2414 }
2415 Self::SnapshotPin { detail } => {
2416 write!(formatter, "snapshot pin is inconsistent: {detail}")
2417 }
2418 Self::SnapshotPurgePending => formatter
2419 .write_str("a physical purge is pending; await it before taking a snapshot"),
2420 }
2421 }
2422}
2423
2424#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
2429pub enum StoreErrorKind {
2430 Io,
2432 InvalidArgument,
2434 StoreBusy,
2436 Unsupported,
2438 Corrupt,
2440 BudgetExceeded,
2442 OutOfMemory,
2444 DimensionMismatch,
2446 EpochMismatch,
2448 EpochUndeclared,
2450 EpochUnstamped,
2452 Internal,
2454 EmptyBatch,
2456 Cancelled,
2458 ReadOnly,
2460 Closing,
2462 Closed,
2464 Panic,
2466 Synchronization,
2468}
2469
2470impl StoreError {
2471 #[must_use]
2473 pub fn kind(&self) -> StoreErrorKind {
2474 match self {
2475 #[cfg(feature = "graph-cypher")]
2476 Self::DocumentMutation(error) => match error.as_ref() {
2477 crate::ingest::IngestError::Store(error) => error.kind(),
2478 _ => StoreErrorKind::InvalidArgument,
2479 },
2480 Self::Io { .. }
2481 | Self::Manifest(crate::manifest::ManifestError::Io { .. })
2483 | Self::Lock(_)
2484 | Self::Statistics { .. }
2485 | Self::BackgroundStart { .. }
2486 | Self::QueryPoolStart { .. }
2487 | Self::WalWrite(_)
2488 | Self::WalRetire(_) => StoreErrorKind::Io,
2489 Self::CascadeCycle { .. }
2490 | Self::NotDirectory { .. }
2491 | Self::Tokenizer(_)
2492 | Self::SchemaMismatch { .. }
2493 | Self::ScanStale { .. }
2494 | Self::InvalidScan { .. }
2495 | Self::NativeGraphDirectory { .. }
2496 | Self::SnapshotTarget { .. } => StoreErrorKind::InvalidArgument,
2497 Self::StoreBusy { .. } | Self::SnapshotViewsOpen => StoreErrorKind::StoreBusy,
2498 Self::Manifest(crate::manifest::ManifestError::GraphUnsupportedBuild)
2499 | Self::Durability(_)
2500 | Self::Kernel(_)
2501 | Self::GraphUnavailable { .. }
2502 | Self::UnsupportedWalMutation { .. }
2503 | Self::SnapshotPurgePending => StoreErrorKind::Unsupported,
2504 Self::Manifest(_)
2505 | Self::Segment(_)
2506 | Self::DuplicateLiveDocument { .. }
2507 | Self::Wal(_)
2508 | Self::WalRecovery(_)
2509 | Self::WalRecord { .. }
2510 | Self::WalMutation { .. }
2511 | Self::WalRevisionOrder { .. }
2512 | Self::WalVector { .. }
2513 | Self::PurgeRecovery { .. } => StoreErrorKind::Corrupt,
2514 Self::BudgetExceeded { .. } | Self::GroupLimitExceeded { .. } => {
2515 StoreErrorKind::BudgetExceeded
2516 }
2517 Self::AllocationFailed { .. } => StoreErrorKind::OutOfMemory,
2518 Self::DimensionMismatch { .. } => StoreErrorKind::DimensionMismatch,
2519 Self::EpochMismatch(_) => StoreErrorKind::EpochMismatch,
2520 Self::EpochUndeclared => StoreErrorKind::EpochUndeclared,
2521 Self::EpochUnstamped => StoreErrorKind::EpochUnstamped,
2522 Self::ActiveRowOverflow
2523 | Self::GenerationOverflow
2524 | Self::ConcurrentActiveMutation { .. }
2525 | Self::PartitionBytesOverflow
2526 | Self::ForeignPreparedSegment
2527 | Self::BackgroundHandshake
2528 | Self::QueryPoolHandshake
2529 | Self::QueryPoolCapacity { .. }
2530 | Self::SnapshotPin { .. } => StoreErrorKind::Internal,
2531 Self::EmptyActiveSegment => StoreErrorKind::EmptyBatch,
2532 Self::SealCancelled | Self::ReadCancelled => StoreErrorKind::Cancelled,
2533 Self::ReadOnly | Self::SealedTombstoneRecoveryRequired => StoreErrorKind::ReadOnly,
2534 Self::Closing => StoreErrorKind::Closing,
2535 Self::Closed => StoreErrorKind::Closed,
2536 Self::BackgroundThreadPanicked | Self::QueryPoolThreadPanicked => StoreErrorKind::Panic,
2537 Self::Synchronization { .. } => StoreErrorKind::Synchronization,
2538 }
2539 }
2540}
2541
2542impl std::error::Error for StoreError {
2543 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
2544 match self {
2545 #[cfg(feature = "graph-cypher")]
2546 Self::DocumentMutation(error) => Some(error.as_ref()),
2547 Self::Io { source, .. } => Some(source),
2548 Self::Lock(error) => Some(error),
2549 Self::Durability(error) => Some(error),
2550 Self::Kernel(error) => Some(error),
2551 Self::Manifest(error) => Some(error),
2552 Self::Tokenizer(error) => Some(error),
2553 Self::EpochMismatch(error) => Some(error),
2554 Self::Segment(error) => Some(error),
2555 Self::Wal(error) => Some(error),
2556 Self::WalRecovery(error) => Some(error),
2557 Self::WalRecord { source, .. } => Some(source),
2558 Self::WalMutation { source, .. } => Some(source),
2559 Self::WalVector { source, .. } => Some(source),
2560 Self::WalWrite(error) => Some(error),
2561 Self::WalRetire(error) => Some(error),
2562 Self::Statistics { source, .. } => Some(source),
2563 Self::BackgroundStart { source } => Some(source),
2564 Self::QueryPoolStart { source } => Some(source),
2565 Self::CascadeCycle { .. }
2566 | Self::NotDirectory { .. }
2567 | Self::StoreBusy { .. }
2568 | Self::EpochUndeclared
2569 | Self::EpochUnstamped
2570 | Self::SchemaMismatch { .. }
2571 | Self::ScanStale { .. }
2572 | Self::InvalidScan { .. }
2573 | Self::GraphUnavailable { .. }
2574 | Self::DuplicateLiveDocument { .. }
2575 | Self::WalRevisionOrder { .. }
2576 | Self::UnsupportedWalMutation { .. }
2577 | Self::BudgetExceeded { .. }
2578 | Self::GroupLimitExceeded { .. }
2579 | Self::AllocationFailed { .. }
2580 | Self::DimensionMismatch { .. }
2581 | Self::ActiveRowOverflow
2582 | Self::EmptyActiveSegment
2583 | Self::SealCancelled
2584 | Self::ReadOnly
2585 | Self::SealedTombstoneRecoveryRequired
2586 | Self::ForeignPreparedSegment
2587 | Self::GenerationOverflow
2588 | Self::ConcurrentActiveMutation { .. }
2589 | Self::PartitionBytesOverflow
2590 | Self::PurgeRecovery { .. }
2591 | Self::Closing
2592 | Self::Closed
2593 | Self::ReadCancelled
2594 | Self::BackgroundHandshake
2595 | Self::BackgroundThreadPanicked
2596 | Self::QueryPoolHandshake
2597 | Self::QueryPoolThreadPanicked
2598 | Self::QueryPoolCapacity { .. }
2599 | Self::Synchronization { .. }
2600 | Self::NativeGraphDirectory { .. }
2601 | Self::SnapshotTarget { .. }
2602 | Self::SnapshotPin { .. }
2603 | Self::SnapshotPurgePending
2604 | Self::SnapshotViewsOpen => None,
2605 }
2606 }
2607}
2608
2609#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
2611pub struct OpenMigrations {
2612 pub schema_added: bool,
2614 pub wal_tail_cut: bool,
2616 pub generation: u64,
2618}
2619
2620pub struct Store {
2622 open_migrations: OpenMigrations,
2623 pub(crate) private_preparation: Option<namespace_batch::PrivatePreparation>,
2624 pub(crate) directory: PathBuf,
2625 pub(crate) vfs: Arc<dyn crate::vfs::Vfs>,
2626 pub(crate) clock: Arc<dyn MonotonicClock>,
2627 pub(crate) state: Mutex<StoreState>,
2628 pub(crate) state_changed: Condvar,
2629 pub(crate) background: Mutex<Option<BackgroundThread>>,
2630 pub(crate) query_pool: Mutex<Option<Arc<pool::QueryPool>>>,
2631 pub(crate) lexical_worker: Mutex<Option<Arc<pool::LexicalWorker>>>,
2632 #[cfg(feature = "graph-cypher")]
2633 pub(crate) native_graph: Arc<native_graph::NativeGraphPublication>,
2634 #[cfg(feature = "graph-cypher")]
2635 graph_enable_pending: std::sync::atomic::AtomicBool,
2636 pub(crate) snapshot: RwLock<Option<Arc<PublishedSnapshot>>>,
2637 pub(crate) active: Mutex<Option<crate::ingest::ActiveState>>,
2640 pub(crate) wal_writer: Mutex<Option<crate::ingest::StoreWal>>,
2641 pub(crate) writer_lock: Mutex<Option<Arc<StoreLock>>>,
2642 #[cfg(feature = "graph-cypher")]
2643 reader_store_lock: Mutex<Option<StoreLock>>,
2644 snapshot_pins: Arc<AtomicU64>,
2645 snapshot_pin: Mutex<Option<snapshot_view::SnapshotPin>>,
2646 logical_writer_lock: Mutex<Option<StoreLock>>,
2647 reclamation_pin: Mutex<Option<Arc<StoreLock>>>,
2648 pub(crate) maintenance: Mutex<()>,
2649 pub(crate) health_state: Mutex<crate::diag::HealthState>,
2650 pub(crate) durability_policy: DurabilityPolicy,
2651 pub(crate) reader_drain_timeout: Duration,
2652 pub(crate) accounting: Arc<stats::Accounting>,
2653 pub(crate) active_queries: AtomicU64,
2654 #[cfg(any(test, feature = "test-seams"))]
2655 text_materialization_work: materialize::TestMaterializationWork,
2656 pub(crate) epoch: Option<crate::epoch::StoreEpoch>,
2657 pub(crate) epoch_alias: crate::epoch::EpochAliasCell,
2658 pub(crate) tokenizer: crate::fts::tokenizer::Analyzer,
2659 pub(crate) schema: crate::meta::Schema,
2660 lexical_index_cache: LexicalIndexCache,
2664 #[cfg(any(test, feature = "test-seams"))]
2665 pub(crate) ingest_retention_fault_controller:
2666 Option<crate::ingest::IngestRetentionFaultController>,
2667 #[cfg(any(test, feature = "test-seams"))]
2668 hybrid_leg_fault: Mutex<Option<HybridLegTestFault>>,
2669 #[cfg(any(test, feature = "test-seams"))]
2670 hybrid_execution_receipt: Mutex<Option<HybridExecutionReceipt>>,
2671 #[cfg(any(test, feature = "test-seams"))]
2672 pub(crate) metadata_test_controller: Option<Arc<crate::planner::MetadataTestController>>,
2673 #[cfg(any(test, feature = "test-seams"))]
2674 pub(crate) vector_fault_controller: Option<crate::scan::vector_fault::VectorFaultController>,
2675 #[cfg(any(test, feature = "test-seams"))]
2676 pub(crate) kernel_fault_controller: Option<crate::kernels::vector_fault::KernelFaultController>,
2677 #[cfg(any(test, feature = "test-seams"))]
2678 pub(crate) vector_seal_scheme: Option<crate::quant::QuantScheme>,
2679 #[cfg(test)]
2680 pub(crate) teardown_probe: Arc<close::TeardownProbe>,
2681}
2682
2683fn acquire_writer_lock(
2684 path: &Path,
2685 access_mode: AccessMode,
2686) -> Result<Option<StoreLock>, StoreError> {
2687 Ok(match access_mode {
2688 AccessMode::ReadWrite => Some(StoreLock::acquire(path).map_err(|error| match error {
2689 StoreLockError::Io { path, source }
2690 if source.kind() == std::io::ErrorKind::WouldBlock =>
2691 {
2692 StoreError::StoreBusy { path }
2693 }
2694 StoreLockError::Io { path, source } => {
2695 StoreError::Lock(StoreLockError::Io { path, source })
2696 }
2697 })?),
2698 AccessMode::ReadOnly => None,
2699 })
2700}
2701
2702fn refuse_native_graph_directory(vfs: &dyn crate::vfs::Vfs, path: &Path) -> Result<(), StoreError> {
2703 match vfs.ensure_directory(path, false) {
2704 Ok(true) => {}
2705 Ok(false) => return Ok(()),
2706 Err(source) if source.kind() == std::io::ErrorKind::NotFound => return Ok(()),
2707 Err(source) => {
2708 return Err(StoreError::Io {
2709 path: path.to_path_buf(),
2710 source,
2711 });
2712 }
2713 }
2714 let document_store = is_legacy_store_directory(vfs, path)?;
2718 let mut native = false;
2719 vfs.for_each_direct_child(path, &mut |child| {
2720 let Some(name) = child.file_name().and_then(|name| name.to_str()) else {
2721 return Ok(());
2722 };
2723 if name == "graph-root.ze"
2724 || (name.starts_with("graph-wal-") && name.ends_with(".ze"))
2725 || (!document_store && name.starts_with("graph-") && name.ends_with(".zgraph"))
2726 || (name.starts_with("graph-root-") && name.ends_with(".tmp"))
2727 {
2728 native = true;
2729 }
2730 Ok(())
2731 })
2732 .map_err(|source| StoreError::Io {
2733 path: path.to_path_buf(),
2734 source,
2735 })?;
2736 if native {
2737 return Err(StoreError::NativeGraphDirectory {
2738 path: path.to_path_buf(),
2739 });
2740 }
2741 Ok(())
2742}
2743
2744pub(crate) fn is_legacy_store_directory(
2748 vfs: &dyn crate::vfs::Vfs,
2749 path: &Path,
2750) -> Result<bool, StoreError> {
2751 for name in [
2752 crate::manifest::io::MANIFEST_FILE,
2753 crate::manifest::io::MANIFEST_TEMP_FILE,
2754 "wal.ze",
2755 ] {
2756 let candidate = path.join(name);
2757 match vfs.open(&candidate) {
2758 Ok(_) => return Ok(true),
2759 Err(source)
2760 if matches!(
2761 source.kind(),
2762 std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
2763 ) => {}
2764 Err(source) => {
2765 return Err(StoreError::Io {
2766 path: candidate,
2767 source,
2768 });
2769 }
2770 }
2771 }
2772 Ok(false)
2773}
2774
2775fn resolve_open_schema(
2778 manifest_exists: bool,
2779 snapshot: &PublishedSnapshot,
2780 declared: Option<&crate::meta::Schema>,
2781 access_mode: AccessMode,
2782) -> Result<(crate::meta::Schema, bool), StoreError> {
2783 if !manifest_exists {
2784 return Ok((
2785 declared
2786 .cloned()
2787 .unwrap_or_else(crate::meta::Schema::timestamp_only),
2788 false,
2789 ));
2790 }
2791 let persisted = snapshot.schema();
2792 let Some(declared) = declared else {
2793 return Ok((persisted.clone(), false));
2794 };
2795 let mismatch = || StoreError::SchemaMismatch {
2796 persisted: persisted.clone(),
2797 declared: declared.clone(),
2798 };
2799 match persisted.additive_evolution(declared) {
2800 Ok(None) => Ok((persisted.clone(), false)),
2801 Ok(Some(evolved)) if access_mode == AccessMode::ReadWrite => Ok((evolved, true)),
2802 Ok(Some(_)) | Err(_) => Err(mismatch()),
2803 }
2804}
2805
2806fn validate_persisted_epoch_before_open(
2807 vfs: &dyn crate::vfs::Vfs,
2808 path: &Path,
2809 options: &OpenOptions,
2810) -> Result<(), StoreError> {
2811 let manifest_path = path.join(crate::manifest::io::MANIFEST_FILE);
2812 let bytes = match vfs.read(&manifest_path) {
2813 Ok(bytes) => bytes,
2814 Err(source) if source.kind() == std::io::ErrorKind::NotFound => return Ok(()),
2815 Err(source) => {
2816 return Err(StoreError::Io {
2817 path: manifest_path,
2818 source,
2819 });
2820 }
2821 };
2822 let manifest = crate::manifest::decode_manifest(&manifest_path.display().to_string(), &bytes)
2823 .map_err(StoreError::Manifest)?;
2824 let declared = if options.inspection {
2825 manifest.epoch_alias
2826 } else {
2827 options
2828 .epoch
2829 .as_ref()
2830 .map(crate::epoch::StoreEpoch::identity)
2831 };
2832 validate_epoch_identity(manifest.epoch_alias, declared, true, options.access_mode)
2833}
2834
2835fn validate_epoch_identity(
2836 persisted: Option<crate::epoch::EpochIdentity>,
2837 declared: Option<crate::epoch::EpochIdentity>,
2838 manifest_exists: bool,
2839 access_mode: AccessMode,
2840) -> Result<(), StoreError> {
2841 match (persisted, declared) {
2842 (Some(expected), Some(declared)) if expected != declared => {
2843 Err(StoreError::EpochMismatch(crate::epoch::EpochMismatch {
2844 expected,
2845 declared,
2846 }))
2847 }
2848 (Some(_), None) => Err(StoreError::EpochUndeclared),
2849 (None, Some(_)) if manifest_exists || access_mode == AccessMode::ReadOnly => {
2850 Err(StoreError::EpochUnstamped)
2851 }
2852 (Some(_), Some(_)) | (None, Some(_)) | (None, None) => Ok(()),
2853 }
2854}
2855
2856fn validate_tokenizer_epoch(
2857 expected: Option<crate::epoch::EpochIdentity>,
2858 tokenizer: &crate::fts::tokenizer::Analyzer,
2859) -> Result<(), StoreError> {
2860 if let Some(expected) = expected
2861 && tokenizer.epoch() != expected.tokenizer
2862 {
2863 return Err(StoreError::EpochMismatch(crate::epoch::EpochMismatch {
2864 expected,
2865 declared: crate::epoch::EpochIdentity {
2866 embedding: expected.embedding,
2867 tokenizer: tokenizer.epoch(),
2868 },
2869 }));
2870 }
2871 Ok(())
2872}
2873
2874fn cleanup_open_orphans(
2875 store: &Store,
2876) -> Result<crate::manifest::io::OrphanCleanupReport, StoreError> {
2877 let (reachable_segments, graph_enabled) = store
2878 .snapshot
2879 .read()
2880 .map_err(|_| StoreError::Synchronization {
2881 component: "published snapshot",
2882 })?
2883 .as_ref()
2884 .map(|snapshot| {
2885 let reachable = snapshot
2886 .all_segments()
2887 .iter()
2888 .map(|segment| store.directory.join(segment.meta().id.file_name()))
2889 .collect::<HashSet<_>>();
2890 #[cfg(feature = "graph-cypher")]
2891 let graph_enabled = snapshot.graph_enabled;
2892 #[cfg(not(feature = "graph-cypher"))]
2893 let graph_enabled = false;
2894 (reachable, graph_enabled)
2895 })
2896 .ok_or(StoreError::Closed)?;
2897 let mut publication = Some(
2898 store
2899 .wal_writer
2900 .lock()
2901 .map_err(|_| StoreError::Synchronization {
2902 component: "WAL writer",
2903 })?
2904 .as_ref()
2905 .ok_or(StoreError::ReadOnly)?
2906 .manifest_publication()?,
2907 );
2908 let report = crate::manifest::io::cleanup_store_orphans(
2909 store.vfs.as_ref(),
2910 &store.directory,
2911 &reachable_segments,
2912 graph_enabled,
2913 store.durability_policy,
2914 || {
2915 publication = publication
2918 .take()
2919 .map(crate::ingest::ManifestPublication::arm);
2920 },
2921 )
2922 .map_err(StoreError::Manifest)?;
2923 if let Some(publication) = publication {
2924 publication.complete();
2925 }
2926 Ok(report)
2927}
2928
2929impl Store {
2930 pub fn open_for_inspection(path: impl AsRef<Path>) -> Result<Self, StoreError> {
2933 let mut options = OpenOptions::read_only();
2934 options.inspection = true;
2935 Self::open(path, options)
2936 }
2937
2938 pub fn open(path: impl AsRef<Path>, options: OpenOptions) -> Result<Self, StoreError> {
2940 let path = path.as_ref();
2941 namespace_batch::validate_store_path(path)?;
2942 if options.access_mode == AccessMode::ReadWrite {
2943 namespace_batch::reclaim_for_open(path)?;
2944 }
2945 let _admission =
2946 namespace_batch::reader_admission(path, options.access_mode == AccessMode::ReadWrite)?;
2947 let selected = namespace_batch::resolve(path)?;
2948 let logical_lock = if selected != path {
2949 acquire_writer_lock(path, options.access_mode)?
2950 } else {
2951 None
2952 };
2953 if logical_lock.is_some() && namespace_batch::resolve(path)? != selected {
2955 return Err(StoreError::StoreBusy {
2956 path: path.to_path_buf(),
2957 });
2958 }
2959 if logical_lock.is_some() {
2960 if let Some(root) = path.parent() {
2963 crate::vfs::Vfs::sync(&crate::vfs::StdVfs, root, crate::vfs::SyncKind::Full)
2964 .map_err(|source| StoreError::Io {
2965 path: root.to_path_buf(),
2966 source,
2967 })?;
2968 }
2969 }
2970 let store = Self::open_with_infrastructure(
2971 &selected,
2972 options,
2973 Arc::new(crate::vfs::StdVfs),
2974 Arc::new(SystemMonotonicClock),
2975 None,
2976 #[cfg(any(test, feature = "test-seams"))]
2977 None,
2978 #[cfg(any(test, feature = "test-seams"))]
2979 None,
2980 #[cfg(any(test, feature = "test-seams"))]
2981 None,
2982 #[cfg(any(test, feature = "test-seams"))]
2983 None,
2984 #[cfg(any(test, feature = "test-seams"))]
2985 None,
2986 #[cfg(any(test, feature = "test-seams"))]
2987 None,
2988 #[cfg(any(test, feature = "test-seams"))]
2989 None,
2990 )?;
2991 if store
2992 .writer_lock
2993 .lock()
2994 .map_err(|_| StoreError::Synchronization {
2995 component: "writer lock",
2996 })?
2997 .is_some()
2998 && namespace_batch::resolve(path)? != selected
2999 {
3000 return Err(StoreError::StoreBusy {
3001 path: path.to_path_buf(),
3002 });
3003 }
3004 *store
3005 .logical_writer_lock
3006 .lock()
3007 .map_err(|_| StoreError::Synchronization {
3008 component: "logical writer lock",
3009 })? = logical_lock;
3010 Ok(store)
3011 }
3012
3013 pub(crate) fn open_private_preparation(
3014 path: &Path,
3015 options: OpenOptions,
3016 authority: namespace_batch::PrivatePreparation,
3017 ) -> Result<Self, StoreError> {
3018 Self::open_with_infrastructure(
3019 path,
3020 options,
3021 Arc::new(crate::vfs::StdVfs),
3022 Arc::new(SystemMonotonicClock),
3023 Some(authority),
3024 #[cfg(any(test, feature = "test-seams"))]
3025 None,
3026 #[cfg(any(test, feature = "test-seams"))]
3027 None,
3028 #[cfg(any(test, feature = "test-seams"))]
3029 None,
3030 #[cfg(any(test, feature = "test-seams"))]
3031 None,
3032 #[cfg(any(test, feature = "test-seams"))]
3033 None,
3034 #[cfg(any(test, feature = "test-seams"))]
3035 None,
3036 #[cfg(any(test, feature = "test-seams"))]
3037 None,
3038 )
3039 }
3040
3041 #[cfg(feature = "graph-cypher")]
3042 pub(crate) fn open_graph_with_infrastructure(
3043 path: &Path,
3044 mut options: OpenOptions,
3045 document: Option<crate::epoch::EmbeddingTower>,
3046 vfs: Arc<dyn crate::vfs::Vfs>,
3047 clock: Arc<dyn MonotonicClock>,
3048 ) -> Result<Self, StoreError> {
3049 options.graph_document = document;
3050 options.max_resident_bytes = options
3051 .max_resident_bytes
3052 .min(crate::property_graph::resources::MAX_GRAPH_RESIDENT_BYTES);
3053 Self::open_with_infrastructure(
3054 path,
3055 options,
3056 vfs,
3057 clock,
3058 None,
3059 #[cfg(any(test, feature = "test-seams"))]
3060 None,
3061 #[cfg(any(test, feature = "test-seams"))]
3062 None,
3063 #[cfg(any(test, feature = "test-seams"))]
3064 None,
3065 #[cfg(any(test, feature = "test-seams"))]
3066 None,
3067 #[cfg(any(test, feature = "test-seams"))]
3068 None,
3069 #[cfg(any(test, feature = "test-seams"))]
3070 None,
3071 #[cfg(any(test, feature = "test-seams"))]
3072 None,
3073 )
3074 }
3075
3076 #[allow(
3077 clippy::too_many_arguments,
3078 reason = "test-support controllers are explicit optional infrastructure dependencies"
3079 )]
3080 pub(crate) fn open_with_infrastructure(
3081 path: impl AsRef<Path>,
3082 options: OpenOptions,
3083 vfs: Arc<dyn crate::vfs::Vfs>,
3084 clock: Arc<dyn MonotonicClock>,
3085 private_preparation: Option<namespace_batch::PrivatePreparation>,
3086 #[cfg(any(test, feature = "test-seams"))] hybrid_leg_fault: Option<HybridLegTestFault>,
3087 #[cfg(any(test, feature = "test-seams"))] storage_fault_controller: Option<
3088 StorageFaultController,
3089 >,
3090 #[cfg(any(test, feature = "test-seams"))] ingest_retention_fault_controller: Option<
3091 crate::ingest::IngestRetentionFaultController,
3092 >,
3093 #[cfg(any(test, feature = "test-seams"))] metadata_test_controller: Option<
3094 Arc<crate::planner::MetadataTestController>,
3095 >,
3096 #[cfg(any(test, feature = "test-seams"))] vector_fault_controller: Option<
3097 crate::scan::vector_fault::VectorFaultController,
3098 >,
3099 #[cfg(any(test, feature = "test-seams"))] kernel_fault_controller: Option<
3100 crate::kernels::vector_fault::KernelFaultController,
3101 >,
3102 #[cfg(any(test, feature = "test-seams"))] vector_seal_scheme: Option<
3103 crate::quant::QuantScheme,
3104 >,
3105 ) -> Result<Self, StoreError> {
3106 let path = path.as_ref();
3107 #[cfg(any(test, feature = "test-seams"))]
3108 match kernel_fault_controller.as_ref() {
3109 Some(controller) => {
3110 controller
3111 .initialize_for_store()
3112 .map_err(StoreError::Kernel)?;
3113 }
3114 None => {
3115 crate::kernels::initialize().map_err(StoreError::Kernel)?;
3116 }
3117 }
3118 #[cfg(not(any(test, feature = "test-seams")))]
3119 crate::kernels::initialize().map_err(StoreError::Kernel)?;
3120 #[cfg(any(test, feature = "test-seams"))]
3121 let vfs = match storage_fault_controller.as_ref() {
3122 Some(controller) => Arc::new(StorageFaultVfs {
3123 inner: vfs,
3124 controller: controller.clone(),
3125 }) as Arc<dyn crate::vfs::Vfs>,
3126 None => vfs,
3127 };
3128 let durability_policy = DurabilityPolicy::new(options.durability_mode, options.commit_tier)
3129 .map_err(StoreError::Durability)?;
3130 refuse_native_graph_directory(vfs.as_ref(), path)?;
3131 let accounting = Arc::new(stats::Accounting::new(
3132 options.max_resident_bytes,
3133 options.max_temp_bytes,
3134 ));
3135 let is_directory = vfs
3136 .ensure_directory(path, options.access_mode == AccessMode::ReadWrite)
3137 .map_err(|source| StoreError::Io {
3138 path: path.to_path_buf(),
3139 source,
3140 })?;
3141 if !is_directory {
3142 return Err(StoreError::NotDirectory {
3143 path: path.to_path_buf(),
3144 });
3145 }
3146 validate_persisted_epoch_before_open(vfs.as_ref(), path, &options)?;
3147 let _reclamation_admission = if private_preparation.is_some() {
3148 namespace_batch::validate_private_preparation(
3149 vfs.as_ref(),
3150 path,
3151 private_preparation.as_ref(),
3152 )?;
3153 None
3154 } else {
3155 namespace_batch::reader_admission(path, options.access_mode == AccessMode::ReadWrite)?
3156 };
3157 let reclamation_pin =
3158 namespace_batch::reader_lease(path, options.access_mode == AccessMode::ReadWrite)?
3159 .map(Arc::new);
3160 namespace_batch::refuse_retired(vfs.as_ref(), path)?;
3161 #[cfg(feature = "graph-cypher")]
3162 let reader_store_lock = if options.access_mode == AccessMode::ReadOnly
3163 && vfs
3164 .read_range(&path.join(crate::manifest::io::MANIFEST_FILE), 10, 2)
3165 .is_ok_and(|version| version.as_slice() == 3_u16.to_le_bytes())
3166 {
3167 Some(
3168 StoreLock::acquire_shared(path).map_err(|error| match error {
3169 StoreLockError::Io { path, source }
3170 if source.kind() == std::io::ErrorKind::WouldBlock =>
3171 {
3172 StoreError::StoreBusy { path }
3173 }
3174 error => StoreError::Lock(error),
3175 })?,
3176 )
3177 } else {
3178 None
3179 };
3180 let writer_lock = acquire_writer_lock(path, options.access_mode)?;
3181 if options.access_mode == AccessMode::ReadWrite && private_preparation.is_none() {
3182 namespace_batch::sync_portable_publication_for_open(vfs.as_ref(), path)?;
3183 }
3184 validate_persisted_epoch_before_open(vfs.as_ref(), path, &options)?;
3187 let mut wal_tail_cut = false;
3188 if options.access_mode == AccessMode::ReadWrite {
3189 wal_tail_cut =
3192 crate::ingest::cut_interrupted_append(vfs.as_ref(), path, durability_policy)?;
3193 }
3194 if options.access_mode == AccessMode::ReadWrite && private_preparation.is_none() {
3195 namespace_batch::adopt_for_open(vfs.as_ref(), path)?;
3196 }
3197 let manifest_path = path.join(crate::manifest::io::MANIFEST_FILE);
3198 let manifest_exists = match vfs.open(&manifest_path) {
3199 Ok(_) => true,
3200 Err(source) if source.kind() == std::io::ErrorKind::NotFound => false,
3201 Err(source) => {
3202 return Err(StoreError::Io {
3203 path: manifest_path,
3204 source,
3205 });
3206 }
3207 };
3208 let mut snapshot = PublishedSnapshot::load_for_open_on_vfs(
3213 path,
3214 &accounting,
3215 vfs.as_ref(),
3216 private_preparation.as_ref(),
3217 )?;
3218 #[cfg(feature = "graph-cypher")]
3219 if options.access_mode == AccessMode::ReadOnly
3220 && snapshot.graph_enabled
3221 && reader_store_lock.is_none()
3222 {
3223 return Err(StoreError::StoreBusy {
3226 path: path.join(lock::STORE_LOCK_FILE),
3227 });
3228 }
3229 let (schema, schema_evolved) = resolve_open_schema(
3230 manifest_exists,
3231 &snapshot,
3232 options.schema.as_ref(),
3233 options.access_mode,
3234 )?;
3235 let persisted_epoch = snapshot.epoch_alias();
3236 let declared_epoch = if options.inspection {
3237 persisted_epoch
3238 } else {
3239 options
3240 .epoch
3241 .as_ref()
3242 .map(crate::epoch::StoreEpoch::identity)
3243 };
3244 validate_epoch_identity(
3245 persisted_epoch,
3246 declared_epoch,
3247 manifest_exists,
3248 options.access_mode,
3249 )?;
3250 let tokenizer = crate::fts::tokenizer::Analyzer::new(
3251 options
3252 .tokenizer
3253 .clone()
3254 .unwrap_or_else(crate::fts::tokenizer::TokenizerConfig::text_default),
3255 )
3256 .map_err(StoreError::Tokenizer)?;
3257 validate_tokenizer_epoch(persisted_epoch.or(declared_epoch), &tokenizer)?;
3258 let absorbed_through = snapshot.absorbed_through();
3259 let wal_path = path.join("wal.ze");
3260 let recovered = crate::ingest::ActiveState::recover(
3261 vfs.as_ref(),
3262 &wal_path,
3263 snapshot.generation(),
3264 absorbed_through,
3265 snapshot.graph_absorbed_through,
3266 #[cfg(feature = "graph-cypher")]
3267 snapshot.graph_manifest.as_ref().map(|graph| &***graph),
3268 #[cfg(feature = "graph-cypher")]
3269 snapshot.graph_enabled,
3270 &accounting,
3271 &schema,
3272 &tokenizer,
3273 private_preparation.as_ref(),
3274 )?;
3275 let mut active = recovered.active;
3276 let recovered_wal = recovered.wal;
3277 let sealed_tombstones = recovered.tombstones;
3278 #[cfg(feature = "graph-cypher")]
3279 let graph_replay = recovered.graph;
3280 let mut open_publication = None;
3281 #[cfg(feature = "graph-cypher")]
3282 let mut pending_graph_schema = None;
3283 if schema_evolved {
3284 let boundary = recovered_wal
3285 .as_ref()
3286 .map_or(absorbed_through, crate::wal::CleanWalReader::durable_end);
3287 let generation = active
3288 .generation
3289 .checked_add(1)
3290 .ok_or(StoreError::GenerationOverflow)?;
3291 let mut manifest =
3292 crate::manifest::io::load_manifest(vfs.as_ref(), &manifest_path, boundary)
3293 .map_err(StoreError::Manifest)?;
3294 manifest.generation = generation;
3295 manifest.schema = schema.clone();
3296 #[cfg(feature = "graph-cypher")]
3297 manifest
3298 .record_generation_bump(boundary)
3299 .map_err(StoreError::Manifest)?;
3300 let mut publication = crate::ingest::ManifestPublication::before_writer_creation();
3301 #[cfg(feature = "graph-cypher")]
3302 let defer_commit = snapshot.graph_enabled;
3303 #[cfg(not(feature = "graph-cypher"))]
3304 let defer_commit = false;
3305 if !defer_commit {
3306 publication
3307 .commit_manifest(vfs.as_ref(), path, &manifest, durability_policy)
3308 .map_err(StoreError::Manifest)?;
3309 }
3310 snapshot =
3311 PublishedSnapshot::from_manifest(vfs.as_ref(), path, &manifest, &accounting)?;
3312 active.generation = generation;
3313 #[cfg(feature = "graph-cypher")]
3314 if defer_commit {
3315 pending_graph_schema = Some(manifest);
3316 }
3317 open_publication = Some(publication);
3318 }
3319 if options.access_mode == AccessMode::ReadWrite
3320 && !manifest_exists
3321 && (options.epoch.is_some() || options.schema.is_some())
3322 {
3323 let mut publication = crate::ingest::ManifestPublication::before_writer_creation();
3324 publication
3325 .commit_manifest(
3326 vfs.as_ref(),
3327 path,
3328 &crate::manifest::Manifest {
3329 #[cfg(feature = "graph-cypher")]
3330 graph: None,
3331 generation: active.generation,
3332 log_seq: 0,
3333 segments: Vec::new(),
3334 epochs: options
3335 .epoch
3336 .as_ref()
3337 .cloned()
3338 .map(crate::manifest::EpochMeta::from)
3339 .into_iter()
3340 .collect(),
3341 epoch_alias: options
3342 .epoch
3343 .as_ref()
3344 .map(crate::epoch::StoreEpoch::identity),
3345 schema: schema.clone(),
3346 },
3347 durability_policy,
3348 )
3349 .map_err(StoreError::Manifest)?;
3350 open_publication = Some(publication);
3351 }
3352 let wal_writer = match options.access_mode {
3353 AccessMode::ReadWrite => Some(match recovered_wal {
3354 Some(recovered) => crate::ingest::StoreWal::resume(
3355 vfs.as_ref(),
3356 &wal_path,
3357 recovered,
3358 durability_policy,
3359 absorbed_through,
3360 &accounting,
3361 )?,
3362 None => crate::ingest::StoreWal::create(
3363 Arc::clone(&vfs),
3364 path,
3365 &wal_path,
3366 durability_policy,
3367 &accounting,
3368 )?,
3369 }),
3370 AccessMode::ReadOnly => {
3371 drop(recovered_wal);
3372 None
3373 }
3374 };
3375 if let Some(publication) = open_publication.as_mut() {
3376 publication.bind_writer(wal_writer.as_ref().ok_or(StoreError::ReadOnly)?);
3377 }
3378 let background = match options.access_mode {
3379 AccessMode::ReadWrite => Some(BackgroundThread::start()?),
3380 AccessMode::ReadOnly => None,
3381 };
3382 #[cfg(feature = "graph-cypher")]
3383 let native_graph =
3384 native_graph::NativeGraphPublication::new(&accounting, snapshot.graph_enabled)?;
3385 #[cfg(test)]
3386 let (snapshot, background, teardown_probe) = {
3387 let mut snapshot = snapshot;
3388 let mut background = background;
3389 let teardown_probe = Arc::new(close::TeardownProbe::new());
3390 snapshot.set_teardown_probe(Arc::clone(&teardown_probe));
3391 if let Some(background) = background.as_mut() {
3392 background.set_teardown_probe(Arc::clone(&teardown_probe));
3393 }
3394 (snapshot, background, teardown_probe)
3395 };
3396 let mut store = Self {
3397 private_preparation,
3398 open_migrations: OpenMigrations {
3399 schema_added: schema_evolved,
3400 wal_tail_cut,
3401 generation: active.generation,
3402 },
3403 directory: path.to_path_buf(),
3404 vfs,
3405 clock,
3406 state: Mutex::new(StoreState::Open),
3407 state_changed: Condvar::new(),
3408 background: Mutex::new(background),
3409 query_pool: Mutex::new(None),
3410 lexical_worker: Mutex::new(None),
3411 #[cfg(feature = "graph-cypher")]
3412 native_graph,
3413 #[cfg(feature = "graph-cypher")]
3414 graph_enable_pending: std::sync::atomic::AtomicBool::new(false),
3415 snapshot: RwLock::new(None),
3416 active: Mutex::new(Some(active)),
3417 wal_writer: Mutex::new(wal_writer),
3418 writer_lock: Mutex::new(writer_lock.map(Arc::new)),
3419 #[cfg(feature = "graph-cypher")]
3420 reader_store_lock: Mutex::new(reader_store_lock),
3421 snapshot_pins: Arc::new(AtomicU64::new(0)),
3422 snapshot_pin: Mutex::new(None),
3423 logical_writer_lock: Mutex::new(None),
3424 reclamation_pin: Mutex::new(reclamation_pin),
3425 maintenance: Mutex::new(()),
3426 health_state: Mutex::new(crate::diag::HealthState::default()),
3427 durability_policy,
3428 reader_drain_timeout: options.reader_drain_timeout,
3429 accounting,
3430 active_queries: AtomicU64::new(0),
3431 #[cfg(any(test, feature = "test-seams"))]
3432 text_materialization_work: materialize::TestMaterializationWork::default(),
3433 epoch_alias: crate::epoch::EpochAliasCell::new(persisted_epoch.or(declared_epoch)),
3434 epoch: options.epoch,
3435 tokenizer,
3436 schema,
3437 lexical_index_cache: LexicalIndexCache::new(),
3438 #[cfg(any(test, feature = "test-seams"))]
3439 ingest_retention_fault_controller,
3440 #[cfg(any(test, feature = "test-seams"))]
3441 hybrid_leg_fault: Mutex::new(hybrid_leg_fault),
3442 #[cfg(any(test, feature = "test-seams"))]
3443 hybrid_execution_receipt: Mutex::new(None),
3444 #[cfg(any(test, feature = "test-seams"))]
3445 metadata_test_controller,
3446 #[cfg(any(test, feature = "test-seams"))]
3447 vector_fault_controller,
3448 #[cfg(any(test, feature = "test-seams"))]
3449 kernel_fault_controller,
3450 #[cfg(any(test, feature = "test-seams"))]
3451 vector_seal_scheme,
3452 #[cfg(test)]
3453 teardown_probe,
3454 };
3455 #[cfg(feature = "graph-cypher")]
3456 if let Some(graph) = snapshot.graph_manifest.as_ref() {
3457 native_graph::recovery::install_unified(
3458 &store,
3459 graph,
3460 snapshot.generation(),
3461 &graph_replay,
3462 options.access_mode,
3463 options.graph_document.clone(),
3464 )?;
3465 }
3466 #[cfg(feature = "graph-cypher")]
3467 if let Some(manifest) = pending_graph_schema {
3468 open_publication
3472 .as_mut()
3473 .ok_or(StoreError::Synchronization {
3474 component: "schema publication",
3475 })?
3476 .commit_manifest(store.vfs.as_ref(), path, &manifest, durability_policy)
3477 .map_err(StoreError::Manifest)?;
3478 }
3479 if options.access_mode == AccessMode::ReadWrite && manifest_exists {
3480 match durability_policy.directory_sync() {
3484 SyncRequirement::Skip => {}
3485 SyncRequirement::Sync(kind) => {
3486 store
3487 .vfs
3488 .sync(path, kind)
3489 .map_err(|source| StoreError::Io {
3490 path: path.to_path_buf(),
3491 source,
3492 })?;
3493 }
3494 }
3495 }
3496 #[cfg(feature = "graph-cypher")]
3497 let repair_before_purge = snapshot.graph_enabled;
3498 #[cfg(not(feature = "graph-cypher"))]
3499 let repair_before_purge = false;
3500 store.publish_snapshot(snapshot)?;
3501 if let Some(publication) = open_publication {
3502 publication.complete();
3503 }
3504 if repair_before_purge {
3505 store.recover_sealed_tombstones(&sealed_tombstones)?;
3507 }
3508 #[cfg(feature = "graph-cypher")]
3509 if options.access_mode == AccessMode::ReadWrite
3510 && store.native_graph.has_pending_reclaim()?
3511 {
3512 for _ in 0..2 {
3513 let admission = store.admit_native_graph_maintenance().map_err(|error| {
3514 StoreError::Manifest(crate::manifest::ManifestError::Decode(error.to_string()))
3515 })?;
3516 match store.commit_native_graph_maintenance(
3517 &admission,
3518 &QueryControl::Cancel(CancelToken::new()),
3519 ) {
3520 Ok(_) => break,
3521 Err(native_graph::NativeGraphError::StalePreparation) => continue,
3522 Err(error) => {
3523 return Err(StoreError::Manifest(
3524 crate::manifest::ManifestError::Decode(error.to_string()),
3525 ));
3526 }
3527 }
3528 }
3529 if store.native_graph.has_pending_reclaim()? {
3530 return Err(StoreError::Manifest(
3531 crate::manifest::ManifestError::Decode(
3532 "pending reclaim recovery did not finish after its manifest fold"
3533 .to_owned(),
3534 ),
3535 ));
3536 }
3537 }
3538 store
3539 .recover_pending_physical_purge()
3540 .map_err(|error| StoreError::PurgeRecovery {
3541 detail: error.to_string(),
3542 })?;
3543 if !repair_before_purge {
3544 store.recover_sealed_tombstones(&sealed_tombstones)?;
3546 }
3547 if options.access_mode == AccessMode::ReadWrite {
3548 let cleanup_report = cleanup_open_orphans(&store)?;
3549 #[cfg(any(test, feature = "test-seams"))]
3550 if let Some(controller) = storage_fault_controller.as_ref() {
3551 controller.record_cleanup_report(cleanup_report);
3552 }
3553 #[cfg(not(any(test, feature = "test-seams")))]
3554 let _ = cleanup_report;
3555 }
3556 #[cfg(any(test, feature = "test-seams"))]
3557 if let Some(controller) = storage_fault_controller.as_ref() {
3558 let published = store
3559 .snapshot
3560 .read()
3561 .map_err(|_| StoreError::Synchronization {
3562 component: "published snapshot",
3563 })?;
3564 let snapshot = published.as_ref().ok_or(StoreError::Closed)?;
3565 for segment in snapshot.all_segments() {
3566 register_storage_segment_controller(
3567 &store.directory,
3568 segment.meta().id,
3569 controller,
3570 );
3571 }
3572 }
3573 store.open_migrations.generation = store
3574 .active
3575 .lock()
3576 .map_err(|_| StoreError::Synchronization {
3577 component: "active segment",
3578 })?
3579 .as_ref()
3580 .ok_or(StoreError::Closed)?
3581 .generation;
3582 Ok(store)
3583 }
3584
3585 #[cfg(any(test, feature = "test-seams"))]
3587 #[doc(hidden)]
3588 pub fn open_with_test_dependencies(
3589 path: impl AsRef<Path>,
3590 options: OpenOptions,
3591 dependencies: StoreTestDependencies,
3592 ) -> Result<Self, StoreError> {
3593 let controller = dependencies.storage_fault_controller.clone();
3594 with_storage_open_controller(controller.as_ref(), || {
3595 Self::open_with_infrastructure(
3596 path,
3597 options,
3598 dependencies.vfs,
3599 dependencies.clock,
3600 None,
3601 dependencies.hybrid_leg_fault,
3602 dependencies.storage_fault_controller,
3603 dependencies.ingest_retention_fault_controller,
3604 dependencies.metadata_test_controller,
3605 dependencies.vector_fault_controller,
3606 dependencies.kernel_fault_controller,
3607 dependencies.vector_seal_scheme,
3608 )
3609 })
3610 }
3611
3612 #[cfg(any(test, feature = "test-seams"))]
3614 #[doc(hidden)]
3615 pub fn take_hybrid_execution_receipt(&self) -> Option<HybridExecutionReceipt> {
3616 self.hybrid_execution_receipt
3617 .lock()
3618 .ok()
3619 .and_then(|mut receipt| receipt.take())
3620 }
3621
3622 pub(crate) fn epoch_registry(
3630 &self,
3631 prior: &[crate::manifest::EpochMeta],
3632 ) -> Vec<crate::manifest::EpochMeta> {
3633 if prior.is_empty() {
3634 self.epoch
3635 .as_ref()
3636 .map(crate::manifest::EpochMeta::from)
3637 .into_iter()
3638 .collect()
3639 } else {
3640 prior.to_vec()
3641 }
3642 }
3643
3644 pub fn open_migrations(&self) -> OpenMigrations {
3646 self.open_migrations
3647 }
3648
3649 #[must_use]
3652 pub fn epoch_identity(&self) -> Option<crate::epoch::EpochIdentity> {
3653 self.epoch_alias.load()
3654 }
3655
3656 #[must_use]
3658 pub const fn schema(&self) -> &crate::meta::Schema {
3659 &self.schema
3660 }
3661
3662 pub fn state(&self) -> Result<StoreState, StoreError> {
3664 self.state
3665 .lock()
3666 .map(|state| *state)
3667 .map_err(|_| StoreError::Synchronization { component: "state" })
3668 }
3669
3670 pub fn snapshot(&self) -> Result<SnapshotLease, StoreError> {
3672 let state = self
3673 .state
3674 .lock()
3675 .map_err(|_| StoreError::Synchronization { component: "state" })?;
3676 match *state {
3677 StoreState::Open => {}
3678 StoreState::Closing => return Err(StoreError::Closing),
3679 StoreState::Closed => return Err(StoreError::Closed),
3680 }
3681 let active = self
3682 .active
3683 .lock()
3684 .map_err(|_| StoreError::Synchronization {
3685 component: "active segment",
3686 })?;
3687 let generation = active.as_ref().ok_or(StoreError::Closed)?.generation;
3688 let snapshot = self
3689 .snapshot
3690 .read()
3691 .map_err(|_| StoreError::Synchronization {
3692 component: "published snapshot",
3693 })?
3694 .as_ref()
3695 .cloned()
3696 .ok_or(StoreError::Closed)?;
3697 drop(active);
3698 drop(state);
3699 Ok(SnapshotLease::new_at(snapshot, generation))
3700 }
3701
3702 #[cfg(any(test, feature = "test-seams"))]
3708 #[doc(hidden)]
3709 pub fn test_metadata_row_values(
3710 &self,
3711 row_id: crate::ingest::GlobalRowId,
3712 ) -> Result<Vec<(crate::meta::ColumnId, Option<crate::meta::PredicateValue>)>, StoreError> {
3713 match row_id.source() {
3714 crate::ingest::RowSource::Active => {
3715 let row = usize::try_from(row_id.local_row())
3716 .map_err(|_| StoreError::ActiveRowOverflow)?;
3717 let active = self
3718 .active
3719 .lock()
3720 .map_err(|_| StoreError::Synchronization {
3721 component: "active segment",
3722 })?;
3723 let segment = &active.as_ref().ok_or(StoreError::Closed)?.segment;
3724 let timestamp = segment
3725 .timestamps()
3726 .get(row)
3727 .copied()
3728 .ok_or(StoreError::ActiveRowOverflow)?;
3729 let mut values = segment.column_values(row)?;
3730 values.push((
3731 crate::meta::TIMESTAMP_COLUMN,
3732 crate::meta::PredicateValue::I64(timestamp),
3733 ));
3734 self.schema
3735 .columns()
3736 .iter()
3737 .map(|definition| {
3738 let value = values
3739 .iter()
3740 .find(|(column, _)| *column == definition.id())
3741 .map(|(_, value)| value.clone());
3742 Ok((definition.id(), value))
3743 })
3744 .collect()
3745 }
3746 crate::ingest::RowSource::Sealed(segment_id) => {
3747 let snapshot = self.snapshot()?;
3748 let segment = snapshot
3749 .segments()
3750 .iter()
3751 .find(|segment| segment.meta().id == segment_id)
3752 .ok_or_else(|| {
3753 StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
3754 "public metadata row names absent segment {segment_id}"
3755 )))
3756 })?;
3757 let columns = segment.columns().map_err(StoreError::Segment)?;
3758 if row_id.local_row() >= columns.row_count() {
3759 return Err(StoreError::Segment(crate::segment::SegmentError::Geometry(
3760 format!(
3761 "public metadata row {} is outside {} rows",
3762 row_id.local_row(),
3763 columns.row_count()
3764 ),
3765 )));
3766 }
3767 columns
3768 .schema()
3769 .columns()
3770 .iter()
3771 .map(|definition| {
3772 let column = columns.column(definition.id()).ok_or_else(|| {
3773 StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
3774 "public metadata row is missing column {}",
3775 definition.id().get()
3776 )))
3777 })?;
3778 let value = match column {
3779 crate::meta::Column::Id128(column) => column
3780 .get(row_id.local_row())
3781 .map(crate::meta::PredicateValue::Id128),
3782 crate::meta::Column::U64(column) => column
3783 .get(row_id.local_row())
3784 .map(crate::meta::PredicateValue::U64),
3785 crate::meta::Column::I64(column) => column
3786 .get(row_id.local_row())
3787 .map(crate::meta::PredicateValue::I64),
3788 crate::meta::Column::F64(column) => column
3789 .get(row_id.local_row())
3790 .map(crate::meta::PredicateValue::F64),
3791 crate::meta::Column::Bool(column) => column
3792 .get(row_id.local_row())
3793 .map(crate::meta::PredicateValue::Bool),
3794 crate::meta::Column::DictionaryString(column) => column
3795 .get(row_id.local_row())
3796 .map(|value| crate::meta::PredicateValue::String(value.to_owned())),
3797 crate::meta::Column::RawString(column) => column
3798 .get(row_id.local_row())
3799 .map(|value| crate::meta::PredicateValue::String(value.to_owned())),
3800 };
3801 Ok((definition.id(), value))
3802 })
3803 .collect()
3804 }
3805 }
3806 }
3807
3808 #[cfg(any(test, feature = "test-seams"))]
3810 #[doc(hidden)]
3811 pub fn test_metadata_evaluate_source(
3812 &self,
3813 source: crate::ingest::RowSource,
3814 predicate: &crate::meta::Predicate,
3815 ) -> Result<(u32, Vec<u32>, Vec<u32>), StoreError> {
3816 let evaluate = |columns: &crate::meta::ColumnStore,
3817 alive: &crate::meta::AliveSet|
3818 -> Result<(u32, Vec<u32>, Vec<u32>), StoreError> {
3819 let matches = crate::meta::evaluate(predicate, columns, alive).map_err(|error| {
3820 StoreError::Segment(crate::segment::SegmentError::Columns(format!(
3821 "test metadata source evaluation: {error}"
3822 )))
3823 })?;
3824 Ok((
3825 columns.row_count(),
3826 alive.iter_alive().collect(),
3827 matches.iter().collect(),
3828 ))
3829 };
3830 match source {
3831 crate::ingest::RowSource::Active => {
3832 let active = self
3833 .active
3834 .lock()
3835 .map_err(|_| StoreError::Synchronization {
3836 component: "active segment",
3837 })?;
3838 let segment = &active.as_ref().ok_or(StoreError::Closed)?.segment;
3839 let mut builder = crate::meta::ColumnStoreBuilder::new(self.schema.clone());
3840 for (row, timestamp) in segment.timestamps().iter().copied().enumerate() {
3841 let values = segment.column_values(row)?;
3842 builder
3843 .push_row(timestamp, &crate::ingest::column_inputs(&values))
3844 .map_err(|error| {
3845 StoreError::Segment(crate::segment::SegmentError::Columns(
3846 error.to_string(),
3847 ))
3848 })?;
3849 }
3850 let columns = builder.finish().map_err(|error| {
3851 StoreError::Segment(crate::segment::SegmentError::Columns(error.to_string()))
3852 })?;
3853 let alive = segment.alive()?;
3854 evaluate(&columns, &alive)
3855 }
3856 crate::ingest::RowSource::Sealed(segment_id) => {
3857 let snapshot = self.snapshot()?;
3858 let segment = snapshot
3859 .segments()
3860 .iter()
3861 .find(|segment| segment.meta().id == segment_id)
3862 .ok_or_else(|| {
3863 StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
3864 "metadata source evaluation names absent segment {segment_id}"
3865 )))
3866 })?;
3867 let columns = segment.columns().map_err(StoreError::Segment)?;
3868 let alive = segment.alive().map_err(StoreError::Segment)?;
3869 evaluate(&columns, &alive)
3870 }
3871 }
3872 }
3873
3874 pub(crate) fn publish_snapshot(&self, snapshot: PublishedSnapshot) -> Result<(), StoreError> {
3875 let state = self
3876 .state
3877 .lock()
3878 .map_err(|_| StoreError::Synchronization { component: "state" })?;
3879 match *state {
3880 StoreState::Open => {}
3881 StoreState::Closing => return Err(StoreError::Closing),
3882 StoreState::Closed => return Err(StoreError::Closed),
3883 }
3884 let generation = snapshot.generation();
3885 let mut active = self
3886 .active
3887 .lock()
3888 .map_err(|_| StoreError::Synchronization {
3889 component: "active segment",
3890 })?;
3891 let active_generation = &mut active.as_mut().ok_or(StoreError::Closed)?.generation;
3892 *active_generation = (*active_generation).max(generation);
3893 let mut published = self
3894 .snapshot
3895 .write()
3896 .map_err(|_| StoreError::Synchronization {
3897 component: "published snapshot",
3898 })?;
3899 *published = Some(Arc::new(snapshot));
3900 drop(active);
3901 drop(state);
3902 Ok(())
3903 }
3904
3905 pub fn top_k_with_options(
3910 &self,
3911 request: crate::scan::ScanRequest<'_>,
3912 k: usize,
3913 options: crate::scan::ScanOptions,
3914 control: QueryControl,
3915 ) -> Result<crate::scan::ScanOutcome, QueryError> {
3916 let control = control.with_clock(Arc::clone(&self.clock));
3917 let state = self
3918 .state
3919 .lock()
3920 .map_err(|_| QueryError::Store(StoreError::Synchronization { component: "state" }))?;
3921 match *state {
3922 StoreState::Open => {}
3923 StoreState::Closing => return Err(QueryError::Store(StoreError::Closing)),
3924 StoreState::Closed => return Err(QueryError::Store(StoreError::Closed)),
3925 }
3926 let snapshot = self
3927 .snapshot
3928 .read()
3929 .map_err(|_| {
3930 QueryError::Store(StoreError::Synchronization {
3931 component: "published snapshot",
3932 })
3933 })?
3934 .as_ref()
3935 .cloned()
3936 .ok_or(QueryError::Store(StoreError::Closed))?;
3937 let mut pool_slot = self.query_pool.lock().map_err(|_| {
3938 QueryError::Store(StoreError::Synchronization {
3939 component: "query pool",
3940 })
3941 })?;
3942 if pool_slot.is_none() {
3943 let capacity = crate::scan::physical_thread_capacity().map_err(|error| {
3944 QueryError::Store(StoreError::QueryPoolCapacity {
3945 source: error.to_string(),
3946 })
3947 })?;
3948 let pool =
3949 pool::QueryPool::start(capacity, &self.accounting).map_err(QueryError::Store)?;
3950 *pool_slot = Some(Arc::new(pool));
3951 }
3952 let pool = pool_slot.as_ref().cloned().ok_or({
3953 QueryError::Store(StoreError::Synchronization {
3954 component: "query pool initialization",
3955 })
3956 })?;
3957 self.active_queries.fetch_add(1, Ordering::Relaxed);
3958 let active = ActiveQuery {
3959 count: &self.active_queries,
3960 };
3961 drop(pool_slot);
3962 drop(state);
3963
3964 let result = pool
3965 .execute(request, k, options, control, SnapshotLease::new(snapshot))
3966 .map(|mut outcome| {
3967 outcome.candidates.truncate(k);
3968 outcome
3969 });
3970 drop(active);
3971 result
3972 }
3973
3974 #[cfg(any(test, feature = "test-seams"))]
3979 pub fn panic_query_worker_for_test(&self) -> Result<(), StoreError> {
3980 let pool = self
3981 .query_pool
3982 .lock()
3983 .map_err(|_| StoreError::Synchronization {
3984 component: "query pool",
3985 })?
3986 .as_ref()
3987 .cloned()
3988 .ok_or(StoreError::Synchronization {
3989 component: "query pool initialization",
3990 })?;
3991 pool.panic_one_and_join()
3992 }
3993
3994 pub fn search(
4003 &self,
4004 request: crate::ingest::SearchRequest<'_>,
4005 k: usize,
4006 options: impl Into<SearchOptions>,
4007 control: QueryControl,
4008 ) -> Result<crate::ingest::SearchOutcome, QueryError> {
4009 self.search_with_graph_bound_mode(
4010 request,
4011 k,
4012 options.into(),
4013 control,
4014 GraphBoundMode::Shared,
4015 )
4016 }
4017
4018 fn admit_lexical_query(&self) -> Result<AdmittedLexicalQuery<'_>, QueryError> {
4019 #[cfg(any(test, feature = "test-seams"))]
4020 self.text_materialization_work
4021 .admissions
4022 .fetch_add(1, Ordering::Relaxed);
4023 #[cfg(all(feature = "query-timing", any(test, feature = "test-seams")))]
4024 let lock_started = std::time::Instant::now();
4025 let state = self
4026 .state
4027 .lock()
4028 .map_err(|_| QueryError::Store(StoreError::Synchronization { component: "state" }))?;
4029 #[cfg(all(feature = "query-timing", any(test, feature = "test-seams")))]
4030 self.text_materialization_work
4031 .observe_lexical_lock(lock_started);
4032 match *state {
4033 StoreState::Open => {}
4034 StoreState::Closing => return Err(QueryError::Store(StoreError::Closing)),
4035 StoreState::Closed => return Err(QueryError::Store(StoreError::Closed)),
4036 }
4037 #[cfg(all(feature = "query-timing", any(test, feature = "test-seams")))]
4038 let lock_started = std::time::Instant::now();
4039 let active_guard = self.active.lock().map_err(|_| {
4040 QueryError::Store(StoreError::Synchronization {
4041 component: "active segment",
4042 })
4043 })?;
4044 #[cfg(all(feature = "query-timing", any(test, feature = "test-seams")))]
4045 self.text_materialization_work
4046 .observe_lexical_lock(lock_started);
4047 let active_state = active_guard
4048 .as_ref()
4049 .ok_or(QueryError::Store(StoreError::Closed))?;
4050 let generation = active_state.generation;
4051 let active = Arc::clone(&active_state.segment);
4052 #[cfg(all(feature = "query-timing", any(test, feature = "test-seams")))]
4053 let lock_started = std::time::Instant::now();
4054 let snapshot = self
4055 .snapshot
4056 .read()
4057 .map_err(|_| {
4058 QueryError::Store(StoreError::Synchronization {
4059 component: "published snapshot",
4060 })
4061 })
4062 .inspect(|_| {
4063 #[cfg(all(feature = "query-timing", any(test, feature = "test-seams")))]
4064 self.text_materialization_work
4065 .observe_lexical_lock(lock_started);
4066 })?
4067 .as_ref()
4068 .cloned()
4069 .ok_or(QueryError::Store(StoreError::Closed))?;
4070 self.active_queries.fetch_add(1, Ordering::Relaxed);
4071 let active_query = ActiveQuery {
4072 count: &self.active_queries,
4073 };
4074 drop(active_guard);
4075 drop(state);
4076 Ok(AdmittedLexicalQuery {
4077 generation,
4078 active,
4079 snapshot: snapshot::ReadSnapshot::new(snapshot),
4080 active_query,
4081 })
4082 }
4083
4084 fn admit_document_read(&self) -> Result<AdmittedDocumentRead<'_>, StoreError> {
4085 let state = self
4086 .state
4087 .lock()
4088 .map_err(|_| StoreError::Synchronization { component: "state" })?;
4089 match *state {
4090 StoreState::Open => {}
4091 StoreState::Closing => return Err(StoreError::Closing),
4092 StoreState::Closed => return Err(StoreError::Closed),
4093 }
4094 let active_guard = self
4095 .active
4096 .lock()
4097 .map_err(|_| StoreError::Synchronization {
4098 component: "active segment",
4099 })?;
4100 let active_state = active_guard.as_ref().ok_or(StoreError::Closed)?;
4101 let generation = active_state.generation;
4102 let active = Arc::clone(&active_state.segment);
4103 let snapshot = self
4104 .snapshot
4105 .read()
4106 .map_err(|_| StoreError::Synchronization {
4107 component: "published snapshot",
4108 })?
4109 .as_ref()
4110 .cloned()
4111 .ok_or(StoreError::Closed)?;
4112 self.active_queries.fetch_add(1, Ordering::Relaxed);
4113 let active_query = ActiveQuery {
4114 count: &self.active_queries,
4115 };
4116 drop(active_guard);
4117 drop(state);
4118 Ok(AdmittedDocumentRead {
4119 generation,
4120 active,
4121 snapshot: snapshot::ReadSnapshot::new(snapshot),
4122 active_query,
4123 })
4124 }
4125
4126 pub fn warm_lexical(
4132 &self,
4133 control: QueryControl,
4134 ) -> Result<(), crate::ingest::StoreLexicalError> {
4135 let control = control.with_clock(Arc::clone(&self.clock));
4136 let admission = self.admit_lexical_query()?;
4137 let lease = SnapshotLease::new_at(Arc::clone(&admission.snapshot), admission.generation);
4138 let cancellation = QueryCancellation::new(&control, &lease);
4139 cancellation.check_graph().map_err(QueryError::Scan)?;
4140 let receipt = assemble_unfiltered_lexical_index(
4141 LexicalInputs {
4142 filter: None,
4143 generation: admission.generation,
4144 cache: &self.lexical_index_cache,
4145 snapshot: &admission.snapshot,
4146 active: &admission.active,
4147 accounting: &self.accounting,
4148 },
4149 true,
4150 Some(&cancellation),
4151 )
4152 .map_err(map_store_lexical_assembly_error)?;
4153 receipt.vocabulary(&self.accounting, &cancellation)?;
4154 Ok(())
4155 }
4156
4157 pub fn search_lexical(
4159 &self,
4160 query: &crate::fts::search::TermQuery,
4161 k: usize,
4162 control: QueryControl,
4163 ) -> Result<crate::ingest::StoreLexicalSearchOutcome, crate::ingest::StoreLexicalError> {
4164 self.search_lexical_then(query, k, control, None, |outcome, _, _, _, _, _| {
4165 Ok(outcome)
4166 })
4167 }
4168
4169 pub fn search_lexical_filtered(
4171 &self,
4172 query: &crate::fts::search::TermQuery,
4173 k: usize,
4174 control: QueryControl,
4175 filter: Option<&QueryFilter>,
4176 ) -> Result<crate::ingest::StoreLexicalSearchOutcome, crate::ingest::StoreLexicalError> {
4177 self.search_lexical_then(query, k, control, filter, |outcome, _, _, _, _, _| {
4178 Ok(outcome)
4179 })
4180 }
4181
4182 fn search_lexical_then<R>(
4183 &self,
4184 query: &crate::fts::search::TermQuery,
4185 k: usize,
4186 control: QueryControl,
4187 filter: Option<&QueryFilter>,
4188 finish: impl FnOnce(
4189 crate::ingest::StoreLexicalSearchOutcome,
4190 &PublishedSnapshot,
4191 &crate::ingest::ActiveSegment,
4192 &LexicalAssembly,
4193 &[crate::fts::search::ScoredDoc],
4194 &QueryCancellation<'_>,
4195 ) -> Result<R, crate::ingest::StoreLexicalError>,
4196 ) -> Result<R, crate::ingest::StoreLexicalError> {
4197 let control = control.with_clock(Arc::clone(&self.clock));
4198 let started = self.clock.now();
4199 let admission_started = timing_start(self.clock.as_ref());
4200 let AdmittedLexicalQuery {
4201 generation,
4202 active,
4203 snapshot,
4204 active_query,
4205 } = self.admit_lexical_query()?;
4206 let admission_time = timing_elapsed(self.clock.as_ref(), admission_started);
4207 let lexical_started = timing_start(self.clock.as_ref());
4208
4209 let lease = SnapshotLease::new_at(Arc::clone(&snapshot), generation);
4210 let cancellation = QueryCancellation::new(&control, &lease);
4211 cancellation.check_graph().map_err(QueryError::Scan)?;
4212 let assembly = assemble_lexical_index(
4213 LexicalInputs {
4214 filter,
4215 generation,
4216 cache: &self.lexical_index_cache,
4217 snapshot: &snapshot,
4218 active: &active,
4219 accounting: &self.accounting,
4220 },
4221 true,
4222 Some(&cancellation),
4223 )
4224 .map_err(map_store_lexical_assembly_error)?;
4225 let mut work = crate::fts::control::WorkCheck::new(|| {
4226 cancellation
4227 .check_graph()
4228 .map_err(QueryError::Scan)
4229 .map_err(crate::ingest::StoreLexicalError::from)
4230 });
4231 work.check_now()?;
4232 let mut allow_lists = Vec::with_capacity(assembly.alive_sets.len());
4233 for alive in &assembly.alive_sets {
4234 work.step()?;
4235 allow_lists.push(alive.alive_bitmap());
4236 }
4237 let lexical = crate::planner::search_lexical_filtered_refs_controlled(
4238 &assembly.index,
4239 query,
4240 k,
4241 crate::fts::bm25::Bm25Params::beir(),
4242 &allow_lists,
4243 None,
4244 || {
4245 cancellation
4246 .check_graph()
4247 .map_err(QueryError::Scan)
4248 .map_err(crate::ingest::StoreLexicalError::from)
4249 },
4250 )?;
4251 cancellation.check_graph().map_err(QueryError::Scan)?;
4252 let mut candidates = Vec::with_capacity(lexical.result.hits.len());
4253 for hit in &lexical.result.hits {
4254 work.step()?;
4255 let document =
4256 structured_lexical_document(&snapshot, &active, &assembly.sources, hit.doc, true)
4257 .map_err(map_store_lexical_document_error)?
4258 .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
4259 candidates.push(crate::ingest::LexicalCandidate {
4260 document,
4261 score: hit.score,
4262 });
4263 }
4264 let mut diagnostics =
4265 crate::diag::QueryDiagnostics::lexical(crate::diag::LexicalDiagnostics {
4266 snapshot_generation: generation,
4267 indexed_through_seq: active
4268 .indexed_through_seq()
4269 .max(crate::wal::LogSeq::new(snapshot.absorbed_through())),
4270 requested_k: k,
4271 returned: candidates.len(),
4272 counters: lexical.result.counters,
4273 elapsed: self.clock.now().saturating_duration_since(started),
4274 });
4275 diagnostics.counters.lexical_cache_hits = usize::from(assembly.cache_hit);
4276 diagnostics.counters.lexical_cache_builds = usize::from(!assembly.cache_hit);
4277 diagnostics.timings =
4278 crate::diag::QUERY_TIMING_ENABLED.then(|| crate::diag::QueryTimings {
4279 admission: admission_time,
4280 lexical: timing_elapsed(self.clock.as_ref(), lexical_started),
4281 ..Default::default()
4282 });
4283 let outcome = crate::ingest::StoreLexicalSearchOutcome {
4284 candidates,
4285 generation,
4286 diagnostics,
4287 };
4288 work.check_now()?;
4289 let result = finish(
4290 outcome,
4291 &snapshot,
4292 &active,
4293 &assembly,
4294 &lexical.result.hits,
4295 &cancellation,
4296 );
4297 drop(active_query);
4298 result
4299 }
4300
4301 pub fn stored_text(
4307 &self,
4308 version: crate::ingest::DocumentVersion,
4309 ) -> Result<Option<String>, crate::ingest::StoreLexicalError> {
4310 let AdmittedLexicalQuery {
4311 active,
4312 snapshot,
4313 active_query,
4314 ..
4315 } = self.admit_lexical_query()?;
4316 #[cfg(any(test, feature = "test-seams"))]
4317 self.text_materialization_work
4318 .reverse_version_lookups
4319 .fetch_add(1, Ordering::Relaxed);
4320 if let Some((row, active_version, _)) = active.existing(version.doc_id())
4321 && active_version == version
4322 {
4323 let text = active
4324 .text(row)
4325 .map_err(crate::ingest::StoreLexicalError::from)?;
4326 drop(active_query);
4327 return Ok(text.map(str::to_owned));
4328 }
4329 for segment in snapshot.segments() {
4330 #[cfg(any(test, feature = "test-seams"))]
4331 self.text_materialization_work
4332 .reverse_version_lookups
4333 .fetch_add(1, Ordering::Relaxed);
4334 let Some(row) = segment
4335 .query_row_for_document_version(version)
4336 .map_err(crate::ingest::StoreLexicalError::from)?
4337 else {
4338 continue;
4339 };
4340 let text = segment
4341 .query_stored_text()
4342 .map_err(StoreError::Segment)
4343 .map_err(crate::ingest::StoreLexicalError::from)?
4344 .and_then(|rows| rows.row(row).flatten())
4345 .map(str::to_owned);
4346 drop(active_query);
4347 return Ok(text);
4348 }
4349 drop(active_query);
4350 Ok(None)
4351 }
4352
4353 pub fn get_documents(
4358 &self,
4359 ids: &[crate::ingest::DocId],
4360 fields: DocumentFields,
4361 ) -> Result<Vec<Option<StoredDocument>>, StoreError> {
4362 self.get_documents_with_generation(ids, fields)
4363 .map(|(_, documents)| documents)
4364 }
4365
4366 #[doc(hidden)]
4370 pub fn get_documents_with_generation(
4371 &self,
4372 ids: &[crate::ingest::DocId],
4373 fields: DocumentFields,
4374 ) -> Result<(u64, Vec<Option<StoredDocument>>), StoreError> {
4375 let AdmittedDocumentRead {
4376 generation,
4377 active,
4378 snapshot,
4379 active_query,
4380 } = self.admit_document_read()?;
4381 let mut documents = Vec::new();
4382 documents
4383 .try_reserve_exact(ids.len())
4384 .map_err(|_| StoreError::AllocationFailed {
4385 needed: allocation_bytes::<Option<StoredDocument>>(ids.len()),
4386 component: "get documents",
4387 })?;
4388 for id in ids {
4389 let mut document = active
4390 .existing(*id)
4391 .filter(|(row, _, _)| !active.is_tombstoned(*row))
4392 .map(|(row, _, _)| materialize_active_document(&active, row, fields))
4393 .transpose()?;
4394 for segment in snapshot.segments() {
4395 for row in segment.query_rows_for_doc_id(*id)? {
4396 if document.is_some() {
4397 return Err(StoreError::DuplicateLiveDocument { doc_id: *id });
4398 }
4399 document = Some(materialize_sealed_document(segment, row, fields)?);
4400 }
4401 }
4402 documents.push(document);
4403 }
4404 drop(active_query);
4405 Ok((generation, documents))
4406 }
4407
4408 pub fn scan_documents(
4413 &self,
4414 request: DocumentScanRequest<'_>,
4415 ) -> Result<DocumentScanPage, QueryError> {
4416 let DocumentScanRequest {
4417 limit,
4418 fields,
4419 order,
4420 cursor,
4421 timestamp_range,
4422 predicate,
4423 control,
4424 } = request;
4425 if limit == 0 || limit > MAX_DOCUMENT_SCAN_LIMIT {
4426 return Err(QueryError::Store(StoreError::InvalidScan {
4427 detail: format!("limit must be in 1..={MAX_DOCUMENT_SCAN_LIMIT}, received {limit}"),
4428 }));
4429 }
4430 if let ScanOrder::Attribute { column, .. } = order {
4431 validate_scan_order_attribute(&self.schema, column)?;
4432 }
4433 if let Some(cursor) = cursor.filter(|cursor| cursor.order != order) {
4434 return Err(QueryError::Store(StoreError::InvalidScan {
4435 detail: format!(
4436 "scan cursor was issued for order {:?}, but the request orders by {order:?}",
4437 cursor.order
4438 ),
4439 }));
4440 }
4441 let predicate = document_scan_predicate(predicate, timestamp_range)?;
4442 if let Some(predicate) = &predicate {
4443 crate::planner::validate_predicate(predicate, &self.schema).map_err(|error| {
4444 QueryError::Store(StoreError::InvalidScan {
4445 detail: error.to_string(),
4446 })
4447 })?;
4448 }
4449 let control = control.with_clock(Arc::clone(&self.clock));
4450 control.checkpoint()?;
4451 let AdmittedDocumentRead {
4452 generation,
4453 active,
4454 snapshot,
4455 active_query,
4456 } = self.admit_document_read().map_err(QueryError::Store)?;
4457 if let Some(cursor) = cursor.filter(|cursor| cursor.generation != generation) {
4458 return Err(QueryError::Store(StoreError::ScanStale {
4459 cursor_generation: cursor.generation,
4460 current_generation: generation,
4461 }));
4462 }
4463 let lease = SnapshotLease::new_at(Arc::clone(&snapshot), generation);
4464 let mut work = 0_usize;
4465 let rows = match order {
4466 ScanOrder::Storage => collect_storage_scan_rows(
4467 &snapshot,
4468 &active,
4469 &self.schema,
4470 predicate.as_ref(),
4471 cursor,
4472 limit,
4473 &control,
4474 &lease,
4475 &mut work,
4476 )?,
4477 ScanOrder::TimestampAscending
4478 | ScanOrder::TimestampDescending
4479 | ScanOrder::Attribute { .. } => collect_sorted_scan_rows(
4480 &snapshot,
4481 &active,
4482 &self.schema,
4483 predicate.as_ref(),
4484 cursor,
4485 limit,
4486 order,
4487 &control,
4488 &lease,
4489 &mut work,
4490 )?,
4491 };
4492 let continuation = rows.get(limit).map(|row| DocumentScanCursor {
4493 generation,
4494 order,
4495 source: row.source,
4496 next_row: row.row,
4497 });
4498 let result_len = rows.len().min(limit);
4499 let mut documents = Vec::new();
4500 documents.try_reserve_exact(result_len).map_err(|_| {
4501 QueryError::Store(StoreError::AllocationFailed {
4502 needed: allocation_bytes::<StoredDocument>(result_len),
4503 component: "scan documents",
4504 })
4505 })?;
4506 for row in rows.into_iter().take(result_len) {
4507 scan_control_checkpoint(&control, &lease, &mut work)?;
4508 let document = match row.source {
4509 DocumentScanSource::Active => {
4510 materialize_active_document(&active, row.row as usize, fields)
4511 .map_err(QueryError::Store)?
4512 }
4513 DocumentScanSource::Sealed(segment_id) => {
4514 let segment = snapshot
4515 .segments()
4516 .iter()
4517 .find(|segment| segment.meta().id == segment_id)
4518 .ok_or_else(|| {
4519 QueryError::Store(StoreError::InvalidScan {
4520 detail: format!("scan row names absent segment {segment_id}"),
4521 })
4522 })?;
4523 materialize_sealed_document(segment, row.row as usize, fields)
4524 .map_err(QueryError::Store)?
4525 }
4526 };
4527 documents.push(document);
4528 }
4529 drop(active_query);
4530 Ok(DocumentScanPage {
4531 documents,
4532 generation,
4533 continuation,
4534 })
4535 }
4536
4537 pub fn count_documents(
4539 &self,
4540 predicate: Option<&crate::meta::Predicate>,
4541 timestamp_range: Option<(i64, i64)>,
4542 ) -> Result<DocumentCount, QueryError> {
4543 let predicate = document_scan_predicate(predicate, timestamp_range)?;
4544 if let Some(predicate) = &predicate {
4545 crate::planner::validate_predicate(predicate, &self.schema).map_err(|error| {
4546 QueryError::Store(StoreError::InvalidScan {
4547 detail: error.to_string(),
4548 })
4549 })?;
4550 }
4551 let AdmittedDocumentRead {
4552 generation,
4553 active,
4554 snapshot,
4555 active_query,
4556 } = self.admit_document_read().map_err(QueryError::Store)?;
4557 let mut count = 0_u64;
4558 for segment in snapshot.segments() {
4559 count = count
4560 .checked_add(sealed_scan_rows(segment, predicate.as_ref())?.cardinality())
4561 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
4562 }
4563 count = count
4564 .checked_add(active_scan_rows(&active, &self.schema, predicate.as_ref())?.cardinality())
4565 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
4566 drop(active_query);
4567 Ok(DocumentCount { count, generation })
4568 }
4569
4570 pub(crate) fn live_document_ids_matching(
4574 &self,
4575 active: &crate::ingest::ActiveSegment,
4576 segments: &[crate::segment::reader::SegmentReader],
4577 predicate: &crate::meta::Predicate,
4578 ) -> Result<Vec<crate::ingest::DocId>, QueryError> {
4579 let mut ids = Vec::new();
4580 for segment in segments {
4581 for row in sealed_scan_rows(segment, Some(predicate))?.iter() {
4582 ids.push(sealed_document_scan_row(segment, row)?.doc_id);
4583 }
4584 }
4585 for row in active_scan_rows(active, &self.schema, Some(predicate))?.iter() {
4586 ids.push(active_document_scan_row(active, row)?.doc_id);
4587 }
4588 ids.sort_unstable();
4589 ids.dedup();
4590 Ok(ids)
4591 }
4592
4593 pub fn search_lexical_structured(
4596 &self,
4597 query: &crate::fts::query::LexicalQuery,
4598 k: usize,
4599 snippet_bytes: usize,
4600 control: QueryControl,
4601 ) -> Result<crate::ingest::StoreStructuredLexicalSearchOutcome, crate::ingest::StoreLexicalError>
4602 {
4603 self.search_lexical_structured_filtered(query, k, snippet_bytes, control, None)
4604 }
4605
4606 #[allow(clippy::too_many_arguments)]
4608 pub fn search_lexical_structured_filtered(
4609 &self,
4610 query: &crate::fts::query::LexicalQuery,
4611 k: usize,
4612 snippet_bytes: usize,
4613 control: QueryControl,
4614 filter: Option<&QueryFilter>,
4615 ) -> Result<crate::ingest::StoreStructuredLexicalSearchOutcome, crate::ingest::StoreLexicalError>
4616 {
4617 let control = control.with_clock(Arc::clone(&self.clock));
4618 let started = self.clock.now();
4619 let admission_started = timing_start(self.clock.as_ref());
4620 let AdmittedLexicalQuery {
4621 generation,
4622 active,
4623 snapshot,
4624 active_query,
4625 } = self.admit_lexical_query()?;
4626 let admission_time = timing_elapsed(self.clock.as_ref(), admission_started);
4627 let lexical_started = timing_start(self.clock.as_ref());
4628
4629 let lease = SnapshotLease::new_at(Arc::clone(&snapshot), generation);
4630 let cancellation = QueryCancellation::new(&control, &lease);
4631 cancellation.check_graph().map_err(QueryError::Scan)?;
4632 let assembly = assemble_lexical_index(
4633 LexicalInputs {
4634 filter,
4635 generation,
4636 cache: &self.lexical_index_cache,
4637 snapshot: &snapshot,
4638 active: &active,
4639 accounting: &self.accounting,
4640 },
4641 true,
4642 Some(&cancellation),
4643 )
4644 .map_err(map_store_lexical_assembly_error)?;
4645 let index = &assembly.index;
4646 let sources = &assembly.sources;
4647 let mut expansions = assembly
4648 .expand(query, &self.accounting, &cancellation)
4649 .map_err(|error| match error {
4650 expansion::ExpansionError::Query(error) => {
4651 crate::ingest::StoreLexicalError::Query(error)
4652 }
4653 expansion::ExpansionError::Index(error) => crate::ingest::StoreLexicalError::from(
4654 crate::planner::LexicalFilterError::from(error),
4655 ),
4656 expansion::ExpansionError::Shape(error) => {
4657 crate::ingest::StoreLexicalError::Structured(error)
4658 }
4659 })?;
4660 let allow_lists = assembly
4661 .alive_sets
4662 .iter()
4663 .map(|alive| alive.alive_bitmap())
4664 .collect::<Vec<_>>();
4665 let mut prepared_memory =
4666 stats::AccountedCounter::new(&self.accounting, stats::AllocationComponent::Temporary)
4667 .map_err(QueryError::Store)?;
4668 let mut work = crate::fts::control::WorkCheck::new(|| cancellation.check_graph());
4669 let fields = query
4670 .fields_controlled(&mut work)
4671 .map_err(QueryError::Scan)?;
4672 prepared_memory
4673 .set(
4674 crate::fts::query::PreparedWeightedQuery::allocation_bytes_controlled(
4675 &expansions,
4676 &fields,
4677 &mut work,
4678 )
4679 .map_err(QueryError::Scan)?
4680 .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?,
4681 )
4682 .map_err(QueryError::Store)?;
4683 let mut prepared = if index.segments().is_empty() || expansions.is_empty() {
4684 None
4685 } else {
4686 Some(
4687 crate::fts::query::PreparedWeightedQuery::new_controlled(
4688 index,
4689 std::mem::take(&mut expansions),
4690 fields,
4691 || cancellation.check_graph(),
4692 )
4693 .map_err(|error| match error {
4694 crate::fts::search::ControlledSearchError::Index(error) => {
4695 crate::ingest::StoreLexicalError::from(
4696 crate::planner::LexicalFilterError::from(error),
4697 )
4698 }
4699 crate::fts::search::ControlledSearchError::Control(error) => {
4700 crate::ingest::StoreLexicalError::from(QueryError::Scan(error))
4701 }
4702 })?,
4703 )
4704 };
4705 let mut scratch_memory =
4706 stats::AccountedCounter::new(&self.accounting, stats::AllocationComponent::Temporary)
4707 .map_err(QueryError::Store)?;
4708 let mut phrase = prepared_lexical::PhraseEligibility::new(
4709 index,
4710 Some(query),
4711 &self.accounting,
4712 &cancellation,
4713 )?;
4714 let result = match &prepared {
4715 None => crate::fts::prune::weighted::WeightedResult::default(),
4716 Some(prepared) => crate::fts::prune::weighted::search(
4717 index,
4718 prepared,
4719 k,
4720 &allow_lists,
4721 || {
4722 cancellation
4723 .check_graph()
4724 .map_err(QueryError::Scan)
4725 .map_err(crate::ingest::StoreLexicalError::from)
4726 },
4727 |doc| {
4728 phrase
4729 .matches(doc, &cancellation)
4730 .map_err(crate::ingest::StoreLexicalError::from)
4731 },
4732 |bytes| {
4733 prepared_lexical::reserve_weighted_scratch(&mut scratch_memory, bytes)
4734 .map_err(crate::ingest::StoreLexicalError::from)
4735 },
4736 )
4737 .map_err(|error| match error {
4738 crate::fts::search::ControlledSearchError::Index(error) => {
4739 crate::ingest::StoreLexicalError::from(
4740 crate::planner::LexicalFilterError::from(error),
4741 )
4742 }
4743 crate::fts::search::ControlledSearchError::Control(error) => error,
4744 })?,
4745 };
4746 let mut counters = result.counters;
4747 let scored = result.hits;
4748 let mut provenance_memory =
4749 stats::AccountedCounter::new(&self.accounting, stats::AllocationComponent::Temporary)
4750 .map_err(QueryError::Store)?;
4751 let mut candidates = Vec::with_capacity(scored.len());
4752 let mut provenance_work = crate::fts::control::WorkCheck::new(|| {
4753 cancellation.check_graph().map_err(QueryError::Scan)
4754 });
4755 for (doc, score) in scored {
4756 let mut provenance = Vec::new();
4757 let segment = index
4758 .segments()
4759 .get(doc.segment as usize)
4760 .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
4761 let scoring = prepared
4762 .as_ref()
4763 .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
4764 for expansion in scoring.expansions() {
4765 cancellation.check_graph().map_err(QueryError::Scan)?;
4766 let fields = &scoring.scoring().query().fields;
4767 prepared_lexical::reserve_weighted_scratch(
4768 &mut provenance_memory,
4769 crate::fts::sealed::TermStream::allocation_bytes(
4770 segment,
4771 &expansion.term,
4772 fields,
4773 ),
4774 )?;
4775 if let Some(mut stream) = crate::fts::sealed::TermStream::open_sized_controlled(
4776 segment,
4777 &expansion.term,
4778 fields,
4779 &mut provenance_work,
4780 )? {
4781 stream.seek_controlled(doc.row, &mut provenance_work)?;
4782 if stream.current_row() == Some(doc.row) {
4783 provenance.push(expansion.clone());
4784 counters.postings_decoded = counters.postings_decoded.saturating_add(1);
4785 }
4786 counters.blocks_decoded = counters
4787 .blocks_decoded
4788 .saturating_add(stream.blocks_decoded());
4789 counters.blocks_skipped = counters
4790 .blocks_skipped
4791 .saturating_add(stream.blocks_skipped());
4792 }
4793 provenance_memory.set(0).map_err(QueryError::Store)?;
4794 }
4795 cancellation.check_graph().map_err(QueryError::Scan)?;
4796 let (text, document) = structured_lexical_row(&snapshot, &active, sources, doc)?;
4797 let terms = provenance
4798 .iter()
4799 .map(|entry| entry.term.clone())
4800 .collect::<Vec<_>>();
4801 let snippet = crate::fts::snippet::best_window(
4802 &self.tokenizer,
4803 text,
4804 &terms,
4805 snippet_bytes,
4806 true,
4807 )?
4808 .ok_or(crate::ingest::StoreLexicalError::MissingSnippetMatch {
4809 segment: doc.segment,
4810 row: doc.row,
4811 })?;
4812 let snippet_text = snippet
4813 .text(text)
4814 .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
4815 candidates.push(crate::ingest::ExplainedLexicalCandidate {
4816 document,
4817 score,
4818 provenance,
4819 snippet: crate::fts::query::OwnedLexicalSnippet {
4820 text: snippet_text.to_owned(),
4821 source: snippet.window,
4822 highlights: snippet.highlights,
4823 source_len: text.len(),
4824 },
4825 });
4826 }
4827 cancellation.check_graph().map_err(QueryError::Scan)?;
4828 let mut diagnostics =
4829 crate::diag::QueryDiagnostics::lexical(crate::diag::LexicalDiagnostics {
4830 snapshot_generation: generation,
4831 indexed_through_seq: active
4832 .indexed_through_seq()
4833 .max(crate::wal::LogSeq::new(snapshot.absorbed_through())),
4834 requested_k: k,
4835 returned: candidates.len(),
4836 counters,
4837 elapsed: self.clock.now().saturating_duration_since(started),
4838 });
4839 diagnostics.counters.lexical_cache_hits = usize::from(assembly.cache_hit);
4840 diagnostics.counters.lexical_cache_builds = usize::from(!assembly.cache_hit);
4841 diagnostics.timings =
4842 crate::diag::QUERY_TIMING_ENABLED.then(|| crate::diag::QueryTimings {
4843 admission: admission_time,
4844 lexical: timing_elapsed(self.clock.as_ref(), lexical_started),
4845 ..Default::default()
4846 });
4847 drop(active_query);
4848 Ok(crate::ingest::StoreStructuredLexicalSearchOutcome {
4849 candidates,
4850 expansions: prepared
4851 .as_mut()
4852 .map_or(expansions, |query| query.take_expansions()),
4853 generation,
4854 diagnostics,
4855 })
4856 }
4857
4858 pub fn search_hybrid(
4861 &self,
4862 vector_query: crate::ingest::SearchRequest<'_>,
4863 lexical_query: &crate::fts::search::TermQuery,
4864 hybrid_query: &crate::fusion::HybridQuery,
4865 options: impl Into<SearchOptions>,
4866 control: QueryControl,
4867 ) -> Result<crate::ingest::StoreHybridSearchOutcome, crate::fusion::FusionError> {
4868 self.search_hybrid_inner(
4869 vector_query,
4870 PinnedLexicalQuery::Term(lexical_query),
4871 hybrid_query,
4872 options.into(),
4873 control,
4874 )
4875 }
4876
4877 pub fn search_hybrid_structured(
4880 &self,
4881 vector_query: crate::ingest::SearchRequest<'_>,
4882 lexical_query: &crate::fts::query::LexicalQuery,
4883 hybrid_query: &crate::fusion::HybridQuery,
4884 options: impl Into<SearchOptions>,
4885 control: QueryControl,
4886 ) -> Result<crate::ingest::StoreHybridSearchOutcome, crate::fusion::FusionError> {
4887 self.search_hybrid_inner(
4888 vector_query,
4889 PinnedLexicalQuery::Structured(lexical_query),
4890 hybrid_query,
4891 options.into(),
4892 control,
4893 )
4894 }
4895
4896 fn search_hybrid_inner(
4897 &self,
4898 vector_query: crate::ingest::SearchRequest<'_>,
4899 lexical_query: PinnedLexicalQuery<'_>,
4900 hybrid_query: &crate::fusion::HybridQuery,
4901 options: SearchOptions,
4902 control: QueryControl,
4903 ) -> Result<crate::ingest::StoreHybridSearchOutcome, crate::fusion::FusionError> {
4904 self.search_hybrid_then(
4905 vector_query,
4906 lexical_query,
4907 hybrid_query,
4908 options,
4909 control,
4910 false,
4911 |outcome, _, _, _, _| Ok(outcome),
4912 )
4913 }
4914
4915 #[allow(clippy::too_many_arguments)] fn search_hybrid_then<R>(
4917 &self,
4918 vector_query: crate::ingest::SearchRequest<'_>,
4919 lexical_query: PinnedLexicalQuery<'_>,
4920 hybrid_query: &crate::fusion::HybridQuery,
4921 options: SearchOptions,
4922 control: QueryControl,
4923 capture_rows: bool,
4924 finish: impl FnOnce(
4925 crate::ingest::StoreHybridSearchOutcome,
4926 &PublishedSnapshot,
4927 &crate::ingest::ActiveSegment,
4928 &QueryCancellation<'_>,
4929 Option<&materialize::HybridAddresses>,
4930 ) -> Result<R, crate::fusion::FusionError>,
4931 ) -> Result<R, crate::fusion::FusionError> {
4932 self.search_hybrid_prepared_then(
4933 || Ok::<_, std::convert::Infallible>(vector_query),
4934 vector_query.filter,
4935 vector_query.eligible,
4936 lexical_query,
4937 hybrid_query,
4938 options,
4939 control,
4940 capture_rows,
4941 None,
4942 finish,
4943 )
4944 .map_err(|error| match error {
4945 HybridPreparationError::Preparation(never) => match never {},
4946 HybridPreparationError::Search(error) => error,
4947 })
4948 }
4949
4950 #[allow(clippy::too_many_arguments)] fn search_hybrid_prepared_then<'vector, E, R>(
4952 &self,
4953 prepare_vector: impl FnOnce() -> Result<crate::ingest::SearchRequest<'vector>, E>,
4954 filter: Option<&QueryFilter>,
4955 known_eligible: Option<&[crate::ingest::DocId]>,
4956 lexical_query: PinnedLexicalQuery<'_>,
4957 hybrid_query: &crate::fusion::HybridQuery,
4958 options: SearchOptions,
4959 control: QueryControl,
4960 capture_rows: bool,
4961 pinned: Option<AdmittedVectorSearch<'_>>,
4962 finish: impl FnOnce(
4963 crate::ingest::StoreHybridSearchOutcome,
4964 &PublishedSnapshot,
4965 &crate::ingest::ActiveSegment,
4966 &QueryCancellation<'_>,
4967 Option<&materialize::HybridAddresses>,
4968 ) -> Result<R, crate::fusion::FusionError>,
4969 ) -> Result<R, HybridPreparationError<E>> {
4970 let control = if pinned.is_some() {
4971 control
4972 } else {
4973 control.with_clock(Arc::clone(&self.clock))
4974 };
4975 let started = self.clock.now();
4976 let eligible_filter = known_eligible.map(|ids| {
4979 filter.cloned().map_or_else(
4980 || QueryFilter::eligible(&self.schema, ids),
4981 |filter| filter.with_eligible(ids),
4982 )
4983 });
4984 let filter = eligible_filter.as_ref().or(filter);
4985 let admission_started = timing_start(self.clock.as_ref());
4986 let mut options = options;
4987 let requested_tier = options.explicit_tier();
4988 let pinned_epoch = pinned
5004 .as_ref()
5005 .map(|admitted| admitted.snapshot.epoch_alias());
5006 let admitted = match pinned {
5007 Some(admitted) => admitted,
5008 None => self
5009 .admit_vector_search_for(options, requested_tier.is_none())
5010 .map_err(crate::fusion::FusionError::from)?,
5011 };
5012 options = admitted.execution_options;
5013 let tier_resolution = if requested_tier.is_none() {
5014 if snapshot_has_graph(&admitted.snapshot) {
5015 Some(crate::diag::HybridTierResolution::Auto)
5016 } else {
5017 options = options.with_tier(SearchTier::Exact);
5018 Some(crate::diag::HybridTierResolution::Exact)
5019 }
5020 } else {
5021 None
5022 };
5023 let corpus_rows = hybrid::corpus_rows(&admitted.snapshot, &admitted.active_segment)?;
5024 let mut width = if hybrid_query.max_rounds == 0 {
5029 corpus_rows
5030 } else {
5031 hybrid::hybrid_window(hybrid_query.k, corpus_rows)?.width
5032 };
5033 let panic_vector = self.consume_hybrid_test_fault(crate::fusion::FusionLeg::Vector);
5034 let panic_lexical = self.consume_hybrid_test_fault(crate::fusion::FusionLeg::Lexical);
5035 let lexical_worker = self.ensure_lexical_worker()?;
5036 let mut timings = crate::diag::QueryTimings {
5037 admission: timing_elapsed(self.clock.as_ref(), admission_started),
5038 ..Default::default()
5039 };
5040 let mut lexical_inputs = LexicalInputs {
5041 filter,
5042 generation: admitted.generation,
5043 cache: &self.lexical_index_cache,
5044 snapshot: &admitted.snapshot,
5045 active: &admitted.active_segment,
5046 accounting: &self.accounting,
5047 };
5048 #[cfg(any(test, feature = "test-seams"))]
5049 let preparation_observer = crate::fts::preparation_observer::current();
5050 let mut lexical_preparation = prepared_lexical::PreparedLexicalQuery::new(lexical_query);
5051 #[cfg(any(test, feature = "test-seams"))]
5052 let fresh_preparation = hybrid::fresh_round_test_control();
5053 let run_lexical_leg = |lexical_inputs: LexicalInputs<'_>,
5054 preparation: &mut prepared_lexical::PreparedLexicalQuery<'_>,
5055 bound,
5056 queued| {
5057 #[cfg(any(test, feature = "test-seams"))]
5058 if fresh_preparation {
5059 preparation.reset_for_test();
5060 }
5061 #[cfg(any(test, feature = "test-seams"))]
5062 let _preparation_scope =
5063 crate::fts::preparation_observer::enter(preparation_observer.clone());
5064 let queue_time = timing_elapsed(self.clock.as_ref(), queued);
5065 let lexical_started = timing_start(self.clock.as_ref());
5066 let name = std::thread::current().name().map(str::to_owned);
5067 let result = (|| {
5068 maybe_trigger_hybrid_leg_panic(panic_lexical, "injected lexical hybrid leg panic");
5069 let lease =
5070 SnapshotLease::new_at(Arc::clone(&admitted.snapshot), admitted.generation);
5071 let cancellation = QueryCancellation::new(&control, &lease);
5072 cancellation
5073 .check_graph()
5074 .map_err(QueryError::Scan)
5075 .map_err(crate::fusion::FusionError::from)?;
5076 match lexical_query {
5077 PinnedLexicalQuery::Term(_) => {
5078 exact_lexical_leg(lexical_inputs, bound, &cancellation, preparation)
5079 }
5080 PinnedLexicalQuery::Structured(query) => exact_structured_lexical_leg(
5081 lexical_inputs,
5082 &self.tokenizer,
5083 query,
5084 bound,
5085 &cancellation,
5086 preparation,
5087 ),
5088 }
5089 })();
5090 (
5091 name,
5092 result,
5093 queue_time,
5094 timing_elapsed(self.clock.as_ref(), lexical_started),
5095 )
5096 };
5097 let caller_chose_tier = requested_tier.is_some();
5098 let run_vector_leg = |vector_query: crate::ingest::SearchRequest<'_>,
5099 vector_preparation: &mut prepared::PreparedVectorQuery<'_>,
5100 k: usize| {
5101 let graph_available = snapshot_has_graph(&admitted.snapshot);
5102 let scan_reason_override = if caller_chose_tier || !graph_available {
5103 None
5104 } else if k > corpus_rows {
5105 Some(crate::planner::ScanReason::FullMaterialization)
5106 } else if graph_round_is_worth_it(k, corpus_rows) {
5107 None
5108 } else {
5109 Some(crate::planner::ScanReason::WideningCap {
5110 ef: graph_round_ef(k),
5111 rows: corpus_rows,
5112 })
5113 };
5114 search_pinned(
5115 admitted.pool.as_deref(),
5116 &admitted.snapshot,
5117 &admitted.active_segment,
5118 &self.accounting,
5119 admitted.generation,
5120 pinned_epoch.unwrap_or_else(|| self.epoch_identity()),
5121 vector_query,
5122 k,
5123 if caller_chose_tier || graph_round_is_worth_it(k, corpus_rows) {
5135 options
5136 } else {
5137 options.with_tier(SearchTier::Exact)
5138 },
5139 control.clone(),
5140 GraphBoundMode::Shared,
5141 requested_tier,
5142 scan_reason_override,
5143 started,
5144 self.clock.as_ref(),
5145 vector_preparation,
5146 #[cfg(any(test, feature = "test-seams"))]
5147 self.vector_fault_controller.as_ref(),
5148 )
5149 .map_err(crate::fusion::FusionError::from)
5150 };
5151 let lexical_queued = timing_start(self.clock.as_ref());
5152 let (prepared_vector_result, lexical_result) = lexical_worker.run_scoped(
5153 || {
5154 run_lexical_leg(
5155 lexical_inputs,
5156 &mut lexical_preparation,
5157 width.saturating_add(1),
5158 lexical_queued,
5159 )
5160 },
5161 || {
5162 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5163 let vector_query =
5164 prepare_vector().map_err(HybridPreparationError::Preparation)?;
5165 if vector_query.filter.is_some() && filter.is_none() {
5166 return Err(HybridPreparationError::Search(
5167 QueryError::Store(StoreError::InvalidScan {
5168 detail:
5169 "deferred vector preparation cannot introduce a query filter"
5170 .to_owned(),
5171 })
5172 .into(),
5173 ));
5174 }
5175 let deferred_filter = if known_eligible.is_none() {
5176 vector_query.eligible.map(|ids| {
5177 filter.cloned().map_or_else(
5178 || QueryFilter::eligible(&self.schema, ids),
5179 |filter| filter.with_eligible(ids),
5180 )
5181 })
5182 } else {
5183 None
5184 };
5185 let effective_vector_query = vector_query
5186 .with_filter(deferred_filter.as_ref().or(filter).or(vector_query.filter));
5187 let mut vector_preparation = prepared::PreparedVectorQuery::new(
5188 vector_query.vector(),
5189 pinned_epoch.unwrap_or_else(|| self.epoch_identity()),
5190 &self.accounting,
5191 );
5192 maybe_trigger_hybrid_leg_panic(
5193 panic_vector,
5194 "injected vector hybrid leg panic",
5195 );
5196 let result = run_vector_leg(
5197 effective_vector_query,
5198 &mut vector_preparation,
5199 width.saturating_add(1),
5200 );
5201 Ok((vector_query, deferred_filter, vector_preparation, result))
5202 }))
5203 .unwrap_or(Err(HybridPreparationError::Search(
5204 crate::fusion::FusionError::LegPanic {
5205 leg: crate::fusion::FusionLeg::Vector,
5206 detail: "vector hybrid leg panicked",
5207 },
5208 )))
5209 },
5210 )?;
5211 let (lexical_thread_name, lexical_result, queue_time, lexical_time) = lexical_result
5212 .unwrap_or_else(|error| {
5213 (
5214 Some("zeppelin-fts".to_owned()),
5215 Err(error),
5216 std::time::Duration::ZERO,
5217 std::time::Duration::ZERO,
5218 )
5219 });
5220 #[cfg(any(test, feature = "test-seams"))]
5221 if let Ok(mut receipt) = self.hybrid_execution_receipt.lock() {
5222 *receipt = Some(HybridExecutionReceipt {
5223 vector_thread: std::thread::current().id(),
5224 lexical_thread_name,
5225 vector_completed: true,
5226 lexical_completed: true,
5227 });
5228 }
5229 #[cfg(not(any(test, feature = "test-seams")))]
5230 let _ = lexical_thread_name;
5231 let (vector_query, deferred_filter, mut vector_preparation, vector_result) =
5235 match prepared_vector_result {
5236 Ok(prepared) => prepared,
5237 Err(error) => {
5238 let lease =
5239 SnapshotLease::new_at(Arc::clone(&admitted.snapshot), admitted.generation);
5240 QueryCancellation::new(&control, &lease)
5241 .check_graph()
5242 .map_err(map_scan_error)?;
5243 if let Err(lexical_error) = lexical_result
5244 && matches!(
5245 lexical_error,
5246 crate::fusion::FusionError::ReadCancelled { .. }
5247 | crate::fusion::FusionError::Timeout { .. }
5248 | crate::fusion::FusionError::Cancelled { .. }
5249 )
5250 {
5251 return Err(HybridPreparationError::Search(lexical_error));
5252 }
5253 return Err(error);
5254 }
5255 };
5256 let vector_query =
5257 vector_query.with_filter(deferred_filter.as_ref().or(filter).or(vector_query.filter));
5258 let (
5259 mut vector_outcome,
5260 (mut lexical_hits, mut lexical_assembly, mut lexical_counters, mut lexical_cache_hit),
5261 ) = resolve_hybrid_leg_results(vector_result, lexical_result)?;
5262
5263 timings.lexical_queue = queue_time;
5264 timings.lexical = lexical_time;
5265 let mut deferred_initial_cache_hit = None;
5266 if let Some(filter) = deferred_filter.as_ref() {
5267 lexical_inputs.filter = Some(filter);
5271 lexical_preparation = prepared_lexical::PreparedLexicalQuery::new(lexical_query);
5272 let queued = timing_start(self.clock.as_ref());
5273 let (_, reranked) = lexical_worker.run_scoped(
5274 || {
5275 run_lexical_leg(
5276 lexical_inputs,
5277 &mut lexical_preparation,
5278 width.saturating_add(1),
5279 queued,
5280 )
5281 },
5282 || (),
5283 )?;
5284 let (_, reranked, queue_time, lexical_time) = reranked?;
5285 let (hits, assembly, mut counters, cache_hit) = reranked?;
5286 accumulate_search_counters(&mut counters, &lexical_counters);
5287 deferred_initial_cache_hit = Some(lexical_cache_hit);
5288 lexical_hits = hits;
5289 lexical_assembly = assembly;
5290 lexical_counters = counters;
5291 lexical_cache_hit = cache_hit;
5292 timings.lexical_queue += queue_time;
5293 timings.lexical += lexical_time;
5294 }
5295 let anchors = hybrid::HybridAnchors {
5296 vector_ceiling: vector_outcome.vector_ceiling,
5297 lexical_maximum: lexical_hits.first().map_or(0.0, |hit| hit.bm25),
5298 };
5299 let fusion_lease =
5300 SnapshotLease::new_at(Arc::clone(&admitted.snapshot), admitted.generation);
5301 let fusion_cancellation = QueryCancellation::new(&control, &fusion_lease);
5302 let mut work = hybrid::HybridWork::new();
5303 if let Some(cache_hit) = deferred_initial_cache_hit {
5304 work.lexical_cache_hits += usize::from(cache_hit);
5305 work.lexical_cache_builds += usize::from(!cache_hit);
5306 }
5307 let mut cross_scores = hybrid::CrossScoreCache::new(vector_outcome.epoch);
5308 let mut round_buffers = hybrid::HybridRoundBuffers::new();
5309 let mut fusion_scratch =
5310 crate::fusion::StoreFusionScratch::new(Arc::clone(&self.accounting));
5311 let mut rescore_counters = crate::diag::ScanRescoreCounters::default();
5312 let mut rounds = 1_usize;
5313 let mut budget_exhausted = false;
5314 let (fused, report_window, addresses) = loop {
5315 if let Some(vector_timings) = vector_outcome.diagnostics.timings {
5316 timings.vector += vector_timings.vector;
5317 }
5318 let fusion_started = timing_start(self.clock.as_ref());
5319 let round = hybrid::build_round(
5320 &admitted.snapshot,
5321 &admitted.active_segment,
5322 &lexical_assembly,
5323 &lexical_preparation,
5324 &self.tokenizer,
5325 lexical_query,
5326 vector_query.vector(),
5327 &vector_outcome.candidates,
5328 anchors,
5329 &lexical_hits,
5330 width,
5331 &fusion_cancellation,
5332 capture_rows,
5333 &self.accounting,
5334 &mut cross_scores,
5335 &mut round_buffers,
5336 )?;
5337 let cross_fill_dims = u64::try_from(round.vector_scores_computed)
5341 .ok()
5342 .and_then(|rows| rows.checked_mul(vector_query.vector().len() as u64))
5343 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
5344 let cross_fill_bytes = cross_fill_dims
5345 .checked_mul(4)
5346 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
5347 vector_outcome.stats.dims_touched = vector_outcome
5348 .stats
5349 .dims_touched
5350 .checked_add(cross_fill_dims)
5351 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
5352 vector_outcome.stats.bytes_read = vector_outcome
5353 .stats
5354 .bytes_read
5355 .checked_add(cross_fill_bytes)
5356 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
5357 if cross_fill_dims != 0 {
5358 let caller = std::thread::current().id();
5359 if !vector_outcome.stats.worker_thread_ids.contains(&caller) {
5360 vector_outcome.stats.worker_thread_ids.push(caller);
5361 }
5362 vector_outcome.stats.threads_used = vector_outcome.stats.worker_thread_ids.len();
5363 }
5364 accumulate_search_counters(&mut lexical_counters, &round.lexical_counters);
5365 if let Some(counters) = vector_outcome.diagnostics.scan_rescore {
5366 rescored_scan::accumulate(&mut rescore_counters, counters)?;
5367 }
5368 work.complete_round(
5369 &vector_outcome.stats,
5370 vector_outcome.graph_stats,
5371 lexical_counters,
5372 )?;
5373 work.lexical_cache_hits += usize::from(lexical_cache_hit);
5374 work.lexical_cache_builds += usize::from(!lexical_cache_hit);
5375 work.plans.append(&mut vector_outcome.diagnostics.plan);
5376 work.cross_filled_vector = work
5377 .cross_filled_vector
5378 .checked_add(round.vector_scores_computed)
5379 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
5380 work.cross_filled_lexical = work
5381 .cross_filled_lexical
5382 .checked_add(round.lexical_scores_computed)
5383 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
5384 work.vector_candidates_produced = work
5385 .vector_candidates_produced
5386 .checked_add(vector_outcome.candidates.len())
5387 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
5388 work.lexical_candidates_produced = work
5389 .lexical_candidates_produced
5390 .checked_add(lexical_hits.len())
5391 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
5392 let provenance = hybrid::round_provenance(&vector_outcome, lexical_hits.len(), width);
5393 let fused = hybrid::fuse_round(
5394 hybrid_query,
5395 &round,
5396 provenance,
5397 requested_tier == Some(SearchTier::Exact),
5398 &mut fusion_scratch,
5399 )?;
5400 timings.fusion_cross_fill += timing_elapsed(self.clock.as_ref(), fusion_started);
5401 if fused.report.termination != crate::fusion::FusionTermination::WindowUnproven
5402 || width >= corpus_rows
5403 {
5404 break (
5405 fused,
5406 crate::diag::HybridReport {
5407 normalization_policy_version:
5408 crate::fusion::HYBRID_NORMALIZATION_POLICY_VERSION,
5409 lexical_full_materializations: 0,
5410 provenance,
5411 window: width,
5412 vector_returned: round.vector.len(),
5413 lexical_returned: round.lexical.len(),
5414 cross_filled_vector: round.cross_filled_vector,
5415 cross_filled_lexical: round.cross_filled_lexical,
5416 total_cross_filled_vector: work.cross_filled_vector,
5417 total_cross_filled_lexical: work.cross_filled_lexical,
5418 vector_candidates_produced: work.vector_candidates_produced,
5419 lexical_candidates_produced: work.lexical_candidates_produced,
5420 },
5421 round.addresses,
5422 );
5423 }
5424 width = if rounds >= hybrid_query.max_rounds {
5425 budget_exhausted = true;
5426 corpus_rows
5427 } else {
5428 width.saturating_mul(2).min(corpus_rows)
5429 };
5430 rounds = rounds.saturating_add(1);
5431 let lexical_queued = timing_start(self.clock.as_ref());
5432 let (vector_result, lexical_result) = lexical_worker.run_scoped(
5433 || {
5434 run_lexical_leg(
5435 lexical_inputs,
5436 &mut lexical_preparation,
5437 width.saturating_add(1),
5438 lexical_queued,
5439 )
5440 },
5441 || {
5442 run_vector_leg(
5443 vector_query,
5444 &mut vector_preparation,
5445 width.saturating_add(1),
5446 )
5447 },
5448 )?;
5449 let (_, lexical_result, queue_time, lexical_time) =
5450 lexical_result.unwrap_or_else(|error| {
5451 (
5452 Some("zeppelin-fts".to_owned()),
5453 Err(error),
5454 std::time::Duration::ZERO,
5455 std::time::Duration::ZERO,
5456 )
5457 });
5458 let (next_vector, (next_hits, next_sources, next_counters, next_cache_hit)) =
5459 resolve_hybrid_leg_results(vector_result, lexical_result)?;
5460 timings.lexical_queue += queue_time;
5461 timings.lexical += lexical_time;
5462 vector_outcome = next_vector;
5463 lexical_hits = next_hits;
5464 lexical_assembly = next_sources;
5465 lexical_counters = next_counters;
5466 lexical_cache_hit = next_cache_hit;
5467 };
5468
5469 let crate::ingest::SearchOutcome {
5470 candidates: _,
5471 vector_ceiling: _,
5472 stats: _,
5473 graph_stats: _,
5474 generation,
5475 epoch,
5476 diagnostics: vector_diagnostics,
5477 } = vector_outcome;
5478 let mut report = fused.report;
5479 report.rounds = rounds;
5480 if budget_exhausted {
5481 report.budget_exhausted = true;
5482 report.termination = crate::fusion::FusionTermination::BudgetFullMaterialization;
5483 }
5484 let mut diagnostics =
5485 crate::diag::QueryDiagnostics::hybrid(crate::diag::HybridDiagnostics {
5486 snapshot_generation: vector_diagnostics.snapshot_generation,
5487 indexed_through_seq: vector_diagnostics.indexed_through_seq,
5488 plan: work.plans,
5489 approximate: vector_diagnostics.approximate
5490 || report.termination
5491 == crate::fusion::FusionTermination::ApproximateCandidates,
5492 exact_rescore: vector_diagnostics.exact_rescore,
5493 requested_k: hybrid_query.k,
5494 returned: fused.hits.len(),
5495 scan: work.scan,
5496 graph: work.graph,
5497 lexical: work.lexical,
5498 report,
5499 hybrid: report_window,
5500 tier_resolution,
5501 epoch,
5502 elapsed: self.clock.now().saturating_duration_since(started),
5503 });
5504 diagnostics.counters.lexical_cache_hits = work.lexical_cache_hits;
5505 diagnostics.scan_rescore = options.scan_rescore().map(|_| rescore_counters);
5506 diagnostics.counters.lexical_cache_builds = work.lexical_cache_builds;
5507 diagnostics.timings = crate::diag::QUERY_TIMING_ENABLED.then_some(timings);
5508 let outcome = crate::ingest::StoreHybridSearchOutcome {
5509 hits: fused.hits,
5510 lexical_expansions: lexical_preparation.take_expansions()?,
5511 generation,
5512 diagnostics,
5513 };
5514 finish(
5515 outcome,
5516 &admitted.snapshot,
5517 &admitted.active_segment,
5518 &fusion_cancellation,
5519 addresses.as_ref(),
5520 )
5521 .map_err(HybridPreparationError::Search)
5522 }
5523
5524 #[cfg(any(test, feature = "test-seams"))]
5530 #[must_use]
5531 pub fn lexical_index_cache_counters(&self) -> (u64, u64) {
5532 (
5533 self.lexical_index_cache.hits.load(Ordering::Relaxed),
5534 self.lexical_index_cache.builds.load(Ordering::Relaxed),
5535 )
5536 }
5537
5538 #[cfg(any(test, feature = "test-seams"))]
5540 #[must_use]
5541 pub fn lexical_contribution_cache_counters(&self) -> (u64, u64) {
5542 (
5543 self.lexical_index_cache
5544 .contribution_hits
5545 .load(Ordering::Relaxed),
5546 self.lexical_index_cache
5547 .contribution_builds
5548 .load(Ordering::Relaxed),
5549 )
5550 }
5551
5552 fn consume_hybrid_test_fault(&self, leg: crate::fusion::FusionLeg) -> bool {
5553 #[cfg(any(test, feature = "test-seams"))]
5554 {
5555 let Ok(mut fault) = self.hybrid_leg_fault.lock() else {
5556 return false;
5557 };
5558 if matches!(*fault, Some(HybridLegTestFault::Panic(candidate)) if candidate == leg) {
5559 *fault = None;
5560 return true;
5561 }
5562 }
5563 #[cfg(not(any(test, feature = "test-seams")))]
5564 let _ = leg;
5565 false
5566 }
5567
5568 fn search_with_graph_bound_mode(
5569 &self,
5570 request: crate::ingest::SearchRequest<'_>,
5571 k: usize,
5572 options: SearchOptions,
5573 control: QueryControl,
5574 graph_bound_mode: GraphBoundMode,
5575 ) -> Result<crate::ingest::SearchOutcome, QueryError> {
5576 self.search_with_graph_bound_mode_then(
5577 request,
5578 k,
5579 options,
5580 control,
5581 graph_bound_mode,
5582 |outcome, _, _| Ok(outcome),
5583 )
5584 }
5585
5586 fn search_with_graph_bound_mode_then<R>(
5587 &self,
5588 request: crate::ingest::SearchRequest<'_>,
5589 k: usize,
5590 options: SearchOptions,
5591 control: QueryControl,
5592 graph_bound_mode: GraphBoundMode,
5593 finish: impl FnOnce(
5594 crate::ingest::SearchOutcome,
5595 &AdmittedVectorSearch<'_>,
5596 &QueryControl,
5597 ) -> Result<R, QueryError>,
5598 ) -> Result<R, QueryError> {
5599 let eligible_filter = request.eligible.map(|ids| {
5600 request.filter.cloned().map_or_else(
5601 || QueryFilter::eligible(&self.schema, ids),
5602 |filter| filter.with_eligible(ids),
5603 )
5604 });
5605 let request = request.with_filter(eligible_filter.as_ref().or(request.filter));
5606 let control = control.with_clock(Arc::clone(&self.clock));
5607 let started = self.clock.now();
5608 let admission_started = timing_start(self.clock.as_ref());
5609 let admitted = self.admit_vector_search(options)?;
5610 let admission_time = timing_elapsed(self.clock.as_ref(), admission_started);
5611 let mut vector_preparation = prepared::PreparedVectorQuery::new(
5612 request.vector(),
5613 self.epoch_identity(),
5614 &self.accounting,
5615 );
5616 let score = || {
5617 let mut outcome = search_pinned(
5618 admitted.pool.as_deref(),
5619 &admitted.snapshot,
5620 &admitted.active_segment,
5621 &self.accounting,
5622 admitted.generation,
5623 self.epoch_identity(),
5624 request,
5625 k,
5626 options,
5627 control.clone(),
5628 graph_bound_mode,
5629 options.explicit_tier(),
5630 None,
5631 started,
5632 self.clock.as_ref(),
5633 &mut vector_preparation,
5634 #[cfg(any(test, feature = "test-seams"))]
5635 self.vector_fault_controller.as_ref(),
5636 )?;
5637 if let Some(timings) = &mut outcome.diagnostics.timings {
5638 timings.admission = admission_time;
5639 }
5640 finish(outcome, &admitted, &control)
5641 };
5642 #[cfg(any(test, feature = "test-seams"))]
5643 let result = crate::kernels::vector_fault::run_store_scoring(
5644 self.kernel_fault_controller.as_ref(),
5645 score,
5646 );
5647 #[cfg(not(any(test, feature = "test-seams")))]
5648 let result = score();
5649 #[cfg(any(test, feature = "test-seams"))]
5650 {
5651 if let Some(controller) = self.vector_fault_controller.as_ref() {
5652 controller.finalize_search(result.is_ok());
5653 }
5654 }
5655 result
5656 }
5657
5658 fn ensure_query_pool(&self) -> Result<Arc<pool::QueryPool>, QueryError> {
5659 let mut pool_slot = self.query_pool.lock().map_err(|_| {
5660 QueryError::Store(StoreError::Synchronization {
5661 component: "query pool",
5662 })
5663 })?;
5664 if pool_slot.is_none() {
5665 let capacity = crate::scan::physical_thread_capacity().map_err(|error| {
5666 QueryError::Store(StoreError::QueryPoolCapacity {
5667 source: error.to_string(),
5668 })
5669 })?;
5670 let pool =
5671 pool::QueryPool::start(capacity, &self.accounting).map_err(QueryError::Store)?;
5672 *pool_slot = Some(Arc::new(pool));
5673 }
5674 let pool = pool_slot.as_ref().cloned().ok_or({
5675 QueryError::Store(StoreError::Synchronization {
5676 component: "query pool initialization",
5677 })
5678 })?;
5679 drop(pool_slot);
5680 Ok(pool)
5681 }
5682
5683 fn ensure_lexical_worker(
5684 &self,
5685 ) -> Result<Arc<pool::LexicalWorker>, crate::fusion::FusionError> {
5686 let mut worker_slot =
5687 self.lexical_worker
5688 .lock()
5689 .map_err(|_| crate::fusion::FusionError::LegThreadStart {
5690 leg: crate::fusion::FusionLeg::Lexical,
5691 detail: "pooled lexical worker synchronization was poisoned".to_owned(),
5692 })?;
5693 if worker_slot.is_none() {
5694 *worker_slot = Some(Arc::new(pool::LexicalWorker::start()?));
5695 }
5696 let worker = worker_slot.as_ref().cloned().ok_or_else(|| {
5697 crate::fusion::FusionError::LegThreadStart {
5698 leg: crate::fusion::FusionLeg::Lexical,
5699 detail: "pooled lexical worker initialization failed".to_owned(),
5700 }
5701 })?;
5702 drop(worker_slot);
5703 Ok(worker)
5704 }
5705
5706 fn admit_vector_search(
5707 &self,
5708 options: SearchOptions,
5709 ) -> Result<AdmittedVectorSearch<'_>, QueryError> {
5710 self.admit_vector_search_for(options, false)
5711 }
5712
5713 fn admit_vector_search_for(
5714 &self,
5715 options: SearchOptions,
5716 hybrid_default: bool,
5717 ) -> Result<AdmittedVectorSearch<'_>, QueryError> {
5718 #[cfg(any(test, feature = "test-seams"))]
5719 self.text_materialization_work
5720 .admissions
5721 .fetch_add(1, Ordering::Relaxed);
5722 let state = self
5723 .state
5724 .lock()
5725 .map_err(|_| QueryError::Store(StoreError::Synchronization { component: "state" }))?;
5726 match *state {
5727 StoreState::Open => {}
5728 StoreState::Closing => return Err(QueryError::Store(StoreError::Closing)),
5729 StoreState::Closed => return Err(QueryError::Store(StoreError::Closed)),
5730 }
5731 let active_guard = self.active.lock().map_err(|_| {
5732 QueryError::Store(StoreError::Synchronization {
5733 component: "active segment",
5734 })
5735 })?;
5736 let active_state = active_guard
5737 .as_ref()
5738 .ok_or(QueryError::Store(StoreError::Closed))?;
5739 let generation = active_state.generation;
5740 let active_segment = Arc::clone(&active_state.segment);
5741 let snapshot = self
5742 .snapshot
5743 .read()
5744 .map_err(|_| {
5745 QueryError::Store(StoreError::Synchronization {
5746 component: "published snapshot",
5747 })
5748 })?
5749 .as_ref()
5750 .cloned()
5751 .ok_or(QueryError::Store(StoreError::Closed))?;
5752 let execution_options = if hybrid_default && !snapshot_has_graph(&snapshot) {
5753 options.with_tier(SearchTier::Exact)
5754 } else {
5755 options
5756 };
5757 let needs_query_pool = !active_segment.is_empty()
5760 || (hybrid_default && !snapshot.segments().is_empty())
5761 || match execution_options.tier() {
5762 SearchTier::Auto => snapshot.segments().iter().any(|segment| {
5763 !segment.directory().iter().any(|entry| {
5764 entry.kind == crate::segment::layout::RegionKind::GraphNodeBlocks.id()
5765 })
5766 }),
5767 SearchTier::Exact => !snapshot.segments().is_empty(),
5768 SearchTier::Scan => true,
5769 SearchTier::Graph(_) => false,
5770 };
5771 let pool = if needs_query_pool {
5772 Some(self.ensure_query_pool()?)
5773 } else {
5774 None
5775 };
5776 self.active_queries.fetch_add(1, Ordering::Relaxed);
5777 let active_query = ActiveQuery {
5778 count: &self.active_queries,
5779 };
5780 drop(active_guard);
5781 drop(state);
5782 Ok(AdmittedVectorSearch {
5783 pool,
5784 execution_options,
5785 snapshot: snapshot::ReadSnapshot::new(snapshot),
5786 active_segment,
5787 generation,
5788 _active_query: Some(active_query),
5789 })
5790 }
5791
5792 #[cfg(test)]
5793 fn search_independent_for_test(
5794 &self,
5795 request: crate::ingest::SearchRequest<'_>,
5796 k: usize,
5797 options: SearchOptions,
5798 control: QueryControl,
5799 ) -> Result<crate::ingest::SearchOutcome, QueryError> {
5800 self.search_with_graph_bound_mode(request, k, options, control, GraphBoundMode::Independent)
5801 }
5802}
5803
5804#[derive(Clone, Copy)]
5805pub(crate) enum StructuredLexicalSource {
5806 Sealed(usize),
5807 Active,
5808}
5809
5810#[derive(Clone, Debug)]
5811struct EligibleIds {
5812 numeric: Vec<crate::ingest::DocId>,
5813 byte_order: Vec<[u8; 16]>,
5816}
5817
5818const ELIGIBLE_BYTE_ORDER_MIN_IDS: usize = 1_000;
5819
5820#[derive(Clone, Debug)]
5822pub struct QueryFilter {
5823 predicate: Option<crate::meta::Predicate>,
5824 schema: crate::meta::Schema,
5825 eligible_ids: Option<EligibleIds>,
5826}
5827
5828impl QueryFilter {
5829 #[cfg(feature = "graph-cypher")]
5832 pub(crate) fn eligible_capacity_bytes(
5833 schema: &crate::meta::Schema,
5834 count: usize,
5835 ) -> Option<usize> {
5836 let numeric = count.checked_mul(std::mem::size_of::<crate::ingest::DocId>())?;
5837 let byte_order = if count >= ELIGIBLE_BYTE_ORDER_MIN_IDS {
5838 count.checked_mul(std::mem::size_of::<[u8; 16]>())?
5839 } else {
5840 0
5841 };
5842 std::mem::size_of::<Self>()
5843 .checked_add(schema.resident_bytes()?)?
5844 .checked_add(numeric)?
5845 .checked_add(byte_order)
5846 }
5847
5848 pub fn new(
5850 schema: &crate::meta::Schema,
5851 predicate: Option<&crate::meta::Predicate>,
5852 timestamp_range: Option<(i64, i64)>,
5853 ) -> Result<Option<Self>, QueryError> {
5854 let Some(predicate) = document_scan_predicate(predicate, timestamp_range)? else {
5855 return Ok(None);
5856 };
5857 crate::planner::validate_predicate(&predicate, schema).map_err(|error| {
5858 QueryError::Store(StoreError::InvalidScan {
5859 detail: error.to_string(),
5860 })
5861 })?;
5862 Ok(Some(Self {
5863 predicate: Some(predicate),
5864 schema: schema.clone(),
5865 eligible_ids: None,
5866 }))
5867 }
5868
5869 fn active_rows(
5870 &self,
5871 active: &crate::ingest::ActiveSegment,
5872 ) -> Result<crate::meta::DocBitmap, QueryError> {
5873 let mut rows = active_scan_rows(active, &self.schema, self.predicate.as_ref())?;
5874 if let Some(ids) = &self.eligible_ids {
5875 let mut eligible = crate::meta::DocBitmap::new();
5878 for (row, id) in active.doc_ids().iter().enumerate() {
5879 if ids.numeric.binary_search(id).is_ok() {
5880 eligible.insert(
5881 u32::try_from(row)
5882 .map_err(|_| QueryError::Store(StoreError::ActiveRowOverflow))?,
5883 );
5884 }
5885 }
5886 rows.intersect_with(&eligible);
5887 }
5888 Ok(rows)
5889 }
5890
5891 #[must_use]
5894 pub fn eligible(schema: &crate::meta::Schema, ids: &[crate::ingest::DocId]) -> Self {
5895 Self {
5896 predicate: None,
5897 schema: schema.clone(),
5898 eligible_ids: None,
5899 }
5900 .with_eligible(ids)
5901 }
5902
5903 #[must_use]
5905 pub fn with_eligible(mut self, ids: &[crate::ingest::DocId]) -> Self {
5906 let mut ids = ids.to_vec();
5907 ids.sort_unstable();
5908 ids.dedup();
5909 if let Some(existing) = &self.eligible_ids {
5910 ids.retain(|id| existing.numeric.binary_search(id).is_ok());
5911 }
5912 let mut byte_order = if ids.len() >= ELIGIBLE_BYTE_ORDER_MIN_IDS {
5913 ids.iter()
5914 .map(|id| id.get().to_le_bytes())
5915 .collect::<Vec<_>>()
5916 } else {
5917 Vec::new()
5918 };
5919 byte_order.sort_unstable();
5920 self.eligible_ids = Some(EligibleIds {
5921 numeric: ids,
5922 byte_order,
5923 });
5924 self
5925 }
5926
5927 fn sealed_rows(
5928 &self,
5929 segment: &crate::segment::reader::SegmentReader,
5930 ) -> Result<crate::meta::DocBitmap, QueryError> {
5931 let mut rows = sealed_scan_rows(segment, self.predicate.as_ref())?;
5932 if let Some(ids) = &self.eligible_ids {
5933 rows.intersect_with(
5934 &segment
5935 .query_rows_for_eligible_ids(&ids.numeric, &ids.byte_order)
5936 .map_err(QueryError::Store)?,
5937 );
5938 }
5939 Ok(rows)
5940 }
5941}
5942
5943struct LexicalAssembly {
5944 index: crate::fts::index::LexicalIndex,
5945 alive_sets: Vec<Arc<crate::meta::AliveSet>>,
5946 sources: Vec<StructuredLexicalSource>,
5947 contributions: Vec<Arc<CachedLexicalContribution>>,
5948 vocabulary: Arc<Mutex<Option<Arc<CachedVocabulary>>>>,
5949 _base: Option<Arc<LexicalAssembly>>,
5951 memory: stats::AccountedCounter,
5953}
5954
5955struct CachedVocabulary {
5956 view: crate::fts::vocabulary::Vocabulary,
5957 phonetic: Mutex<Option<Arc<expansion::CachedPhonetic>>>,
5958 _memory: stats::AccountedCounter,
5960}
5961
5962impl LexicalAssembly {
5963 fn vocabulary(
5964 &self,
5965 accounting: &Arc<stats::Accounting>,
5966 cancellation: &QueryCancellation<'_>,
5967 ) -> Result<Arc<CachedVocabulary>, QueryError> {
5968 let mut cached = cancellation.cache_lock(&self.vocabulary, "lexical vocabulary cache")?;
5969 if let Some(value) = cached.as_ref() {
5970 return Ok(Arc::clone(value));
5971 }
5972 let mut memory =
5973 stats::AccountedCounter::new(accounting, stats::AllocationComponent::Cache)
5974 .map_err(QueryError::Store)?;
5975 let mut temporary =
5976 stats::AccountedCounter::new(accounting, stats::AllocationComponent::Temporary)
5977 .map_err(QueryError::Store)?;
5978 let mut work = crate::fts::control::WorkCheck::new(|| {
5979 cancellation.check_graph().map_err(QueryError::Scan)
5980 });
5981 let view = crate::fts::vocabulary::Vocabulary::build_controlled(
5982 self.index.vocabulary_terms(),
5983 |scratch, owned| {
5984 cancellation.check_graph().map_err(QueryError::Scan)?;
5985 prepared_lexical::reserve_weighted_scratch(&mut temporary, scratch)?;
5986 let bytes = owned
5987 .and_then(|bytes| bytes.checked_add(std::mem::size_of::<CachedVocabulary>()))
5988 .ok_or(QueryError::Store(StoreError::BudgetExceeded {
5989 needed: u64::MAX,
5990 budget: u64::MAX,
5991 component: "lexical vocabulary cache",
5992 }))?;
5993 memory.set(bytes).map_err(QueryError::Store)
5994 },
5995 &mut work,
5996 )?;
5997 let value = Arc::new(CachedVocabulary {
5998 view,
5999 phonetic: Mutex::new(None),
6000 _memory: memory,
6001 });
6002 *cached = Some(Arc::clone(&value));
6003 Ok(value)
6004 }
6005
6006 fn owned_bytes(&self) -> Option<usize> {
6009 std::mem::size_of::<Self>()
6010 .checked_add(self.index.resident_spine_bytes()?)?
6011 .checked_add(
6012 self.alive_sets
6013 .capacity()
6014 .checked_mul(std::mem::size_of::<Arc<crate::meta::AliveSet>>())?,
6015 )?
6016 .checked_add(
6017 self.sources
6018 .capacity()
6019 .checked_mul(std::mem::size_of::<StructuredLexicalSource>())?,
6020 )?
6021 .checked_add(
6022 self.contributions
6023 .capacity()
6024 .checked_mul(std::mem::size_of::<Arc<CachedLexicalContribution>>())?,
6025 )
6026 }
6027}
6028
6029enum LexicalContributionKey {
6030 Sealed(crate::segment::SegmentMeta, (u64, u64)),
6033 Active(std::sync::Weak<crate::ingest::ActiveSegment>),
6034}
6035
6036struct CachedLexicalContribution {
6037 key: LexicalContributionKey,
6038 statistics: Arc<crate::fts::index::LiveSegmentStatistics>,
6039 active_alive: Option<Arc<crate::meta::AliveSet>>,
6042 _memory: stats::AccountedCounter,
6043}
6044
6045impl CachedLexicalContribution {
6046 fn build(
6047 key: LexicalContributionKey,
6048 postings: &crate::fts::sealed::SealedSegment,
6049 alive: &Arc<crate::meta::AliveSet>,
6050 ordinal: usize,
6051 accounting: &Arc<stats::Accounting>,
6052 work: &mut crate::fts::control::WorkCheck<impl FnMut() -> Result<(), LexicalAssemblyError>>,
6053 ) -> Result<Arc<Self>, LexicalAssemblyError> {
6054 work.check_now()?;
6055 let active = matches!(key, LexicalContributionKey::Active(_));
6056 let alive_bytes = if active {
6057 alive
6058 .resident_bytes()
6059 .and_then(|n| n.checked_add(std::mem::size_of::<crate::meta::AliveSet>()))
6060 .and_then(|n| n.checked_add(2 * std::mem::size_of::<usize>()))
6061 } else {
6062 Some(0)
6063 };
6064 let bytes =
6065 crate::fts::index::LiveSegmentStatistics::allocation_bytes(alive.alive_bitmap())
6066 .and_then(|n| n.checked_add(std::mem::size_of::<Self>()))
6067 .and_then(|n| n.checked_add(2 * std::mem::size_of::<usize>()))
6068 .and_then(|n| n.checked_add(alive_bytes?))
6069 .ok_or(LexicalAssemblyError::Store(StoreError::BudgetExceeded {
6070 needed: u64::MAX,
6071 budget: u64::MAX,
6072 component: "cache",
6073 }))?;
6074 let mut memory =
6075 stats::AccountedCounter::new(accounting, stats::AllocationComponent::Cache)
6076 .map_err(LexicalAssemblyError::Store)?;
6077 memory.set(bytes).map_err(LexicalAssemblyError::Store)?;
6078 let mut statistics = crate::fts::index::LiveSegmentStatistics::build_controlled(
6079 ordinal,
6080 postings,
6081 alive.alive_bitmap(),
6082 work,
6083 )?;
6084 if statistics.rows.cardinality() != 0
6085 && statistics.rows.cardinality() != u64::from(postings.row_count())
6086 {
6087 statistics.frequency_cache = Some(
6088 crate::fts::live_df::LiveFrequencyCache::new(accounting)
6089 .map_err(LexicalAssemblyError::Store)?,
6090 );
6091 }
6092 let statistics = Arc::new(statistics);
6093 work.check_now()?;
6094 Ok(Arc::new(Self {
6095 key,
6096 statistics,
6097 active_alive: active.then(|| Arc::clone(alive)),
6098 _memory: memory,
6099 }))
6100 }
6101}
6102
6103enum LexicalAssemblyError {
6104 Store(StoreError),
6105 Lexical(crate::fts::index::IndexError),
6106 MissingDocumentIdentity(crate::segment::SegmentId),
6107 Cancelled(crate::scan::ScanError),
6108}
6109
6110impl From<crate::fts::index::IndexError> for LexicalAssemblyError {
6111 fn from(error: crate::fts::index::IndexError) -> Self {
6112 Self::Lexical(error)
6113 }
6114}
6115
6116impl From<StoreError> for LexicalAssemblyError {
6117 fn from(error: StoreError) -> Self {
6118 Self::Store(error)
6119 }
6120}
6121
6122struct CachedLexicalAssembly {
6149 generation: u64,
6150 snapshot: std::sync::Weak<PublishedSnapshot>,
6151 active: std::sync::Weak<crate::ingest::ActiveSegment>,
6152 document_identity_verified: bool,
6157 assembly: Arc<LexicalAssembly>,
6158}
6159
6160impl CachedLexicalAssembly {
6161 fn matches(
6162 &self,
6163 snapshot: &Arc<PublishedSnapshot>,
6164 active: &Arc<crate::ingest::ActiveSegment>,
6165 ) -> bool {
6166 self.snapshot
6167 .upgrade()
6168 .is_some_and(|cached| Arc::ptr_eq(&cached, snapshot))
6169 && self
6170 .active
6171 .upgrade()
6172 .is_some_and(|cached| Arc::ptr_eq(&cached, active))
6173 }
6174}
6175
6176pub(crate) struct LexicalIndexCache {
6182 entry: Mutex<Option<CachedLexicalAssembly>>,
6183 #[cfg(any(test, feature = "test-seams"))]
6184 hits: AtomicU64,
6185 #[cfg(any(test, feature = "test-seams"))]
6186 builds: AtomicU64,
6187 #[cfg(any(test, feature = "test-seams"))]
6188 contribution_hits: AtomicU64,
6189 #[cfg(any(test, feature = "test-seams"))]
6190 contribution_builds: AtomicU64,
6191}
6192
6193impl LexicalIndexCache {
6194 pub(crate) const fn new() -> Self {
6195 Self {
6196 entry: Mutex::new(None),
6197 #[cfg(any(test, feature = "test-seams"))]
6198 hits: AtomicU64::new(0),
6199 #[cfg(any(test, feature = "test-seams"))]
6200 builds: AtomicU64::new(0),
6201 #[cfg(any(test, feature = "test-seams"))]
6202 contribution_hits: AtomicU64::new(0),
6203 #[cfg(any(test, feature = "test-seams"))]
6204 contribution_builds: AtomicU64::new(0),
6205 }
6206 }
6207
6208 #[cfg(any(test, feature = "test-seams"))]
6209 fn record_hit(&self) {
6210 self.hits.fetch_add(1, Ordering::Relaxed);
6211 }
6212
6213 #[cfg(not(any(test, feature = "test-seams")))]
6214 const fn record_hit(&self) {}
6215
6216 #[cfg(any(test, feature = "test-seams"))]
6217 fn record_build(&self) {
6218 self.builds.fetch_add(1, Ordering::Relaxed);
6219 }
6220
6221 #[cfg(not(any(test, feature = "test-seams")))]
6222 const fn record_build(&self) {}
6223
6224 fn record_contribution(&self, hit: bool) {
6225 #[cfg(any(test, feature = "test-seams"))]
6226 if hit {
6227 self.contribution_hits.fetch_add(1, Ordering::Relaxed);
6228 } else {
6229 self.contribution_builds.fetch_add(1, Ordering::Relaxed);
6230 }
6231 #[cfg(not(any(test, feature = "test-seams")))]
6232 let _ = hit;
6233 }
6234
6235 fn lock(
6236 &self,
6237 ) -> Result<std::sync::MutexGuard<'_, Option<CachedLexicalAssembly>>, LexicalAssemblyError>
6238 {
6239 self.entry.lock().map_err(|_| {
6240 LexicalAssemblyError::Store(StoreError::Synchronization {
6241 component: "lexical index cache",
6242 })
6243 })
6244 }
6245
6246 fn lock_controlled(
6247 &self,
6248 cancellation: Option<&QueryCancellation<'_>>,
6249 ) -> Result<std::sync::MutexGuard<'_, Option<CachedLexicalAssembly>>, LexicalAssemblyError>
6250 {
6251 let Some(cancellation) = cancellation else {
6252 return self.lock();
6253 };
6254 cancellation
6255 .check_graph()
6256 .map_err(LexicalAssemblyError::Cancelled)?;
6257 let guard = loop {
6258 match self.entry.try_lock() {
6259 Ok(guard) => break guard,
6260 Err(std::sync::TryLockError::Poisoned(_)) => {
6261 return Err(LexicalAssemblyError::Store(StoreError::Synchronization {
6262 component: "lexical index cache",
6263 }));
6264 }
6265 Err(std::sync::TryLockError::WouldBlock) => {}
6266 }
6267 cancellation
6268 .check_graph()
6269 .map_err(LexicalAssemblyError::Cancelled)?;
6270 std::thread::park_timeout(Duration::from_millis(1));
6271 };
6272 cancellation
6273 .check_graph()
6274 .map_err(LexicalAssemblyError::Cancelled)?;
6275 Ok(guard)
6276 }
6277}
6278
6279#[derive(Clone, Copy)]
6283struct LexicalInputs<'a> {
6284 filter: Option<&'a QueryFilter>,
6285 generation: u64,
6286 cache: &'a LexicalIndexCache,
6287 snapshot: &'a Arc<PublishedSnapshot>,
6288 active: &'a Arc<crate::ingest::ActiveSegment>,
6289 accounting: &'a Arc<stats::Accounting>,
6290}
6291
6292struct LexicalAssemblyReceipt {
6293 assembly: Arc<LexicalAssembly>,
6294 cache_hit: bool,
6295}
6296
6297impl std::ops::Deref for LexicalAssemblyReceipt {
6298 type Target = LexicalAssembly;
6299 fn deref(&self) -> &Self::Target {
6300 &self.assembly
6301 }
6302}
6303
6304fn map_eligibility_error(error: QueryError) -> LexicalAssemblyError {
6305 match error {
6306 QueryError::Store(error) => LexicalAssemblyError::Store(error),
6307 QueryError::Scan(error) => LexicalAssemblyError::Cancelled(error),
6308 error => LexicalAssemblyError::Store(StoreError::InvalidScan {
6309 detail: error.to_string(),
6310 }),
6311 }
6312}
6313
6314fn assemble_lexical_index(
6315 inputs: LexicalInputs<'_>,
6316 require_document_identity: bool,
6317 cancellation: Option<&QueryCancellation<'_>>,
6318) -> Result<LexicalAssemblyReceipt, LexicalAssemblyError> {
6319 let receipt =
6320 assemble_unfiltered_lexical_index(inputs, require_document_identity, cancellation)?;
6321 let Some(filter) = inputs.filter else {
6322 return Ok(receipt);
6323 };
6324 let mut alive_sets = Vec::with_capacity(receipt.alive_sets.len());
6325 for (source, alive) in receipt.sources.iter().zip(&receipt.alive_sets) {
6326 if let Some(control) = cancellation {
6327 control
6328 .check_graph()
6329 .map_err(LexicalAssemblyError::Cancelled)?;
6330 }
6331 let rows = match source {
6332 StructuredLexicalSource::Active => filter.active_rows(inputs.active),
6333 StructuredLexicalSource::Sealed(ordinal) => inputs
6334 .snapshot
6335 .segments()
6336 .get(*ordinal)
6337 .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))
6338 .and_then(|segment| filter.sealed_rows(segment)),
6339 }
6340 .map_err(map_eligibility_error)?;
6341 let mut eligible = (**alive).clone();
6342 #[cfg(any(test, feature = "test-seams"))]
6343 crate::fts::preparation_observer::record_filter_work(0, 0, 1);
6344 eligible.retain(&rows);
6345 alive_sets.push(Arc::new(eligible));
6346 }
6347 if let Some(control) = cancellation {
6348 control
6349 .check_graph()
6350 .map_err(LexicalAssemblyError::Cancelled)?;
6351 }
6352 let memory =
6353 stats::AccountedCounter::new(inputs.accounting, stats::AllocationComponent::Temporary)
6354 .map_err(LexicalAssemblyError::Store)?;
6355 let mut assembly = LexicalAssembly {
6356 index: receipt.index.clone(),
6357 alive_sets,
6358 sources: receipt.sources.clone(),
6359 contributions: receipt.contributions.clone(),
6360 vocabulary: Arc::clone(&receipt.vocabulary),
6361 _base: Some(Arc::clone(&receipt.assembly)),
6362 memory,
6363 };
6364 let bytes = assembly
6365 .alive_sets
6366 .iter()
6367 .fold(assembly.owned_bytes(), |bytes, alive| {
6368 bytes?
6369 .checked_add(alive.resident_bytes()?)?
6370 .checked_add(std::mem::size_of::<crate::meta::AliveSet>())?
6371 .checked_add(2 * std::mem::size_of::<usize>())
6372 })
6373 .ok_or(LexicalAssemblyError::Store(StoreError::ActiveRowOverflow))?;
6374 assembly
6375 .memory
6376 .set(bytes)
6377 .map_err(LexicalAssemblyError::Store)?;
6378 Ok(LexicalAssemblyReceipt {
6379 cache_hit: receipt.cache_hit,
6380 assembly: Arc::new(assembly),
6381 })
6382}
6383
6384fn assemble_unfiltered_lexical_index(
6385 inputs: LexicalInputs<'_>,
6386 require_document_identity: bool,
6387 cancellation: Option<&QueryCancellation<'_>>,
6388) -> Result<LexicalAssemblyReceipt, LexicalAssemblyError> {
6389 let LexicalInputs {
6390 filter: _,
6391 generation,
6392 cache,
6393 snapshot,
6394 active,
6395 accounting,
6396 } = inputs;
6397 let mut entry = cache.lock_controlled(cancellation)?;
6398 if let Some(cached) = entry.as_mut()
6399 && cached.matches(snapshot, active)
6400 {
6401 if require_document_identity && !cached.document_identity_verified {
6402 verify_document_identity(snapshot, cancellation)?;
6403 cached.document_identity_verified = true;
6404 }
6405 let assembly = Arc::clone(&cached.assembly);
6406 cache.record_hit();
6407 return Ok(LexicalAssemblyReceipt {
6408 assembly,
6409 cache_hit: true,
6410 });
6411 }
6412 let previous = entry.as_ref().map(|cached| Arc::clone(&cached.assembly));
6416 let contributions = previous
6417 .as_ref()
6418 .map_or(&[][..], |a| a.contributions.as_slice());
6419 let (mut assembly, owned_bytes) = build_lexical_assembly(
6420 snapshot,
6421 active,
6422 accounting,
6423 require_document_identity,
6424 cancellation,
6425 cache,
6426 contributions,
6427 )?;
6428 cache.record_build();
6429 assembly
6430 .memory
6431 .set(owned_bytes)
6432 .map_err(LexicalAssemblyError::Store)?;
6433 if let Some(cancellation) = cancellation {
6434 cancellation
6435 .check_graph()
6436 .map_err(LexicalAssemblyError::Cancelled)?;
6437 }
6438 let assembly = Arc::new(assembly);
6439 if entry
6440 .as_ref()
6441 .is_none_or(|cached| generation >= cached.generation)
6442 {
6443 *entry = Some(CachedLexicalAssembly {
6444 generation,
6445 snapshot: Arc::downgrade(snapshot),
6446 active: Arc::downgrade(active),
6447 document_identity_verified: require_document_identity,
6448 assembly: Arc::clone(&assembly),
6449 });
6450 }
6451 drop(entry);
6452 Ok(LexicalAssemblyReceipt {
6453 assembly,
6454 cache_hit: false,
6455 })
6456}
6457
6458fn check_segment_document_identity(
6459 segment: &crate::segment::reader::SegmentReader,
6460 postings: &crate::fts::sealed::SealedSegment,
6461) -> Result<(), LexicalAssemblyError> {
6462 if postings.row_count() != 0
6463 && segment
6464 .document_version(0)
6465 .map_err(StoreError::Segment)
6466 .map_err(LexicalAssemblyError::Store)?
6467 .is_none()
6468 {
6469 return Err(LexicalAssemblyError::MissingDocumentIdentity(
6470 segment.meta().id,
6471 ));
6472 }
6473 Ok(())
6474}
6475
6476fn verify_document_identity(
6482 snapshot: &PublishedSnapshot,
6483 cancellation: Option<&QueryCancellation<'_>>,
6484) -> Result<(), LexicalAssemblyError> {
6485 for segment in snapshot.segments() {
6486 if let Some(cancellation) = cancellation {
6487 cancellation
6488 .check_graph()
6489 .map_err(LexicalAssemblyError::Cancelled)?;
6490 }
6491 if let Some(postings) = segment
6492 .query_postings()
6493 .map_err(LexicalAssemblyError::Store)?
6494 {
6495 check_segment_document_identity(segment, &postings)?;
6496 }
6497 }
6498 Ok(())
6499}
6500
6501fn build_lexical_assembly(
6502 snapshot: &PublishedSnapshot,
6503 active: &Arc<crate::ingest::ActiveSegment>,
6504 accounting: &Arc<stats::Accounting>,
6505 require_document_identity: bool,
6506 cancellation: Option<&QueryCancellation<'_>>,
6507 cache: &LexicalIndexCache,
6508 previous: &[Arc<CachedLexicalContribution>],
6509) -> Result<(LexicalAssembly, usize), LexicalAssemblyError> {
6510 let mut work = crate::fts::control::WorkCheck::new(|| {
6511 cancellation
6512 .map_or(Ok(()), |control| control.check_graph())
6513 .map_err(LexicalAssemblyError::Cancelled)
6514 });
6515 work.check_now()?;
6516 let mut index = crate::fts::index::LexicalIndex::new();
6517 let mut alive_sets = Vec::new();
6518 let mut sources = Vec::new();
6519 let mut contributions = Vec::new();
6520 for (ordinal, segment) in snapshot.segments().iter().enumerate() {
6521 if let Some(cancellation) = cancellation {
6522 cancellation
6523 .check_graph()
6524 .map_err(LexicalAssemblyError::Cancelled)?;
6525 }
6526 if let Some(postings) = segment
6527 .query_postings()
6528 .map_err(LexicalAssemblyError::Store)?
6529 {
6530 if require_document_identity {
6531 check_segment_document_identity(segment, &postings)?;
6532 }
6533 let alive = segment.query_alive().map_err(LexicalAssemblyError::Store)?;
6534 let identity = segment
6535 .lexical_postings_identity()
6536 .map_err(StoreError::Segment)
6537 .map_err(LexicalAssemblyError::Store)?;
6538 let reused = previous
6539 .binary_search_by(|value| match &value.key {
6540 LexicalContributionKey::Sealed(meta, _) => meta.id.cmp(&segment.meta().id),
6541 LexicalContributionKey::Active(_) => std::cmp::Ordering::Greater,
6542 })
6543 .ok()
6544 .and_then(|slot| previous.get(slot))
6545 .filter(|value| {
6546 matches!(&value.key, LexicalContributionKey::Sealed(meta, stored)
6547 if meta.same_segment_file(segment.meta()) && *stored == identity)
6548 && value.statistics.rows == *alive.alive_bitmap()
6549 });
6550 cache.record_contribution(reused.is_some());
6551 let contribution = match reused {
6552 Some(value) => Arc::clone(value),
6553 None => CachedLexicalContribution::build(
6554 LexicalContributionKey::Sealed(segment.meta().clone(), identity),
6555 &postings,
6556 &alive,
6557 index.segments().len(),
6558 accounting,
6559 &mut work,
6560 )?,
6561 };
6562 index.push_shared_with_statistics(postings, Arc::clone(&contribution.statistics));
6563 contributions.push(contribution);
6564 alive_sets.push(alive);
6565 sources.push(StructuredLexicalSource::Sealed(ordinal));
6566 }
6567 }
6568 if active.has_text_controlled(&mut work)? {
6569 let sealed = active.sealed_lexical_controlled(accounting, &mut work)?;
6570 let reused = previous.last().filter(|value| {
6571 matches!(&value.key, LexicalContributionKey::Active(cached)
6572 if cached.upgrade().is_some_and(|cached| Arc::ptr_eq(&cached, active)))
6573 });
6574 cache.record_contribution(reused.is_some());
6575 let contribution = match reused {
6576 Some(value) => Arc::clone(value),
6577 None => {
6578 let alive = Arc::new(active.alive_controlled(&mut work)?);
6579 CachedLexicalContribution::build(
6580 LexicalContributionKey::Active(Arc::downgrade(active)),
6581 &sealed,
6582 &alive,
6583 index.segments().len(),
6584 accounting,
6585 &mut work,
6586 )?
6587 }
6588 };
6589 let alive = contribution
6590 .active_alive
6591 .as_ref()
6592 .ok_or(LexicalAssemblyError::Store(StoreError::Synchronization {
6593 component: "active lexical contribution",
6594 }))?;
6595 index.push_shared_with_statistics(sealed, Arc::clone(&contribution.statistics));
6596 alive_sets.push(Arc::clone(alive));
6597 contributions.push(contribution);
6598 sources.push(StructuredLexicalSource::Active);
6599 }
6600 crate::fts::control::sort_by(
6601 &mut contributions,
6602 |left, right| match (&left.key, &right.key) {
6603 (LexicalContributionKey::Sealed(a, _), LexicalContributionKey::Sealed(b, _)) => {
6604 a.id.cmp(&b.id)
6605 }
6606 (LexicalContributionKey::Active(_), LexicalContributionKey::Active(_)) => {
6607 std::cmp::Ordering::Equal
6608 }
6609 (LexicalContributionKey::Active(_), _) => std::cmp::Ordering::Greater,
6610 (_, LexicalContributionKey::Active(_)) => std::cmp::Ordering::Less,
6611 },
6612 &mut work,
6613 )?;
6614 let assembly = LexicalAssembly {
6615 index,
6616 alive_sets,
6617 sources,
6618 contributions,
6619 vocabulary: Arc::new(Mutex::new(None)),
6620 _base: None,
6621 memory: stats::AccountedCounter::new(accounting, stats::AllocationComponent::Cache)
6622 .map_err(LexicalAssemblyError::Store)?,
6623 };
6624 let owned_bytes = assembly
6625 .owned_bytes()
6626 .and_then(|n| {
6627 n.checked_add(
6628 std::mem::size_of::<Mutex<Option<Arc<CachedVocabulary>>>>()
6629 + 2 * std::mem::size_of::<usize>(),
6630 )
6631 })
6632 .ok_or(LexicalAssemblyError::Store(StoreError::BudgetExceeded {
6633 needed: u64::MAX,
6634 budget: u64::MAX,
6635 component: "cache",
6636 }))?;
6637 work.check_now()?;
6638 Ok((assembly, owned_bytes))
6639}
6640
6641fn map_store_lexical_assembly_error(
6642 error: LexicalAssemblyError,
6643) -> crate::ingest::StoreLexicalError {
6644 match error {
6645 LexicalAssemblyError::Store(error) => QueryError::Store(error).into(),
6646 LexicalAssemblyError::Lexical(error) => {
6647 crate::planner::LexicalFilterError::from(error).into()
6648 }
6649 LexicalAssemblyError::MissingDocumentIdentity(segment_id) => {
6650 crate::ingest::StoreLexicalError::MissingDocumentIdentity { segment_id }
6651 }
6652 LexicalAssemblyError::Cancelled(error) => QueryError::Scan(error).into(),
6653 }
6654}
6655
6656fn map_fusion_lexical_assembly_error(error: LexicalAssemblyError) -> crate::fusion::FusionError {
6657 match error {
6658 LexicalAssemblyError::Store(error) => crate::fusion::FusionError::Leg {
6659 leg: crate::fusion::FusionLeg::Lexical,
6660 kind: crate::fusion::LegFailureKind::Store(error.kind()),
6661 detail: error.to_string(),
6662 },
6663 LexicalAssemblyError::Lexical(error) => crate::fusion::FusionError::Leg {
6664 leg: crate::fusion::FusionLeg::Lexical,
6665 kind: crate::fusion::LegFailureKind::Lexical,
6666 detail: error.to_string(),
6667 },
6668 LexicalAssemblyError::MissingDocumentIdentity(segment_id) => {
6669 crate::fusion::FusionError::Leg {
6670 leg: crate::fusion::FusionLeg::Lexical,
6671 kind: crate::fusion::LegFailureKind::Invariant,
6672 detail: format!("sealed lexical segment {segment_id} has no document identity"),
6673 }
6674 }
6675 LexicalAssemblyError::Cancelled(error) => {
6676 crate::fusion::FusionError::from(QueryError::Scan(error))
6677 }
6678 }
6679}
6680
6681fn accumulate_search_counters(
6682 total: &mut crate::fts::search::SearchCounters,
6683 delta: &crate::fts::search::SearchCounters,
6684) {
6685 total.docs_evaluated = total.docs_evaluated.saturating_add(delta.docs_evaluated);
6686 total.postings_decoded = total
6687 .postings_decoded
6688 .saturating_add(delta.postings_decoded);
6689 total.blocks_decoded = total.blocks_decoded.saturating_add(delta.blocks_decoded);
6690 total.blocks_skipped = total.blocks_skipped.saturating_add(delta.blocks_skipped);
6691}
6692
6693fn map_fusion_controlled_lexical_error(
6694 error: crate::fts::search::ControlledSearchError<crate::scan::ScanError>,
6695) -> crate::fusion::FusionError {
6696 match error {
6697 crate::fts::search::ControlledSearchError::Index(error) => {
6698 crate::fusion::FusionError::Leg {
6699 leg: crate::fusion::FusionLeg::Lexical,
6700 kind: crate::fusion::LegFailureKind::Lexical,
6701 detail: error.to_string(),
6702 }
6703 }
6704 crate::fts::search::ControlledSearchError::Control(error) => {
6705 crate::fusion::FusionError::from(QueryError::Scan(error))
6706 }
6707 }
6708}
6709
6710#[derive(Clone, Copy)]
6711enum PinnedLexicalQuery<'a> {
6712 Term(&'a crate::fts::search::TermQuery),
6713 Structured(&'a crate::fts::query::LexicalQuery),
6714}
6715
6716enum LexicalDocumentError {
6717 SourceOrdinalOutOfRange,
6718 RowOverflow,
6719 SealedSourceAbsent,
6720 Segment(crate::segment::SegmentError),
6721 MissingDocumentIdentity(crate::segment::SegmentId),
6722 ActiveRowAbsent,
6723}
6724
6725fn structured_lexical_document(
6726 snapshot: &PublishedSnapshot,
6727 active: &crate::ingest::ActiveSegment,
6728 sources: &[StructuredLexicalSource],
6729 doc: crate::fts::search::GlobalDocId,
6730 require_document_identity: bool,
6731) -> Result<Option<crate::ingest::DocumentVersion>, LexicalDocumentError> {
6732 let source = usize::try_from(doc.segment)
6733 .ok()
6734 .and_then(|slot| sources.get(slot))
6735 .ok_or(LexicalDocumentError::SourceOrdinalOutOfRange)?;
6736 let row = usize::try_from(doc.row).map_err(|_| LexicalDocumentError::RowOverflow)?;
6737 match source {
6738 StructuredLexicalSource::Sealed(ordinal) => {
6739 let segment = snapshot
6740 .segments()
6741 .get(*ordinal)
6742 .ok_or(LexicalDocumentError::SealedSourceAbsent)?;
6743 let document = segment
6744 .document_version(row)
6745 .map_err(LexicalDocumentError::Segment)?;
6746 if require_document_identity && document.is_none() {
6747 return Err(LexicalDocumentError::MissingDocumentIdentity(
6748 segment.meta().id,
6749 ));
6750 }
6751 Ok(document)
6752 }
6753 StructuredLexicalSource::Active => {
6754 let document = active.document(row);
6755 if require_document_identity && document.is_none() {
6756 return Err(LexicalDocumentError::ActiveRowAbsent);
6757 }
6758 Ok(document)
6759 }
6760 }
6761}
6762
6763fn map_store_lexical_document_error(
6764 error: LexicalDocumentError,
6765) -> crate::ingest::StoreLexicalError {
6766 match error {
6767 LexicalDocumentError::SourceOrdinalOutOfRange
6768 | LexicalDocumentError::RowOverflow
6769 | LexicalDocumentError::SealedSourceAbsent
6770 | LexicalDocumentError::ActiveRowAbsent => {
6771 QueryError::Store(StoreError::ActiveRowOverflow).into()
6772 }
6773 LexicalDocumentError::Segment(error) => {
6774 QueryError::Store(StoreError::Segment(error)).into()
6775 }
6776 LexicalDocumentError::MissingDocumentIdentity(segment_id) => {
6777 crate::ingest::StoreLexicalError::MissingDocumentIdentity { segment_id }
6778 }
6779 }
6780}
6781
6782fn map_fusion_lexical_document_error(
6783 error: LexicalDocumentError,
6784 invariant_kind: crate::fusion::LegFailureKind,
6785 segment_kind: crate::fusion::LegFailureKind,
6786) -> crate::fusion::FusionError {
6787 let (kind, detail) = match error {
6788 LexicalDocumentError::SourceOrdinalOutOfRange => (
6789 invariant_kind,
6790 "lexical source ordinal is out of range".to_owned(),
6791 ),
6792 LexicalDocumentError::RowOverflow => {
6793 (invariant_kind, "lexical row exceeds usize".to_owned())
6794 }
6795 LexicalDocumentError::SealedSourceAbsent => {
6796 (invariant_kind, "lexical sealed source is absent".to_owned())
6797 }
6798 LexicalDocumentError::Segment(error) => (segment_kind, error.to_string()),
6799 LexicalDocumentError::MissingDocumentIdentity(segment_id) => (
6800 invariant_kind,
6801 format!("sealed lexical segment {segment_id} has no document identity"),
6802 ),
6803 LexicalDocumentError::ActiveRowAbsent => (
6804 invariant_kind,
6805 "lexical active source row is absent".to_owned(),
6806 ),
6807 };
6808 crate::fusion::FusionError::Leg {
6809 leg: crate::fusion::FusionLeg::Lexical,
6810 kind,
6811 detail,
6812 }
6813}
6814
6815fn map_structured_leg_document_error(error: LexicalDocumentError) -> crate::fusion::FusionError {
6816 map_fusion_lexical_document_error(
6817 error,
6818 crate::fusion::LegFailureKind::Lexical,
6819 crate::fusion::LegFailureKind::Lexical,
6820 )
6821}
6822
6823fn map_term_leg_document_error(error: LexicalDocumentError) -> crate::fusion::FusionError {
6824 map_fusion_lexical_document_error(
6825 error,
6826 crate::fusion::LegFailureKind::Invariant,
6827 crate::fusion::LegFailureKind::Segment,
6828 )
6829}
6830
6831fn structured_lexical_row<'a>(
6832 snapshot: &'a PublishedSnapshot,
6833 active: &'a crate::ingest::ActiveSegment,
6834 sources: &[StructuredLexicalSource],
6835 doc: crate::fts::search::GlobalDocId,
6836) -> Result<(&'a str, crate::ingest::DocumentVersion), crate::ingest::StoreLexicalError> {
6837 let source = usize::try_from(doc.segment)
6838 .ok()
6839 .and_then(|slot| sources.get(slot))
6840 .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
6841 let row =
6842 usize::try_from(doc.row).map_err(|_| QueryError::Store(StoreError::ActiveRowOverflow))?;
6843 match source {
6844 StructuredLexicalSource::Sealed(ordinal) => {
6845 let segment = snapshot
6846 .segments()
6847 .get(*ordinal)
6848 .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
6849 let text = segment
6850 .query_stored_text()
6851 .map_err(StoreError::Segment)
6852 .map_err(QueryError::Store)?
6853 .and_then(|rows| rows.row(row).flatten())
6854 .ok_or(crate::ingest::StoreLexicalError::MissingStoredText {
6855 segment_id: segment.meta().id,
6856 row: doc.row,
6857 })?;
6858 let document = segment
6859 .document_version(row)
6860 .map_err(StoreError::Segment)
6861 .map_err(QueryError::Store)?
6862 .ok_or(crate::ingest::StoreLexicalError::MissingDocumentIdentity {
6863 segment_id: segment.meta().id,
6864 })?;
6865 Ok((text, document))
6866 }
6867 StructuredLexicalSource::Active => {
6868 let text = active
6869 .text(row)
6870 .map_err(QueryError::Store)?
6871 .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
6872 let document = active
6873 .document(row)
6874 .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
6875 Ok((text, document))
6876 }
6877 }
6878}
6879
6880type ExactLexicalLeg = (
6881 Vec<hybrid::LexicalHit>,
6882 Arc<LexicalAssembly>,
6883 crate::fts::search::SearchCounters,
6884 bool,
6885);
6886
6887fn resolve_hybrid_leg_results<Vector, Lexical>(
6888 vector: Result<Vector, crate::fusion::FusionError>,
6889 lexical: Result<Lexical, crate::fusion::FusionError>,
6890) -> Result<(Vector, Lexical), crate::fusion::FusionError> {
6891 fn control_priority(error: &crate::fusion::FusionError) -> Option<u8> {
6892 match error {
6893 crate::fusion::FusionError::ReadCancelled { .. } => Some(0),
6894 crate::fusion::FusionError::Timeout { .. } => Some(1),
6895 crate::fusion::FusionError::Cancelled { .. } => Some(2),
6896 _ => None,
6897 }
6898 }
6899
6900 match (vector, lexical) {
6901 (Ok(vector), Ok(lexical)) => Ok((vector, lexical)),
6902 (Err(vector), Ok(_)) => Err(vector),
6903 (Ok(_), Err(lexical)) => Err(lexical),
6904 (Err(vector), Err(lexical)) => {
6905 match (control_priority(&vector), control_priority(&lexical)) {
6906 (Some(vector_priority), Some(lexical_priority)) => {
6907 if vector_priority <= lexical_priority {
6908 Err(vector)
6909 } else {
6910 Err(lexical)
6911 }
6912 }
6913 (Some(_), None) => Err(vector),
6914 (None, Some(_)) => Err(lexical),
6915 (None, None) => Err(vector),
6916 }
6917 }
6918 }
6919}
6920
6921fn exact_structured_lexical_leg(
6922 inputs: LexicalInputs<'_>,
6923 _analyzer: &crate::fts::tokenizer::Analyzer,
6924 query: &crate::fts::query::LexicalQuery,
6925 bound: usize,
6926 cancellation: &QueryCancellation<'_>,
6927 preparation: &mut prepared_lexical::PreparedLexicalQuery<'_>,
6928) -> Result<ExactLexicalLeg, crate::fusion::FusionError> {
6929 let (snapshot, active) = (inputs.snapshot, inputs.active);
6930 let lexical_error = |detail: String| crate::fusion::FusionError::Leg {
6931 leg: crate::fusion::FusionLeg::Lexical,
6932 kind: crate::fusion::LegFailureKind::Lexical,
6933 detail,
6934 };
6935 let (prepared, cache_hit) = preparation.prepare_structured(inputs, cancellation)?;
6936 let assembly = &prepared.assembly;
6937 let index = &assembly.index;
6938 let sources = &assembly.sources;
6939 let Some(query_scoring) = prepared.query.as_ref() else {
6940 return Ok((
6941 Vec::new(),
6942 Arc::clone(assembly),
6943 crate::fts::search::SearchCounters::default(),
6944 cache_hit,
6945 ));
6946 };
6947 let allow_lists = assembly
6948 .alive_sets
6949 .iter()
6950 .map(|alive| alive.alive_bitmap())
6951 .collect::<Vec<_>>();
6952 let mut scratch_memory =
6953 stats::AccountedCounter::new(inputs.accounting, stats::AllocationComponent::Temporary)
6954 .map_err(QueryError::Store)?;
6955 let mut phrase = prepared_lexical::PhraseEligibility::new(
6956 index,
6957 Some(query),
6958 inputs.accounting,
6959 cancellation,
6960 )?;
6961 let result = crate::fts::prune::weighted::search(
6962 index,
6963 query_scoring,
6964 bound,
6965 &allow_lists,
6966 || {
6967 cancellation
6968 .check_graph()
6969 .map_err(QueryError::Scan)
6970 .map_err(crate::fusion::FusionError::from)
6971 },
6972 |doc| {
6973 phrase
6974 .matches(doc, cancellation)
6975 .map_err(crate::fusion::FusionError::from)
6976 },
6977 |bytes| {
6978 prepared_lexical::reserve_weighted_scratch(&mut scratch_memory, bytes)
6979 .map_err(crate::fusion::FusionError::from)
6980 },
6981 )
6982 .map_err(|error| match error {
6983 crate::fts::search::ControlledSearchError::Index(error) => lexical_error(error.to_string()),
6984 crate::fts::search::ControlledSearchError::Control(error) => error,
6985 })?;
6986 let counters = result.counters;
6987 let scored = result.hits;
6988 let mut joined = Vec::with_capacity(scored.len());
6989 for (doc, score) in scored {
6990 let document = structured_lexical_document(snapshot, active, sources, doc, false)
6991 .map_err(map_structured_leg_document_error)?;
6992 joined.push(hybrid::LexicalHit {
6993 doc,
6994 document,
6995 bm25: score,
6996 });
6997 }
6998 Ok((joined, Arc::clone(assembly), counters, cache_hit))
6999}
7000
7001fn exact_lexical_leg(
7002 inputs: LexicalInputs<'_>,
7003 bound: usize,
7004 cancellation: &QueryCancellation<'_>,
7005 preparation: &mut prepared_lexical::PreparedLexicalQuery<'_>,
7006) -> Result<ExactLexicalLeg, crate::fusion::FusionError> {
7007 let (snapshot, active) = (inputs.snapshot, inputs.active);
7008 let (prepared, cache_hit) = preparation.prepare_term(inputs, cancellation)?;
7009 let assembly = &prepared.assembly;
7010 let index = &assembly.index;
7011 let sources = &assembly.sources;
7012 if index.segments().is_empty() {
7013 return Ok((
7014 Vec::new(),
7015 Arc::clone(assembly),
7016 crate::fts::search::SearchCounters::default(),
7017 cache_hit,
7018 ));
7019 }
7020 let k = bound.min(usize::try_from(index.document_count()).unwrap_or(usize::MAX));
7021 let allow_lists = assembly
7022 .alive_sets
7023 .iter()
7024 .map(|alive| alive.alive_bitmap())
7025 .collect::<Vec<_>>();
7026 let result =
7027 exact_hybrid_lexical_search(index, prepared.scoring()?, k, &allow_lists, cancellation)?;
7028 let mut joined = Vec::with_capacity(result.hits.len());
7029 for hit in result.hits {
7030 let document = structured_lexical_document(snapshot, active, sources, hit.doc, false)
7031 .map_err(map_term_leg_document_error)?;
7032 joined.push(hybrid::LexicalHit {
7033 doc: hit.doc,
7034 document,
7035 bm25: hit.score,
7036 });
7037 }
7038 Ok((joined, Arc::clone(assembly), result.counters, cache_hit))
7039}
7040
7041fn exact_hybrid_lexical_search(
7042 index: &crate::fts::index::LexicalIndex,
7043 prepared: &crate::fts::search::PreparedTermQuery,
7044 k: usize,
7045 allow_lists: &[&crate::meta::DocBitmap],
7046 cancellation: &QueryCancellation<'_>,
7047) -> Result<crate::fts::search::SearchResult, crate::fusion::FusionError> {
7048 let allowed = allow_lists
7049 .iter()
7050 .map(|allow_list| allow_list.cardinality())
7051 .fold(0_u64, u64::saturating_add);
7052 if allowed.saturating_mul(crate::planner::LEXICAL_ALLOW_LIST_DIVISOR) <= index.document_count()
7053 {
7054 return crate::fts::search::search_allow_list_prepared_controlled(
7055 index,
7056 prepared,
7057 k,
7058 allow_lists,
7059 || cancellation.check_graph(),
7060 )
7061 .map_err(map_fusion_controlled_lexical_error);
7062 }
7063 crate::fts::prune::search_pruned_prepared_controlled(index, prepared, k, allow_lists, || {
7064 cancellation.check_graph()
7065 })
7066 .map_err(map_fusion_controlled_lexical_error)
7067}
7068
7069#[allow(clippy::too_many_arguments)]
7070fn search_pinned(
7071 pool: Option<&pool::QueryPool>,
7072 snapshot: &Arc<PublishedSnapshot>,
7073 active: &crate::ingest::ActiveSegment,
7074 accounting: &Arc<stats::Accounting>,
7075 generation: u64,
7076 epoch: Option<crate::epoch::EpochIdentity>,
7077 request: crate::ingest::SearchRequest<'_>,
7078 k: usize,
7079 options: SearchOptions,
7080 control: QueryControl,
7081 graph_bound_mode: GraphBoundMode,
7082 requested_tier: Option<SearchTier>,
7083 hybrid_scan_reason: Option<crate::planner::ScanReason>,
7084 started: std::time::Instant,
7085 clock: &dyn MonotonicClock,
7086 vector_preparation: &mut prepared::PreparedVectorQuery<'_>,
7087 #[cfg(any(test, feature = "test-seams"))] vector_fault_controller: Option<
7088 &crate::scan::vector_fault::VectorFaultController,
7089 >,
7090) -> Result<crate::ingest::SearchOutcome, QueryError> {
7091 use crate::ingest::{GraphSearchStats, RowSource, SearchOutcome};
7092 use crate::quant::prepare_bit4_query;
7093 use crate::scan::{ScanQuery, ScanRequest, ScanRows, ScanStats};
7094
7095 let vector_started = timing_start(clock);
7096 let scan_options = options.scan();
7097 let rescore_options = options.scan_rescore();
7098 let mut rescore_counters = crate::diag::ScanRescoreCounters::default();
7099 if let Some(options) = rescore_options {
7100 options.frontier(k, 0)?;
7101 }
7102 let query = request.vector();
7103 prepared::validate_vector_coordinates(query, crate::kernels::MAX_DOT_I8_DIMENSION, |_| {
7104 Ok::<(), std::convert::Infallible>(())
7105 })
7106 .map_err(|error| match error {
7107 prepared::VectorValidationError::Data(error) => {
7108 QueryError::Scan(crate::scan::ScanError::Quant(error))
7109 }
7110 prepared::VectorValidationError::Control(never) => match never {},
7111 })?;
7112 vector_preparation.validate_binding(query, epoch)?;
7113 let bit4_query = std::cell::OnceCell::new();
7114 let int8_query = std::cell::OnceCell::new();
7115 let mut candidates = Vec::new();
7116 let mut dims_touched = 0_u64;
7117 let mut bytes_read = 0_u64;
7118 let mut worst_squared_l2: Option<f64> = None;
7119 let mut worst_exhaustive = true;
7120 let mut worker_thread_ids = Vec::new();
7121 let mut graph_stats = GraphSearchStats::default();
7122 let mut plans = Vec::new();
7123 let auto_graph_options = if matches!(options.tier(), SearchTier::Auto)
7124 && snapshot.segments().iter().any(|segment| {
7125 segment
7126 .directory()
7127 .iter()
7128 .any(|entry| entry.kind == crate::segment::layout::RegionKind::GraphNodeBlocks.id())
7129 }) {
7130 Some(auto_graph_search_options(snapshot)?)
7131 } else {
7132 None
7133 };
7134
7135 if matches!(options.tier(), SearchTier::Graph(_)) {
7136 for segment in snapshot.segments() {
7137 if !segment
7138 .directory()
7139 .iter()
7140 .any(|entry| entry.kind == crate::segment::layout::RegionKind::GraphNodeBlocks.id())
7141 {
7142 return Err(QueryError::Store(StoreError::GraphUnavailable {
7143 segment_id: segment.meta().id,
7144 }));
7145 }
7146 }
7147 }
7148 let full_precision = matches!(options.tier(), SearchTier::Exact | SearchTier::Graph(_))
7149 || (matches!(options.tier(), SearchTier::Auto)
7150 && auto_uses_full_precision(snapshot, active));
7151
7152 let exact_lane = if full_precision {
7154 pool.map_or(0, |pool| {
7155 pool.reserve_exact_lane(scan_options.thread_budget)
7156 })
7157 } else {
7158 0
7159 };
7160
7161 if !active.is_empty() {
7162 let mut alive = active.alive().map_err(QueryError::Store)?;
7163 if let Some(filter) = request.filter {
7164 alive.retain(&filter.active_rows(active)?);
7165 }
7166 let mut exact_score = full_precision;
7167 let mut approximate = false;
7168 let frontier = rescore_options
7169 .map(|options| options.frontier(k, alive.live_count()))
7170 .transpose()?
7171 .unwrap_or(k);
7172 let mut exact_memory = ExactScanMemory::new(accounting)?;
7173 let mut outcome = match options.tier() {
7174 SearchTier::Auto if full_precision => {
7175 let lease = SnapshotLease::new_at(Arc::clone(snapshot), generation);
7176 scan_active_squared_l2(
7177 active,
7178 &alive,
7179 request.vector(),
7180 frontier,
7181 ExactScanDispatch {
7182 pool,
7183 accounting,
7184 control: &control,
7185 lease: &lease,
7186 thread_budget: scan_options.thread_budget,
7187 lane: exact_lane,
7188 },
7189 &mut exact_memory,
7190 #[cfg(any(test, feature = "test-seams"))]
7191 vector_fault_controller,
7192 #[cfg(any(test, feature = "test-seams"))]
7193 vector_fault_tier(options.tier()),
7194 )?
7195 }
7196 SearchTier::Auto | SearchTier::Scan => {
7197 let query_pool = pool.ok_or(QueryError::Store(StoreError::Synchronization {
7198 component: "active scan-tier query pool",
7199 }))?;
7200 let bit4_query = bit4_query
7201 .get_or_init(|| prepare_bit4_query(request.vector(), 0))
7202 .as_ref()
7203 .map_err(|error| crate::scan::ScanError::Quant(error.clone()))
7204 .map_err(QueryError::Scan)?;
7205 execute_store_scan(
7206 query_pool,
7207 ScanRequest {
7208 query: ScanQuery::Bit4(bit4_query),
7209 rows: ScanRows::Bit4RowMajor {
7210 codes: active.codes(),
7211 factors: active.factors(),
7212 },
7213 row_mask: Some(alive.scan_mask()),
7214 },
7215 frontier,
7216 scan_options,
7217 control.clone(),
7218 SnapshotLease::new_at(Arc::clone(snapshot), generation),
7219 #[cfg(any(test, feature = "test-seams"))]
7220 vector_fault_controller,
7221 #[cfg(any(test, feature = "test-seams"))]
7222 crate::scan::vector_fault::VectorRowSource::Active,
7223 #[cfg(any(test, feature = "test-seams"))]
7224 vector_fault_tier(options.tier()),
7225 )?
7226 }
7227 SearchTier::Exact | SearchTier::Graph(_) => {
7228 let lease = SnapshotLease::new_at(Arc::clone(snapshot), generation);
7229 scan_active_squared_l2(
7230 active,
7231 &alive,
7232 request.vector(),
7233 frontier,
7234 ExactScanDispatch {
7235 pool,
7236 accounting,
7237 control: &control,
7238 lease: &lease,
7239 thread_budget: scan_options.thread_budget,
7240 lane: exact_lane,
7241 },
7242 &mut exact_memory,
7243 #[cfg(any(test, feature = "test-seams"))]
7244 vector_fault_controller,
7245 #[cfg(any(test, feature = "test-seams"))]
7246 vector_fault_tier(options.tier()),
7247 )?
7248 }
7249 };
7250 if let Some(rescore) = rescore_options {
7251 let lease = SnapshotLease::new_at(Arc::clone(snapshot), generation);
7252 let cancellation = QueryCancellation::new(&control, &lease);
7253 let (counters, exhaustive, worst) = rescored_scan::rescore(
7254 &mut outcome.candidates,
7255 &mut outcome.stats,
7256 active.vectors(),
7257 query,
7258 k,
7259 alive.live_count(),
7260 rescore,
7261 &cancellation,
7262 accounting,
7263 &mut exact_memory,
7264 )?;
7265 outcome.worst_score = worst;
7266 exact_score = true;
7267 approximate = !exhaustive;
7268 rescored_scan::accumulate(&mut rescore_counters, counters)?;
7269 }
7270 fold_worst_squared_l2(&mut worst_squared_l2, &mut worst_exhaustive, &outcome);
7271 merge_store_outcome(
7272 outcome,
7273 RowSource::Active,
7274 |row| Ok(active.document(row)),
7275 &mut candidates,
7276 &mut dims_touched,
7277 &mut bytes_read,
7278 &mut worker_thread_ids,
7279 exact_score,
7280 #[cfg(any(test, feature = "test-seams"))]
7281 vector_fault_controller,
7282 #[cfg(any(test, feature = "test-seams"))]
7283 vector_fault_tier(options.tier()),
7284 )?;
7285 let mut plan = crate::planner::SegmentPlan::unfiltered_scan(
7286 RowSource::Active,
7287 crate::planner::SegmentTier::ActiveScan,
7288 alive.live_count(),
7289 crate::planner::ScanReason::ActiveSegment,
7290 );
7291 plan.approximate = approximate;
7292 plans.push(plan);
7293 if matches!(graph_bound_mode, GraphBoundMode::Shared) {
7294 retain_global_top_k(&mut candidates, k);
7295 }
7296 }
7297
7298 let mut ordered_segments = snapshot.segments().iter().collect::<Vec<_>>();
7299 if matches!(graph_bound_mode, GraphBoundMode::Shared) {
7300 ordered_segments.sort_unstable_by(|left, right| {
7304 right
7305 .meta()
7306 .row_count
7307 .cmp(&left.meta().row_count)
7308 .then_with(|| left.meta().id.cmp(&right.meta().id))
7309 });
7310 }
7311
7312 for segment in ordered_segments {
7313 let original_alive = segment.query_alive().map_err(QueryError::Store)?;
7314 let mut filtered_alive;
7315 let alive = if let Some(filter) = request.filter {
7316 filtered_alive = (*original_alive).clone();
7317 filtered_alive.retain(&filter.sealed_rows(segment)?);
7318 &filtered_alive
7319 } else {
7320 original_alive.as_ref()
7321 };
7322 let source = RowSource::Sealed(segment.meta().id);
7323 let graph_options = match options.tier() {
7328 SearchTier::Graph(graph_options) => Some(graph_options),
7329 SearchTier::Auto
7330 if segment.directory().iter().any(|entry| {
7331 entry.kind == crate::segment::layout::RegionKind::GraphNodeBlocks.id()
7332 }) =>
7333 {
7334 auto_graph_options
7335 }
7336 SearchTier::Auto | SearchTier::Exact | SearchTier::Scan => None,
7337 };
7338 if let Some(graph_options) = graph_options {
7339 worst_exhaustive = false;
7340 let competitive_distance = global_competitive_distance(&candidates, k);
7341 let MergedSegmentGraph {
7342 plan,
7343 traversed,
7344 _result,
7345 _scratch,
7346 _lease,
7347 } = traverse_segment_graph(
7348 segment,
7349 alive,
7350 source,
7351 graph_options,
7352 graph_bound_mode,
7353 request,
7354 k,
7355 vector_preparation,
7356 accounting,
7357 snapshot,
7358 generation,
7359 &control,
7360 competitive_distance,
7361 #[cfg(any(test, feature = "test-seams"))]
7362 vector_fault_controller,
7363 options.tier(),
7364 )?
7365 .merge_into(
7366 segment,
7367 alive,
7368 source,
7369 &mut candidates,
7370 &mut dims_touched,
7371 &mut bytes_read,
7372 &mut worker_thread_ids,
7373 &mut graph_stats,
7374 #[cfg(any(test, feature = "test-seams"))]
7375 vector_fault_controller,
7376 options.tier(),
7377 )?;
7378 plans.push(plan);
7379 if traversed && matches!(graph_bound_mode, GraphBoundMode::Shared) {
7380 retain_global_top_k(&mut candidates, k);
7381 }
7382 continue;
7383 }
7384
7385 let mut exact_memory = ExactScanMemory::new(accounting)?;
7386 let frontier = rescore_options
7387 .map(|options| options.frontier(k, alive.live_count()))
7388 .transpose()?
7389 .unwrap_or(k);
7390 let mut outcome = scan_sealed_segment(
7391 pool,
7392 snapshot,
7393 segment,
7394 alive,
7395 generation,
7396 request,
7397 frontier,
7398 scan_options,
7399 &control,
7400 &bit4_query,
7401 &int8_query,
7402 full_precision,
7403 exact_lane,
7404 accounting,
7405 &mut exact_memory,
7406 source,
7407 options.tier(),
7408 #[cfg(any(test, feature = "test-seams"))]
7409 vector_fault_controller,
7410 )?;
7411 let mut exact_score = full_precision || segment.meta().scheme == 0;
7412 let mut approximate = false;
7413 if let Some(rescore) = rescore_options {
7414 let lease = SnapshotLease::new_at(Arc::clone(snapshot), generation);
7415 let cancellation = QueryCancellation::new(&control, &lease);
7416 let vectors = exact_rescore_rows_for_search(
7417 segment,
7418 #[cfg(any(test, feature = "test-seams"))]
7419 vector_fault_controller,
7420 #[cfg(any(test, feature = "test-seams"))]
7421 vector_fault_tier(options.tier()),
7422 )?;
7423 let (counters, exhaustive, worst) = rescored_scan::rescore(
7424 &mut outcome.candidates,
7425 &mut outcome.stats,
7426 vectors,
7427 query,
7428 k,
7429 alive.live_count(),
7430 rescore,
7431 &cancellation,
7432 accounting,
7433 &mut exact_memory,
7434 )?;
7435 outcome.worst_score = worst;
7436 exact_score = true;
7437 approximate = !exhaustive;
7438 rescored_scan::accumulate(&mut rescore_counters, counters)?;
7439 }
7440 fold_worst_squared_l2(&mut worst_squared_l2, &mut worst_exhaustive, &outcome);
7441 merge_store_outcome(
7442 outcome,
7443 source,
7444 |row| {
7445 segment
7446 .document_version(row)
7447 .map_err(StoreError::Segment)
7448 .map_err(QueryError::Store)
7449 },
7450 &mut candidates,
7451 &mut dims_touched,
7452 &mut bytes_read,
7453 &mut worker_thread_ids,
7454 exact_score,
7455 #[cfg(any(test, feature = "test-seams"))]
7456 vector_fault_controller,
7457 #[cfg(any(test, feature = "test-seams"))]
7458 vector_fault_tier(options.tier()),
7459 )?;
7460 let tier = if segment
7461 .directory()
7462 .iter()
7463 .any(|entry| entry.kind == crate::segment::layout::RegionKind::GraphNodeBlocks.id())
7464 {
7465 crate::planner::SegmentTier::SealedGraph
7466 } else {
7467 crate::planner::SegmentTier::SealedScan
7468 };
7469 let mut plan = crate::planner::SegmentPlan::unfiltered_scan(
7470 source,
7471 tier,
7472 alive.live_count(),
7473 sealed_scan_reason(segment, requested_tier, hybrid_scan_reason),
7474 );
7475 plan.approximate = approximate;
7476 plans.push(plan);
7477 if matches!(graph_bound_mode, GraphBoundMode::Shared) {
7478 retain_global_top_k(&mut candidates, k);
7479 }
7480 }
7481
7482 retain_global_top_k(&mut candidates, k);
7483 let stats = ScanStats {
7484 dims_touched,
7485 bytes_read,
7486 threads_used: worker_thread_ids.len(),
7487 worker_thread_ids,
7488 };
7489 accounting.record_plans(&plans).map_err(QueryError::Store)?;
7490 let vector_ceiling = Some(exact_vector_ceiling(snapshot, active, query)?);
7493 let mut diagnostics = crate::diag::QueryDiagnostics::vector(crate::diag::VectorDiagnostics {
7494 snapshot_generation: generation,
7495 indexed_through_seq: active
7496 .indexed_through_seq()
7497 .max(crate::wal::LogSeq::new(snapshot.absorbed_through())),
7498 approximate: plans.iter().any(|plan| plan.approximate),
7499 exact_rescore: candidates.iter().all(|candidate| candidate.exact_score()),
7500 requested_k: k,
7501 returned: candidates.len(),
7502 budget_exhausted: false,
7503 plan: plans,
7504 scan: stats.clone(),
7505 graph: graph_stats,
7506 epoch,
7507 elapsed: clock.now().saturating_duration_since(started),
7508 });
7509 diagnostics.scan_rescore = rescore_options.map(|_| rescore_counters);
7510 diagnostics.timings = crate::diag::QUERY_TIMING_ENABLED.then(|| crate::diag::QueryTimings {
7511 vector: timing_elapsed(clock, vector_started),
7512 ..Default::default()
7513 });
7514 Ok(SearchOutcome {
7515 candidates,
7516 vector_ceiling,
7521 stats,
7522 graph_stats,
7523 generation,
7524 epoch,
7525 diagnostics,
7526 })
7527}
7528
7529const GRAPH_WIDENING_CAP_NUMERATOR: usize = 1;
7537const GRAPH_WIDENING_CAP_DENOMINATOR: usize = 4;
7539
7540fn graph_round_is_worth_it(k: usize, corpus_rows: usize) -> bool {
7547 if corpus_rows == 0 {
7548 return true;
7549 }
7550 let ef = graph_round_ef(k);
7551 let cap =
7552 corpus_rows.saturating_mul(GRAPH_WIDENING_CAP_NUMERATOR) / GRAPH_WIDENING_CAP_DENOMINATOR;
7553 ef <= cap
7554}
7555
7556fn graph_round_ef(k: usize) -> usize {
7557 k.saturating_mul(4).max(140)
7558}
7559
7560fn sealed_scan_reason(
7561 segment: &crate::segment::reader::SegmentReader,
7562 requested_tier: Option<SearchTier>,
7563 hybrid_scan_reason: Option<crate::planner::ScanReason>,
7564) -> crate::planner::ScanReason {
7565 use crate::planner::{ExplicitScanTier, ScanReason};
7566
7567 let has_graph = segment
7568 .directory()
7569 .iter()
7570 .any(|entry| entry.kind == crate::segment::layout::RegionKind::GraphNodeBlocks.id());
7571 if has_graph && let Some(reason) = hybrid_scan_reason {
7572 return reason;
7573 }
7574 match requested_tier {
7575 Some(SearchTier::Exact) => return ScanReason::ExplicitTier(ExplicitScanTier::Exact),
7576 Some(SearchTier::Scan) => return ScanReason::ExplicitTier(ExplicitScanTier::Scan),
7577 Some(SearchTier::Auto | SearchTier::Graph(_)) | None => {}
7578 }
7579 let rows = segment.meta().row_count;
7580 let min_rows = crate::tier::thresholds::for_bucket(segment.meta().dims, segment.meta().scheme)
7581 .graph_min_rows;
7582 if rows < min_rows {
7583 ScanReason::BelowGraphThreshold { rows, min_rows }
7584 } else {
7585 ScanReason::GraphPending
7586 }
7587}
7588
7589fn snapshot_has_graph(snapshot: &PublishedSnapshot) -> bool {
7590 snapshot.segments().iter().any(|segment| {
7591 segment
7592 .directory()
7593 .iter()
7594 .any(|entry| entry.kind == crate::segment::layout::RegionKind::GraphNodeBlocks.id())
7595 })
7596}
7597
7598fn exact_vector_ceiling(
7599 snapshot: &PublishedSnapshot,
7600 active: &crate::ingest::ActiveSegment,
7601 query: &[f32],
7602) -> Result<f64, QueryError> {
7603 fold_exact_vector_ceiling(
7604 snapshot,
7605 active,
7606 query,
7607 remembered_segment_vector_ceiling_norm_range,
7608 )
7609}
7610
7611fn remembered_segment_vector_ceiling_norm_range(
7618 segment: &crate::segment::reader::SegmentReader,
7619) -> Result<crate::graph::search::GraphSegmentNormRange, QueryError> {
7620 if let Some(range) = segment.cached_vector_ceiling_norm_range() {
7621 return Ok(range);
7622 }
7623 let range = compute_segment_vector_ceiling_norm_range(segment)?;
7624 segment.remember_vector_ceiling_norm_range(range);
7625 Ok(range)
7626}
7627
7628fn fold_exact_vector_ceiling(
7634 snapshot: &PublishedSnapshot,
7635 active: &crate::ingest::ActiveSegment,
7636 query: &[f32],
7637 mut segment_range: impl FnMut(
7638 &crate::segment::reader::SegmentReader,
7639 )
7640 -> Result<crate::graph::search::GraphSegmentNormRange, QueryError>,
7641) -> Result<f64, QueryError> {
7642 let mut ceiling = 0.0_f64;
7643 if !active.is_empty() {
7644 ceiling = fold_vector_ceiling(
7647 ceiling,
7648 crate::graph::search::GraphSegmentNormRange::from_factors(active.factors()),
7649 query,
7650 )?;
7651 }
7652 for segment in snapshot.segments() {
7653 let range = segment_range(segment)?;
7654 ceiling = fold_vector_ceiling(ceiling, range, query)?;
7655 }
7656 Ok(ceiling)
7657}
7658
7659fn compute_segment_vector_ceiling_norm_range(
7661 segment: &crate::segment::reader::SegmentReader,
7662) -> Result<crate::graph::search::GraphSegmentNormRange, QueryError> {
7663 match segment.meta().scheme {
7664 4 => Ok(crate::graph::search::GraphSegmentNormRange::from_factors(
7665 segment
7666 .query_bit4_factors()
7667 .map_err(StoreError::Segment)
7668 .map_err(QueryError::Store)?,
7669 )),
7670 0 | 2 => Ok(
7671 crate::graph::search::GraphSegmentNormRange::from_exact_rows(
7672 segment
7673 .query_rescore_f32()
7674 .map_err(StoreError::Segment)
7675 .map_err(QueryError::Store)?,
7676 segment.meta().dims as usize,
7677 ),
7678 ),
7679 scheme => Err(QueryError::Store(StoreError::Segment(
7680 crate::segment::SegmentError::Geometry(format!(
7681 "vector norm enclosure does not support scheme {scheme}"
7682 )),
7683 ))),
7684 }
7685}
7686
7687fn fold_vector_ceiling(
7688 current: f64,
7689 range: crate::graph::search::GraphSegmentNormRange,
7690 query: &[f32],
7691) -> Result<f64, QueryError> {
7692 let ceiling = range.squared_l2_upper_bound(query);
7693 if !ceiling.is_finite() {
7694 return Err(QueryError::Store(StoreError::Segment(
7695 crate::segment::SegmentError::Geometry(
7696 "vector norm enclosure did not produce a finite squared-L2 ceiling".to_owned(),
7697 ),
7698 )));
7699 }
7700 Ok(current.max(ceiling))
7701}
7702
7703fn fold_worst_squared_l2(
7707 worst: &mut Option<f64>,
7708 exhaustive: &mut bool,
7709 outcome: &crate::scan::ScanOutcome,
7710) {
7711 match outcome.worst_score {
7712 Some(score) => {
7713 let squared_l2 = -f64::from(score);
7714 *worst = Some(worst.map_or(squared_l2, |current: f64| current.max(squared_l2)));
7715 }
7716 None => {
7717 if !outcome.candidates.is_empty() {
7718 *exhaustive = false;
7719 }
7720 }
7721 }
7722}
7723
7724#[allow(clippy::large_enum_variant)]
7726enum SegmentGraphResult<'a> {
7727 Pruned {
7728 plan: crate::planner::SegmentPlan,
7729 graph_stats: crate::ingest::GraphSearchStats,
7730 _lease: SnapshotLease,
7731 },
7732 Traversed {
7733 result: crate::graph::search::GraphSearchResult,
7734 plan: crate::planner::SegmentPlan,
7735 dims_touched: u64,
7736 bytes_read: u64,
7737 caller_thread: std::thread::ThreadId,
7738 graph_stats: crate::ingest::GraphSearchStats,
7739 _scratch: graph_cache::GraphScratchLease<'a>,
7740 _lease: SnapshotLease,
7741 },
7742}
7743
7744struct MergedSegmentGraph<'a> {
7745 plan: crate::planner::SegmentPlan,
7746 traversed: bool,
7747 _result: Option<crate::graph::search::GraphSearchResult>,
7748 _scratch: Option<graph_cache::GraphScratchLease<'a>>,
7749 _lease: SnapshotLease,
7750}
7751
7752impl<'a> SegmentGraphResult<'a> {
7753 #[allow(clippy::too_many_arguments)]
7754 fn merge_into(
7755 self,
7756 segment: &crate::segment::reader::SegmentReader,
7757 alive: &crate::meta::AliveSet,
7758 source: crate::ingest::RowSource,
7759 candidates: &mut Vec<crate::ingest::SearchCandidate>,
7760 dims_touched: &mut u64,
7761 bytes_read: &mut u64,
7762 worker_thread_ids: &mut Vec<std::thread::ThreadId>,
7763 graph_stats: &mut crate::ingest::GraphSearchStats,
7764 #[cfg(any(test, feature = "test-seams"))] vector_fault_controller: Option<
7765 &crate::scan::vector_fault::VectorFaultController,
7766 >,
7767 _tier: SearchTier,
7768 ) -> Result<MergedSegmentGraph<'a>, QueryError> {
7769 match self {
7770 Self::Pruned {
7771 plan,
7772 graph_stats: delta,
7773 _lease,
7774 } => {
7775 add_traversal_stats(graph_stats, delta)?;
7776 Ok(MergedSegmentGraph {
7777 plan,
7778 traversed: false,
7779 _result: None,
7780 _scratch: None,
7781 _lease,
7782 })
7783 }
7784 Self::Traversed {
7785 result,
7786 plan,
7787 dims_touched: traversal_dims_touched,
7788 bytes_read: traversal_bytes_read,
7789 caller_thread,
7790 graph_stats: delta,
7791 _scratch,
7792 _lease,
7793 } => {
7794 *dims_touched = dims_touched
7795 .checked_add(traversal_dims_touched)
7796 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
7797 *bytes_read = bytes_read
7798 .checked_add(traversal_bytes_read)
7799 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
7800 if !worker_thread_ids.contains(&caller_thread) {
7801 worker_thread_ids.push(caller_thread);
7802 }
7803 add_traversal_stats(graph_stats, delta)?;
7804 reserve_global_candidates(
7805 candidates,
7806 result
7807 .candidates()
7808 .iter()
7809 .filter(|candidate| alive.is_alive(candidate.row_id()))
7810 .count(),
7811 #[cfg(any(test, feature = "test-seams"))]
7812 vector_fault_controller,
7813 #[cfg(any(test, feature = "test-seams"))]
7814 vector_fault_tier(_tier),
7815 )?;
7816 for candidate in result.candidates() {
7817 if !alive.is_alive(candidate.row_id()) {
7818 continue;
7819 }
7820 let score = -(candidate.distance() as f32);
7821 if !score.is_finite() {
7822 return Err(QueryError::Graph(
7823 crate::graph::search::GraphSearchError::Geometry(format!(
7824 "graph result distance for row {} is not representable as f32",
7825 candidate.row_id()
7826 )),
7827 ));
7828 }
7829 candidates.push(crate::ingest::SearchCandidate::new(
7830 crate::ingest::GlobalRowId::new(source, candidate.row_id()),
7831 segment
7832 .document_version(candidate.row_id() as usize)
7833 .map_err(StoreError::Segment)
7834 .map_err(QueryError::Store)?,
7835 score,
7836 true,
7837 ));
7838 }
7839 Ok(MergedSegmentGraph {
7840 plan,
7841 traversed: true,
7842 _result: Some(result),
7843 _scratch: Some(_scratch),
7844 _lease,
7845 })
7846 }
7847 }
7848 }
7849}
7850
7851#[allow(clippy::too_many_arguments)]
7852fn traverse_segment_graph<'a>(
7853 segment: &'a crate::segment::reader::SegmentReader,
7854 alive: &crate::meta::AliveSet,
7855 source: crate::ingest::RowSource,
7856 graph_options: GraphSearchOptions,
7857 graph_bound_mode: GraphBoundMode,
7858 request: crate::ingest::SearchRequest<'_>,
7859 k: usize,
7860 vector_preparation: &mut prepared::PreparedVectorQuery<'_>,
7861 accounting: &Arc<stats::Accounting>,
7862 snapshot: &Arc<PublishedSnapshot>,
7863 generation: u64,
7864 control: &QueryControl,
7865 competitive_distance: Option<f32>,
7866 #[cfg(any(test, feature = "test-seams"))] vector_fault_controller: Option<
7867 &crate::scan::vector_fault::VectorFaultController,
7868 >,
7869 _tier: SearchTier,
7870) -> Result<SegmentGraphResult<'a>, QueryError> {
7871 let lease = SnapshotLease::new_at(Arc::clone(snapshot), generation);
7872 let cancellation = QueryCancellation::new(control, &lease);
7873 cancellation.check_graph().map_err(map_scan_error)?;
7874 let (graph, graph_validated, prepared_entry_seed_discovered, norm_range) =
7875 match graph_bound_mode {
7876 GraphBoundMode::Shared => {
7877 let prepared = segment
7878 .graph_search_cache
7879 .prepare_shared(segment, &cancellation)
7880 .map_err(map_graph_cache_error)?;
7881 (
7882 prepared.graph,
7883 prepared.graph_validated,
7884 prepared.entry_seed_discovered,
7885 Some(prepared.norm_range),
7886 )
7887 }
7888 #[cfg(test)]
7889 GraphBoundMode::Independent => {
7890 let (graph, graph_validated) = segment
7891 .graph_search_cache
7892 .bind_graph(segment)
7893 .map_err(map_graph_cache_error)?;
7894 (graph, graph_validated, false, None)
7895 }
7896 };
7897 let rescore = query_rescore_rows_for_search(
7898 segment,
7899 #[cfg(any(test, feature = "test-seams"))]
7900 vector_fault_controller,
7901 #[cfg(any(test, feature = "test-seams"))]
7902 vector_fault_tier(_tier),
7903 )?;
7904 let node_count = graph.node_count() as usize;
7905 let segment_k = k.min(node_count);
7906 let has_tombstones = alive.tombstone_count() != 0;
7907 let target_live_k = if has_tombstones {
7908 usize::try_from(alive.live_count())
7909 .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?
7910 .min(k)
7911 } else {
7912 segment_k
7913 };
7914 let maximum_candidate_k = if has_tombstones {
7915 let tombstone_count = usize::try_from(alive.tombstone_count())
7916 .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
7917 target_live_k
7918 .checked_add(tombstone_count)
7919 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?
7920 .min(node_count)
7921 .max(segment_k)
7922 } else {
7923 segment_k
7924 };
7925 if let Some(ef) = graph_options.ef()
7926 && ef < segment_k
7927 {
7928 return Err(QueryError::Graph(
7929 crate::graph::search::GraphSearchError::AdaptiveEf(
7930 crate::graph::search::AdaptiveEfError::ExplicitBelowK { k: segment_k, ef },
7931 ),
7932 ));
7933 }
7934 let request_for = |candidate_k| {
7935 let graph_request = crate::graph::search::GraphSearchRequest::new(
7936 request.vector(),
7937 candidate_k,
7938 graph_options.seed(),
7939 )
7940 .with_profile(graph_options.profile());
7941 graph_options.ef().map_or(graph_request, |ef| {
7946 graph_request.with_ef(ef.max(candidate_k))
7947 })
7948 };
7949 let mut candidate_k = segment_k;
7950 let mut graph_request = request_for(candidate_k);
7951 let effective_ef = graph_request
7952 .effective_ef(graph.node_count() as usize)
7953 .map_err(crate::graph::search::GraphSearchError::AdaptiveEf)
7954 .map_err(QueryError::Graph)?;
7955 let requestable_max_k = maximum_candidate_k;
7956 let scratch_ef = if has_tombstones && requestable_max_k > candidate_k {
7957 request_for(requestable_max_k)
7958 .effective_ef(node_count)
7959 .map_err(crate::graph::search::GraphSearchError::AdaptiveEf)
7960 .map_err(QueryError::Graph)?
7961 } else {
7962 effective_ef
7963 };
7964 if let (Some(norm_range), Some(competitive_distance)) = (norm_range, competitive_distance)
7965 && norm_range.squared_l2_upper_bound(request.vector()) <= f64::from(f32::MAX)
7966 && (norm_range.squared_l2_lower_bound(request.vector()) as f32) > competitive_distance
7967 {
7968 cancellation.check_graph().map_err(map_scan_error)?;
7969 let graph_stats = crate::ingest::GraphSearchStats {
7970 graph_validations: usize::from(graph_validated),
7971 entry_seed_discoveries: usize::from(prepared_entry_seed_discovered),
7972 segments_pruned_by_bound: 1,
7973 ..crate::ingest::GraphSearchStats::default()
7974 };
7975 let plan = crate::planner::SegmentPlan::unfiltered_pruned(source, alive.live_count());
7976 return Ok(SegmentGraphResult::Pruned {
7977 plan,
7978 graph_stats,
7979 _lease: lease,
7980 });
7981 }
7982 let mut scratch = segment
7983 .graph_search_cache
7984 .checkout(graph, scratch_ef, accounting, &cancellation)
7985 .map_err(map_graph_cache_error)?;
7986 let entry_seed_discovered = prepared_entry_seed_discovered || scratch.entry_seed_discovered();
7987 let entries = scratch.entries();
7988 let mut searcher = crate::graph::search::GraphSearcher::with_entry_row_ids(
7989 graph,
7990 rescore,
7991 entries,
7992 scratch.scratch_mut().map_err(map_graph_error)?,
7993 )
7994 .map_err(map_graph_error)?
7995 .with_rescore_validator(segment);
7996 #[cfg(any(test, feature = "test-seams"))]
7997 if let Some(after_hops) = graph_options.cancel_after_hops() {
7998 let token = cancellation.cancel_token_for_test().ok_or_else(|| {
7999 QueryError::Graph(crate::graph::search::GraphSearchError::Geometry(
8000 "test hop cancellation requires a Cancel query control".to_owned(),
8001 ))
8002 })?;
8003 let _observed_hops = searcher.cancel_after_hops(after_hops, token);
8004 }
8005 #[cfg(any(test, feature = "test-seams"))]
8006 if let Some(controller) = vector_fault_controller {
8007 searcher = searcher.with_vector_fault_controller(
8008 controller,
8009 vector_fault_source(source),
8010 crate::scan::vector_fault::VectorSearchTier::Graph,
8011 );
8012 }
8013 let mut traversal_dims_touched = 0_u64;
8014 let mut traversal_bytes_read = 0_u64;
8015 let mut traversal_epoch_clears = 0_usize;
8016 let mut traversal_candidates_scored = 0_usize;
8017 let mut traversal_candidates_rescored = 0_usize;
8018 let result = loop {
8019 let result = searcher.search_reusing(
8020 graph_request,
8021 Some(&cancellation),
8022 vector_preparation,
8023 segment,
8024 source,
8025 )?;
8026 let counters = result.counters();
8027 traversal_dims_touched = traversal_dims_touched
8028 .checked_add(counters.dims_touched())
8029 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8030 traversal_bytes_read = traversal_bytes_read
8031 .checked_add(counters.bytes_read())
8032 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8033 traversal_epoch_clears = traversal_epoch_clears
8034 .checked_add(usize::from(counters.visited_epoch_cleared()))
8035 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8036 traversal_candidates_scored = traversal_candidates_scored
8037 .checked_add(counters.candidates_scored())
8038 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8039 traversal_candidates_rescored = traversal_candidates_rescored
8040 .checked_add(counters.candidates_rescored())
8041 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8042
8043 if !has_tombstones {
8044 break result;
8045 }
8046 let retained_live = result
8047 .candidates()
8048 .iter()
8049 .filter(|candidate| alive.is_alive(candidate.row_id()))
8050 .count();
8051 if retained_live >= target_live_k {
8052 break result;
8053 }
8054 if candidate_k >= requestable_max_k {
8055 return Err(QueryError::Graph(
8056 crate::graph::search::GraphSearchError::Geometry(format!(
8057 "graph traversal exhausted {candidate_k} candidates but retained {retained_live} live rows, expected {target_live_k}"
8058 )),
8059 ));
8060 }
8061 let widened_k = candidate_k
8062 .saturating_mul(2)
8063 .max(candidate_k.saturating_add(1))
8064 .min(requestable_max_k);
8065 if widened_k <= candidate_k {
8066 return Err(QueryError::Graph(
8067 crate::graph::search::GraphSearchError::Geometry(format!(
8068 "graph traversal could not widen beyond {candidate_k} candidates"
8069 )),
8070 ));
8071 }
8072 candidate_k = widened_k;
8073 graph_request = request_for(candidate_k);
8074 };
8075 let graph_stats = crate::ingest::GraphSearchStats {
8076 segments_traversed: 1,
8077 graph_validations: usize::from(graph_validated),
8078 entry_seed_discoveries: usize::from(entry_seed_discovered),
8079 visited_epoch_clears: traversal_epoch_clears,
8080 candidates_scored: traversal_candidates_scored,
8081 candidates_rescored: traversal_candidates_rescored,
8082 segments_pruned_by_bound: 0,
8083 };
8084 let plan = crate::planner::SegmentPlan::unfiltered_graph(
8085 source,
8086 alive.live_count(),
8087 graph_options.ef(),
8088 effective_ef,
8089 graph_options.profile(),
8090 );
8091 let caller_thread = std::thread::current().id();
8092 #[cfg(any(test, feature = "test-seams"))]
8093 drop(searcher);
8094 Ok(SegmentGraphResult::Traversed {
8095 result,
8096 plan,
8097 dims_touched: traversal_dims_touched,
8098 bytes_read: traversal_bytes_read,
8099 caller_thread,
8100 graph_stats,
8101 _scratch: scratch,
8102 _lease: lease,
8103 })
8104}
8105
8106fn add_traversal_stats(
8107 stats: &mut crate::ingest::GraphSearchStats,
8108 delta: crate::ingest::GraphSearchStats,
8109) -> Result<(), QueryError> {
8110 stats.segments_traversed = stats
8111 .segments_traversed
8112 .checked_add(delta.segments_traversed)
8113 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8114 stats.graph_validations = stats
8115 .graph_validations
8116 .checked_add(delta.graph_validations)
8117 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8118 stats.entry_seed_discoveries = stats
8119 .entry_seed_discoveries
8120 .checked_add(delta.entry_seed_discoveries)
8121 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8122 stats.visited_epoch_clears = stats
8123 .visited_epoch_clears
8124 .checked_add(delta.visited_epoch_clears)
8125 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8126 stats.candidates_scored = stats
8127 .candidates_scored
8128 .checked_add(delta.candidates_scored)
8129 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8130 stats.candidates_rescored = stats
8131 .candidates_rescored
8132 .checked_add(delta.candidates_rescored)
8133 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8134 stats.segments_pruned_by_bound = stats
8135 .segments_pruned_by_bound
8136 .checked_add(delta.segments_pruned_by_bound)
8137 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8138 Ok(())
8139}
8140
8141#[allow(clippy::too_many_arguments)]
8142fn scan_sealed_segment(
8143 pool: Option<&pool::QueryPool>,
8144 snapshot: &Arc<PublishedSnapshot>,
8145 segment: &crate::segment::reader::SegmentReader,
8146 alive: &crate::meta::AliveSet,
8147 generation: u64,
8148 request: crate::ingest::SearchRequest<'_>,
8149 k: usize,
8150 scan_options: crate::scan::ScanOptions,
8151 control: &QueryControl,
8152 bit4_query: &std::cell::OnceCell<Result<crate::quant::Bit4Query, crate::quant::QuantError>>,
8153 int8_query: &std::cell::OnceCell<Result<crate::quant::Int8Query, crate::quant::QuantError>>,
8154 full_precision: bool,
8155 exact_lane: usize,
8156 accounting: &Arc<stats::Accounting>,
8157 exact_memory: &mut ExactScanMemory,
8158 _source: crate::ingest::RowSource,
8159 _tier: SearchTier,
8160 #[cfg(any(test, feature = "test-seams"))] vector_fault_controller: Option<
8161 &crate::scan::vector_fault::VectorFaultController,
8162 >,
8163) -> Result<crate::scan::ScanOutcome, QueryError> {
8164 use crate::scan::{ScanQuery, ScanRequest, ScanRows};
8165
8166 if full_precision {
8167 let lease = SnapshotLease::new_at(Arc::clone(snapshot), generation);
8168 let vectors = exact_rescore_rows_for_search(
8169 segment,
8170 #[cfg(any(test, feature = "test-seams"))]
8171 vector_fault_controller,
8172 #[cfg(any(test, feature = "test-seams"))]
8173 vector_fault_tier(_tier),
8174 )?;
8175 return scan_squared_l2(
8176 vectors,
8177 segment.meta().row_count as usize,
8178 alive,
8179 request.vector(),
8180 k,
8181 ExactScanDispatch {
8182 pool,
8183 accounting,
8184 control,
8185 lease: &lease,
8186 thread_budget: scan_options.thread_budget,
8187 lane: exact_lane,
8188 },
8189 exact_memory,
8190 #[cfg(any(test, feature = "test-seams"))]
8191 vector_fault_controller,
8192 #[cfg(any(test, feature = "test-seams"))]
8193 vector_fault_source(_source),
8194 #[cfg(any(test, feature = "test-seams"))]
8195 vector_fault_tier(_tier),
8196 );
8197 }
8198
8199 let query_pool = pool.ok_or(QueryError::Store(StoreError::Synchronization {
8200 component: "sealed scan-tier query pool",
8201 }))?;
8202 match segment.meta().scheme {
8203 0 => execute_store_scan(
8204 query_pool,
8205 ScanRequest {
8206 query: ScanQuery::F32(request.vector()),
8207 rows: ScanRows::F32BorrowedRowMajor(
8208 segment
8209 .query_f32_codes()
8210 .map_err(StoreError::Segment)
8211 .map_err(QueryError::Store)?,
8212 ),
8213 row_mask: Some(alive.scan_mask()),
8214 },
8215 k,
8216 scan_options,
8217 control.clone(),
8218 SnapshotLease::new_at(Arc::clone(snapshot), generation),
8219 #[cfg(any(test, feature = "test-seams"))]
8220 vector_fault_controller,
8221 #[cfg(any(test, feature = "test-seams"))]
8222 vector_fault_source(_source),
8223 #[cfg(any(test, feature = "test-seams"))]
8224 vector_fault_tier(_tier),
8225 ),
8226 4 => execute_store_scan(
8227 query_pool,
8228 ScanRequest {
8229 query: ScanQuery::Bit4(
8230 bit4_query
8231 .get_or_init(|| crate::quant::prepare_bit4_query(request.vector(), 0))
8232 .as_ref()
8233 .map_err(|error| crate::scan::ScanError::Quant(error.clone()))
8234 .map_err(QueryError::Scan)?,
8235 ),
8236 rows: ScanRows::Bit4RowMajor {
8237 codes: segment
8238 .query_bit4_codes()
8239 .map_err(StoreError::Segment)
8240 .map_err(QueryError::Store)?,
8241 factors: segment
8242 .query_bit4_factors()
8243 .map_err(StoreError::Segment)
8244 .map_err(QueryError::Store)?,
8245 },
8246 row_mask: Some(alive.scan_mask()),
8247 },
8248 k,
8249 scan_options,
8250 control.clone(),
8251 SnapshotLease::new_at(Arc::clone(snapshot), generation),
8252 #[cfg(any(test, feature = "test-seams"))]
8253 vector_fault_controller,
8254 #[cfg(any(test, feature = "test-seams"))]
8255 vector_fault_source(_source),
8256 #[cfg(any(test, feature = "test-seams"))]
8257 vector_fault_tier(_tier),
8258 ),
8259 2 => {
8260 let factors = segment.query_int8_factors().map_err(QueryError::Store)?;
8261 execute_store_scan(
8262 query_pool,
8263 ScanRequest {
8264 query: ScanQuery::Int8(
8265 int8_query
8266 .get_or_init(|| crate::quant::prepare_int8_query(request.vector()))
8267 .as_ref()
8268 .map_err(|error| crate::scan::ScanError::Quant(error.clone()))
8269 .map_err(QueryError::Scan)?,
8270 ),
8271 rows: ScanRows::Int8RowMajor {
8272 codes: segment
8273 .query_int8_codes()
8274 .map_err(StoreError::Segment)
8275 .map_err(QueryError::Store)?,
8276 factors: factors.as_slice(),
8277 },
8278 row_mask: Some(alive.scan_mask()),
8279 },
8280 k,
8281 scan_options,
8282 control.clone(),
8283 SnapshotLease::new_at(Arc::clone(snapshot), generation),
8284 #[cfg(any(test, feature = "test-seams"))]
8285 vector_fault_controller,
8286 #[cfg(any(test, feature = "test-seams"))]
8287 vector_fault_source(_source),
8288 #[cfg(any(test, feature = "test-seams"))]
8289 vector_fault_tier(_tier),
8290 )
8291 }
8292 scheme => Err(QueryError::Store(StoreError::Segment(
8293 crate::segment::SegmentError::Geometry(format!(
8294 "store search does not support sealed scheme {scheme}"
8295 )),
8296 ))),
8297 }
8298}
8299
8300#[allow(clippy::too_many_arguments)]
8301fn execute_store_scan(
8302 pool: &pool::QueryPool,
8303 request: crate::scan::ScanRequest<'_>,
8304 k: usize,
8305 options: crate::scan::ScanOptions,
8306 control: QueryControl,
8307 lease: SnapshotLease,
8308 #[cfg(any(test, feature = "test-seams"))] controller: Option<
8309 &crate::scan::vector_fault::VectorFaultController,
8310 >,
8311 #[cfg(any(test, feature = "test-seams"))] source: crate::scan::vector_fault::VectorRowSource,
8312 #[cfg(any(test, feature = "test-seams"))] tier: crate::scan::vector_fault::VectorSearchTier,
8313) -> Result<crate::scan::ScanOutcome, QueryError> {
8314 #[cfg(any(test, feature = "test-seams"))]
8315 if let Some(controller) = controller {
8316 let geometry = crate::scan::scan_geometry(request).map_err(QueryError::Scan)?;
8317 let cancellation = QueryCancellation::new(&control, &lease);
8318 let partition = crate::scan::scan_partition_with_vector_faults(
8319 request,
8320 k,
8321 0..geometry.row_count,
8322 Some(&cancellation),
8323 controller,
8324 source,
8325 tier,
8326 )
8327 .map_err(map_scan_error)?;
8328 return Ok(crate::scan::ScanOutcome {
8329 candidates: partition.candidates,
8330 worst_score: None,
8331 stats: crate::scan::ScanStats {
8332 dims_touched: partition.dims_touched,
8333 bytes_read: partition.bytes_read,
8334 threads_used: 1,
8335 worker_thread_ids: vec![partition.worker_thread_id],
8336 },
8337 });
8338 }
8339 pool.execute(request, k, options, control, lease)
8340}
8341
8342#[cfg(any(test, feature = "test-seams"))]
8343fn vector_fault_tier(tier: SearchTier) -> crate::scan::vector_fault::VectorSearchTier {
8344 match tier {
8345 SearchTier::Exact => crate::scan::vector_fault::VectorSearchTier::Exact,
8346 SearchTier::Scan => crate::scan::vector_fault::VectorSearchTier::Scan,
8347 SearchTier::Graph(_) => crate::scan::vector_fault::VectorSearchTier::Graph,
8348 SearchTier::Auto => crate::scan::vector_fault::VectorSearchTier::Auto,
8349 }
8350}
8351
8352#[cfg(any(test, feature = "test-seams"))]
8353fn vector_fault_source(
8354 source: crate::ingest::RowSource,
8355) -> crate::scan::vector_fault::VectorRowSource {
8356 match source {
8357 crate::ingest::RowSource::Active => crate::scan::vector_fault::VectorRowSource::Active,
8358 crate::ingest::RowSource::Sealed(segment) => {
8359 crate::scan::vector_fault::VectorRowSource::Sealed(*segment.as_bytes())
8360 }
8361 }
8362}
8363
8364pub(crate) fn auto_uses_full_precision(
8365 snapshot: &PublishedSnapshot,
8366 active: &crate::ingest::ActiveSegment,
8367) -> bool {
8368 let mut has_exact = false;
8369 let mut has_estimated = !active.is_empty();
8370 for segment in snapshot.segments() {
8371 if segment
8372 .directory()
8373 .iter()
8374 .any(|entry| entry.kind == crate::segment::layout::RegionKind::GraphNodeBlocks.id())
8375 {
8376 return true;
8377 }
8378 if segment.meta().row_count == 0 {
8379 continue;
8380 }
8381 match segment.meta().scheme {
8382 0 => has_exact = true,
8383 2 | 4 => has_estimated = true,
8384 _ => {}
8385 }
8386 }
8387 has_exact && has_estimated
8388}
8389
8390pub(crate) fn exact_rescore_rows(
8400 segment: &crate::segment::reader::SegmentReader,
8401) -> Result<&[f32], QueryError> {
8402 segment
8403 .query_rescore_f32()
8404 .map_err(|source| QueryError::Store(StoreError::Segment(source)))
8405}
8406
8407#[cfg(not(any(test, feature = "test-seams")))]
8408use exact_rescore_rows as exact_rescore_rows_for_search;
8409
8410#[cfg(any(test, feature = "test-seams"))]
8411fn exact_rescore_rows_for_search<'a>(
8412 segment: &'a crate::segment::reader::SegmentReader,
8413 #[cfg(any(test, feature = "test-seams"))] controller: Option<
8414 &crate::scan::vector_fault::VectorFaultController,
8415 >,
8416 #[cfg(any(test, feature = "test-seams"))] tier: crate::scan::vector_fault::VectorSearchTier,
8417) -> Result<&'a [f32], QueryError> {
8418 #[cfg(any(test, feature = "test-seams"))]
8419 if let Some(available_rows) = controller.and_then(|controller| {
8420 controller.missing_rescore_rows(
8421 crate::scan::vector_fault::VectorRowSource::Sealed(*segment.meta().id.as_bytes()),
8422 tier,
8423 crate::scan::vector_fault::MissingRescoreSite::ExactRescoreRows,
8424 segment.meta().row_count,
8425 )
8426 }) {
8427 return Err(missing_rescore_rows_error(segment, available_rows));
8428 }
8429 exact_rescore_rows(segment)
8430}
8431
8432pub(crate) fn query_rescore_rows(
8433 segment: &crate::segment::reader::SegmentReader,
8434) -> Result<&[f32], QueryError> {
8435 segment
8436 .query_rescore_f32()
8437 .map_err(|source| QueryError::Store(StoreError::Segment(source)))
8438}
8439
8440#[cfg(not(any(test, feature = "test-seams")))]
8441use query_rescore_rows as query_rescore_rows_for_search;
8442
8443#[cfg(any(test, feature = "test-seams"))]
8444fn query_rescore_rows_for_search<'a>(
8445 segment: &'a crate::segment::reader::SegmentReader,
8446 #[cfg(any(test, feature = "test-seams"))] controller: Option<
8447 &crate::scan::vector_fault::VectorFaultController,
8448 >,
8449 #[cfg(any(test, feature = "test-seams"))] tier: crate::scan::vector_fault::VectorSearchTier,
8450) -> Result<&'a [f32], QueryError> {
8451 #[cfg(any(test, feature = "test-seams"))]
8452 if let Some(available_rows) = controller.and_then(|controller| {
8453 controller.missing_rescore_rows(
8454 crate::scan::vector_fault::VectorRowSource::Sealed(*segment.meta().id.as_bytes()),
8455 tier,
8456 crate::scan::vector_fault::MissingRescoreSite::QueryRescoreRows,
8457 segment.meta().row_count,
8458 )
8459 }) {
8460 return Err(missing_rescore_rows_error(segment, available_rows));
8461 }
8462 query_rescore_rows(segment)
8463}
8464
8465#[cfg(any(test, feature = "test-seams"))]
8466fn missing_rescore_rows_error(
8467 segment: &crate::segment::reader::SegmentReader,
8468 available_rows: u32,
8469) -> QueryError {
8470 QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Geometry(
8471 format!(
8472 "exact scores unavailable for segment {}: expected {} rows, got {available_rows}",
8473 segment.meta().id,
8474 segment.meta().row_count,
8475 ),
8476 )))
8477}
8478
8479fn retain_global_top_k(candidates: &mut Vec<crate::ingest::SearchCandidate>, k: usize) {
8480 candidates.sort_unstable_by(crate::ingest::compare_search_candidates);
8481 candidates.truncate(k);
8482}
8483
8484fn global_competitive_distance(
8485 candidates: &[crate::ingest::SearchCandidate],
8486 k: usize,
8487) -> Option<f32> {
8488 if k == 0 || candidates.len() < k {
8489 return None;
8490 }
8491 candidates
8492 .get(k.saturating_sub(1))
8493 .map(|candidate| -candidate.score())
8494 .filter(|distance| distance.is_finite() && *distance >= 0.0)
8495}
8496
8497fn map_graph_cache_error(error: graph_cache::GraphCacheError) -> QueryError {
8498 match error {
8499 graph_cache::GraphCacheError::Store(error) => QueryError::Store(error),
8500 graph_cache::GraphCacheError::Search(error) => map_graph_error(error),
8501 }
8502}
8503
8504pub(crate) fn map_graph_error(error: crate::graph::search::GraphSearchError) -> QueryError {
8505 match error {
8506 crate::graph::search::GraphSearchError::ExactRescoreUnavailable(detail) => {
8507 QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Geometry(
8508 detail,
8509 )))
8510 }
8511 crate::graph::search::GraphSearchError::Cancelled { partial } => {
8512 QueryError::Cancelled { partial }
8513 }
8514 crate::graph::search::GraphSearchError::Timeout { partial } => {
8515 QueryError::Timeout { partial }
8516 }
8517 crate::graph::search::GraphSearchError::ReadCancelled { partial } => {
8518 QueryError::ReadCancelled { partial }
8519 }
8520 crate::graph::search::GraphSearchError::Scan(error) => QueryError::Scan(error),
8521 error => QueryError::Graph(error),
8522 }
8523}
8524
8525#[allow(
8526 clippy::too_many_arguments,
8527 reason = "scoped dispatch and test source identity stay explicit"
8528)]
8529fn scan_active_squared_l2(
8530 active: &crate::ingest::ActiveSegment,
8531 alive: &crate::meta::AliveSet,
8532 query: &[f32],
8533 k: usize,
8534 dispatch: ExactScanDispatch<'_>,
8535 memory: &mut ExactScanMemory,
8536 #[cfg(any(test, feature = "test-seams"))] controller: Option<
8537 &crate::scan::vector_fault::VectorFaultController,
8538 >,
8539 #[cfg(any(test, feature = "test-seams"))] tier: crate::scan::vector_fault::VectorSearchTier,
8540) -> Result<crate::scan::ScanOutcome, QueryError> {
8541 scan_squared_l2(
8542 active.vectors(),
8543 active.row_count(),
8544 alive,
8545 query,
8546 k,
8547 dispatch,
8548 memory,
8549 #[cfg(any(test, feature = "test-seams"))]
8550 controller,
8551 #[cfg(any(test, feature = "test-seams"))]
8552 crate::scan::vector_fault::VectorRowSource::Active,
8553 #[cfg(any(test, feature = "test-seams"))]
8554 tier,
8555 )
8556}
8557
8558#[derive(Debug)]
8559enum ExactScanError {
8560 Query(QueryError),
8561 Narrowing { row_id: usize, score: f64 },
8562}
8563
8564impl From<QueryError> for ExactScanError {
8565 fn from(error: QueryError) -> Self {
8566 Self::Query(error)
8567 }
8568}
8569
8570impl ExactScanError {
8571 fn into_query(self) -> QueryError {
8572 match self {
8573 Self::Query(error) => error,
8574 Self::Narrowing { row_id, .. } => {
8575 QueryError::Scan(crate::scan::ScanError::NonFiniteScore { row_id })
8576 }
8577 }
8578 }
8579}
8580
8581pub(crate) struct ExactScanMemory {
8582 candidates: stats::AccountedCounter,
8583 worker_ids: stats::AccountedCounter,
8584}
8585
8586impl ExactScanMemory {
8587 pub(crate) fn new(accounting: &Arc<stats::Accounting>) -> Result<Self, QueryError> {
8588 Ok(Self {
8589 candidates: stats::AccountedCounter::new(
8590 accounting,
8591 stats::AllocationComponent::Temporary,
8592 )
8593 .map_err(QueryError::Store)?,
8594 worker_ids: stats::AccountedCounter::new(
8595 accounting,
8596 stats::AllocationComponent::Temporary,
8597 )
8598 .map_err(QueryError::Store)?,
8599 })
8600 }
8601}
8602
8603struct ExactScanDispatch<'a> {
8604 pool: Option<&'a pool::QueryPool>,
8605 accounting: &'a Arc<stats::Accounting>,
8606 control: &'a QueryControl,
8607 lease: &'a SnapshotLease,
8608 thread_budget: usize,
8609 lane: usize,
8610}
8611
8612#[allow(
8613 clippy::too_many_arguments,
8614 reason = "scoped dispatch and test source identity stay explicit"
8615)]
8616fn scan_squared_l2(
8617 vectors: &[f32],
8618 row_count: usize,
8619 alive: &crate::meta::AliveSet,
8620 query: &[f32],
8621 k: usize,
8622 dispatch: ExactScanDispatch<'_>,
8623 memory: &mut ExactScanMemory,
8624 #[cfg(any(test, feature = "test-seams"))] controller: Option<
8625 &crate::scan::vector_fault::VectorFaultController,
8626 >,
8627 #[cfg(any(test, feature = "test-seams"))] source: crate::scan::vector_fault::VectorRowSource,
8628 #[cfg(any(test, feature = "test-seams"))] tier: crate::scan::vector_fault::VectorSearchTier,
8629) -> Result<crate::scan::ScanOutcome, QueryError> {
8630 if let Some(pool) = dispatch.pool
8631 && pool.exact_workers(row_count, query.len(), dispatch.thread_budget) > 1
8632 {
8633 return pool.execute_exact(
8634 vectors,
8635 row_count,
8636 alive,
8637 query,
8638 k,
8639 dispatch.thread_budget,
8640 dispatch.lane,
8641 dispatch.control,
8642 dispatch.lease,
8643 dispatch.accounting,
8644 memory,
8645 #[cfg(any(test, feature = "test-seams"))]
8646 controller,
8647 #[cfg(any(test, feature = "test-seams"))]
8648 source,
8649 #[cfg(any(test, feature = "test-seams"))]
8650 tier,
8651 );
8652 }
8653 let cancellation = QueryCancellation::new(dispatch.control, dispatch.lease);
8654 #[cfg(any(test, feature = "test-seams"))]
8655 if controller.is_some_and(|controller| controller.has_exact_row_hooks()) {
8656 return scan_exact_partition::<false>(
8657 vectors,
8658 row_count,
8659 0..row_count,
8660 alive,
8661 query,
8662 k,
8663 &cancellation,
8664 memory,
8665 #[cfg(any(test, feature = "test-seams"))]
8666 controller,
8667 #[cfg(any(test, feature = "test-seams"))]
8668 source,
8669 #[cfg(any(test, feature = "test-seams"))]
8670 tier,
8671 )
8672 .map_err(ExactScanError::into_query);
8673 }
8674 scan_serial_squared_l2(
8675 vectors,
8676 row_count,
8677 alive,
8678 query,
8679 k,
8680 &cancellation,
8681 &mut memory.candidates,
8682 &mut memory.worker_ids,
8683 #[cfg(any(test, feature = "test-seams"))]
8684 controller,
8685 #[cfg(any(test, feature = "test-seams"))]
8686 source,
8687 #[cfg(any(test, feature = "test-seams"))]
8688 tier,
8689 )
8690}
8691
8692#[allow(clippy::too_many_arguments)]
8693fn scan_exact_partition<const OFFSET_ROWS: bool>(
8694 vectors: &[f32],
8695 row_count: usize,
8696 range: std::ops::Range<usize>,
8697 alive: &crate::meta::AliveSet,
8698 query: &[f32],
8699 k: usize,
8700 cancellation: &QueryCancellation<'_>,
8701 memory: &mut ExactScanMemory,
8702 #[cfg(any(test, feature = "test-seams"))] controller: Option<
8703 &crate::scan::vector_fault::VectorFaultController,
8704 >,
8705 #[cfg(any(test, feature = "test-seams"))] source: crate::scan::vector_fault::VectorRowSource,
8706 #[cfg(any(test, feature = "test-seams"))] tier: crate::scan::vector_fault::VectorSearchTier,
8707) -> Result<crate::scan::ScanOutcome, ExactScanError> {
8708 if query.is_empty() {
8709 return Err(QueryError::Scan(crate::scan::ScanError::ZeroDimension).into());
8710 }
8711 let expected = row_count
8712 .checked_mul(query.len())
8713 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8714 if vectors.len() != expected {
8715 return Err(QueryError::Scan(crate::scan::ScanError::RowDataLength {
8716 dimension: query.len(),
8717 actual: vectors.len(),
8718 })
8719 .into());
8720 }
8721 if range.start > range.end || range.end > row_count || (!OFFSET_ROWS && range.start != 0) {
8724 return Err(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow).into());
8725 }
8726 let mut live_rows = 0;
8727 for row in range.clone() {
8728 if row.is_multiple_of(64) {
8729 cancellation.check_graph().map_err(map_scan_error)?;
8730 }
8731 let local_row = u32::try_from(row)
8732 .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8733 live_rows += usize::from(alive.is_alive(local_row));
8734 }
8735 cancellation.check_graph().map_err(map_scan_error)?;
8736 let mut collector = crate::scan::topk::ExactTopK::try_new(k, live_rows, &mut memory.candidates)
8737 .map_err(QueryError::Store)?;
8738 let mut worst_score: Option<f32> = None;
8739 let mut narrowing_error: Option<(usize, f64)> = None;
8740 let start = range
8741 .start
8742 .checked_mul(query.len())
8743 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8744 let end = range
8745 .end
8746 .checked_mul(query.len())
8747 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8748 let partition_vectors = vectors
8749 .get(start..end)
8750 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8751 let rows = partition_vectors
8752 .chunks_exact(query.len())
8753 .enumerate()
8754 .map(|(local, vector)| {
8755 (
8756 if OFFSET_ROWS {
8757 range.start + local
8758 } else {
8759 local
8760 },
8761 vector,
8762 )
8763 })
8764 .filter(|(row, _)| u32::try_from(*row).is_ok_and(|row| alive.is_alive(row)));
8765 #[cfg(any(test, feature = "test-seams"))]
8766 let mut trace =
8767 controller.and_then(|controller| controller.trace_rows(source, tier, range.clone()));
8768 let scored_rows = crate::quant::exact_squared_l2_with_sink(
8769 query,
8770 rows,
8771 |_row, is_checkpoint| {
8772 #[cfg(any(test, feature = "test-seams"))]
8773 if let Some(controller) = controller {
8774 if let Some(trace) = &mut trace {
8775 trace.checked(_row);
8776 }
8777 if is_checkpoint {
8778 cancellation.check_graph().map_err(map_scan_error)?;
8779 }
8780 if controller.after_eligible_row(source, tier, _row) {
8781 return Err(QueryError::Cancelled { partial: false });
8782 }
8783 return Ok(());
8784 }
8785 if is_checkpoint {
8786 cancellation.check_graph().map_err(map_scan_error)?;
8787 }
8788 Ok(())
8789 },
8790 |row_id, exact| {
8791 let score = exact as f32;
8792 if !score.is_finite() {
8793 if narrowing_error.is_none_or(|(row, previous)| {
8797 exact.total_cmp(&previous).is_gt()
8798 || (exact.total_cmp(&previous).is_eq() && row_id < row)
8799 }) {
8800 narrowing_error = Some((row_id, exact));
8801 }
8802 return Ok(());
8803 }
8804 if worst_score.is_none_or(|worst| score.total_cmp(&worst).is_lt()) {
8805 worst_score = Some(score);
8806 }
8807 collector
8808 .try_push(
8809 crate::scan::ScanCandidate { row_id, score },
8810 &mut memory.candidates,
8811 )
8812 .map_err(QueryError::Store)
8813 },
8814 )
8815 .map_err(|error| match error {
8816 crate::quant::RescoreCheckError::Rescore(error) => map_l2_rescore_error(error),
8817 crate::quant::RescoreCheckError::Check(error) => error,
8818 })?;
8819 cancellation.check_graph().map_err(map_scan_error)?;
8820 if let Some((row_id, score)) = narrowing_error {
8821 return Err(ExactScanError::Narrowing { row_id, score });
8822 }
8823 let (candidates, _collector_capacity) = collector
8824 .try_into_sorted_with_ties(&mut memory.candidates)
8825 .map_err(QueryError::Store)?;
8826 #[cfg(any(test, feature = "test-seams"))]
8827 if let Some(controller) = controller {
8828 controller.record_exact_scan(crate::scan::vector_fault::ExactScanWork {
8829 sorted_items: candidates.len(),
8830 collector_capacity: _collector_capacity,
8831 scored_rows,
8832 worst_score,
8833 ..Default::default()
8834 });
8835 }
8836 let dims = u64::try_from(query.len())
8837 .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8838 let scored_rows = u64::try_from(scored_rows)
8839 .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8840 let dims_touched = scored_rows
8841 .checked_mul(dims)
8842 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8843 let bytes_read = dims_touched
8844 .checked_mul(std::mem::size_of::<f32>() as u64)
8845 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8846 memory
8847 .worker_ids
8848 .set(std::mem::size_of::<std::thread::ThreadId>())
8849 .map_err(QueryError::Store)?;
8850 let mut worker_thread_ids = Vec::new();
8851 worker_thread_ids.try_reserve_exact(1).map_err(|_| {
8852 QueryError::Store(StoreError::AllocationFailed {
8853 needed: memory.worker_ids.bytes(),
8854 component: "exact worker IDs",
8855 })
8856 })?;
8857 worker_thread_ids.push(std::thread::current().id());
8858 Ok(crate::scan::ScanOutcome {
8859 candidates,
8860 worst_score,
8861 stats: crate::scan::ScanStats {
8862 dims_touched,
8863 bytes_read,
8864 threads_used: 1,
8865 worker_thread_ids,
8866 },
8867 })
8868}
8869
8870#[allow(clippy::too_many_arguments)]
8873fn scan_serial_squared_l2(
8874 vectors: &[f32],
8875 row_count: usize,
8876 alive: &crate::meta::AliveSet,
8877 query: &[f32],
8878 k: usize,
8879 cancellation: &QueryCancellation<'_>,
8880 memory: &mut stats::AccountedCounter,
8881 worker_memory: &mut stats::AccountedCounter,
8882 #[cfg(any(test, feature = "test-seams"))] controller: Option<
8883 &crate::scan::vector_fault::VectorFaultController,
8884 >,
8885 #[cfg(any(test, feature = "test-seams"))] source: crate::scan::vector_fault::VectorRowSource,
8886 #[cfg(any(test, feature = "test-seams"))] tier: crate::scan::vector_fault::VectorSearchTier,
8887) -> Result<crate::scan::ScanOutcome, QueryError> {
8888 if query.is_empty() {
8889 return Err(QueryError::Scan(crate::scan::ScanError::ZeroDimension));
8890 }
8891 let expected = row_count
8892 .checked_mul(query.len())
8893 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8894 if vectors.len() != expected {
8895 return Err(QueryError::Scan(crate::scan::ScanError::RowDataLength {
8896 dimension: query.len(),
8897 actual: vectors.len(),
8898 }));
8899 }
8900 let mut live_rows = 0;
8903 for row in 0..row_count {
8904 if row.is_multiple_of(64) {
8905 cancellation.check_graph().map_err(map_scan_error)?;
8906 }
8907 let local_row = u32::try_from(row)
8908 .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8909 live_rows += usize::from(alive.is_alive(local_row));
8910 }
8911 cancellation.check_graph().map_err(map_scan_error)?;
8912 let mut collector =
8913 crate::scan::topk::ExactTopK::try_new(k, live_rows, memory).map_err(QueryError::Store)?;
8914 let mut worst_score: Option<f32> = None;
8915 let mut narrowing_error: Option<(usize, f64)> = None;
8916 let rows = vectors
8917 .chunks_exact(query.len())
8918 .enumerate()
8919 .filter(|(row, _)| u32::try_from(*row).is_ok_and(|row| alive.is_alive(row)));
8920 let scored_rows = crate::quant::exact_squared_l2_with_sink(
8921 query,
8922 rows,
8923 |_row, is_checkpoint| {
8924 if is_checkpoint {
8925 cancellation.check_graph().map_err(map_scan_error)?;
8926 }
8927 #[cfg(any(test, feature = "test-seams"))]
8928 if controller
8929 .is_some_and(|controller| controller.after_eligible_row(source, tier, _row))
8930 {
8931 return Err(QueryError::Cancelled { partial: false });
8932 }
8933 Ok(())
8934 },
8935 |row_id, exact| {
8936 let score = exact as f32;
8937 if !score.is_finite() {
8938 if narrowing_error.is_none_or(|(row, previous)| {
8942 exact.total_cmp(&previous).is_gt()
8943 || (exact.total_cmp(&previous).is_eq() && row_id < row)
8944 }) {
8945 narrowing_error = Some((row_id, exact));
8946 }
8947 return Ok(());
8948 }
8949 if worst_score.is_none_or(|worst| score.total_cmp(&worst).is_lt()) {
8950 worst_score = Some(score);
8951 }
8952 collector
8953 .try_push(crate::scan::ScanCandidate { row_id, score }, memory)
8954 .map_err(QueryError::Store)
8955 },
8956 )
8957 .map_err(|error| match error {
8958 crate::quant::RescoreCheckError::Rescore(error) => map_l2_rescore_error(error),
8959 crate::quant::RescoreCheckError::Check(error) => error,
8960 })?;
8961 cancellation.check_graph().map_err(map_scan_error)?;
8962 if let Some((row_id, _)) = narrowing_error {
8963 return Err(QueryError::Scan(crate::scan::ScanError::NonFiniteScore {
8964 row_id,
8965 }));
8966 }
8967 let (candidates, _collector_capacity) = collector
8968 .try_into_sorted_with_ties(memory)
8969 .map_err(QueryError::Store)?;
8970 #[cfg(any(test, feature = "test-seams"))]
8971 if let Some(controller) = controller {
8972 controller.record_exact_scan(crate::scan::vector_fault::ExactScanWork {
8973 sorted_items: candidates.len(),
8974 collector_capacity: _collector_capacity,
8975 scored_rows,
8976 worst_score,
8977 ..Default::default()
8978 });
8979 }
8980 let dims = u64::try_from(query.len())
8981 .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8982 let scored_rows = u64::try_from(scored_rows)
8983 .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8984 let dims_touched = scored_rows
8985 .checked_mul(dims)
8986 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8987 let bytes_read = dims_touched
8988 .checked_mul(std::mem::size_of::<f32>() as u64)
8989 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
8990 worker_memory
8991 .set(std::mem::size_of::<std::thread::ThreadId>())
8992 .map_err(QueryError::Store)?;
8993 let mut worker_thread_ids = Vec::new();
8994 worker_thread_ids.try_reserve_exact(1).map_err(|_| {
8995 QueryError::Store(StoreError::AllocationFailed {
8996 needed: worker_memory.bytes(),
8997 component: "exact worker IDs",
8998 })
8999 })?;
9000 worker_thread_ids.push(std::thread::current().id());
9001 Ok(crate::scan::ScanOutcome {
9002 candidates,
9003 worst_score,
9004 stats: crate::scan::ScanStats {
9005 dims_touched,
9006 bytes_read,
9007 threads_used: 1,
9008 worker_thread_ids,
9009 },
9010 })
9011}
9012
9013fn map_l2_rescore_error(error: crate::quant::RescoreError) -> QueryError {
9014 let error = match error {
9015 crate::quant::RescoreError::ZeroDimension => crate::scan::ScanError::ZeroDimension,
9016 crate::quant::RescoreError::QueryDimension { expected, actual }
9017 | crate::quant::RescoreError::CoarseScoreCount { expected, actual }
9018 | crate::quant::RescoreError::CandidateRowCount { expected, actual } => {
9019 crate::scan::ScanError::RowDataLength {
9020 dimension: expected,
9021 actual,
9022 }
9023 }
9024 crate::quant::RescoreError::RowDataLength { dimension, actual } => {
9025 crate::scan::ScanError::RowDataLength { dimension, actual }
9026 }
9027 crate::quant::RescoreError::CandidateRowOutOfRange {
9028 row_index,
9029 row_count,
9030 ..
9031 } => crate::scan::ScanError::RowDataLength {
9032 dimension: row_count,
9033 actual: row_index,
9034 },
9035 crate::quant::RescoreError::NonFiniteCoarseScore { index }
9036 | crate::quant::RescoreError::NonFiniteExactScore { row_index: index } => {
9037 crate::scan::ScanError::NonFiniteScore { row_id: index }
9038 }
9039 crate::quant::RescoreError::ZeroK
9040 | crate::quant::RescoreError::ZeroOversample
9041 | crate::quant::RescoreError::InsufficientCandidates { .. }
9042 | crate::quant::RescoreError::ArithmeticOverflow => {
9043 crate::scan::ScanError::ArithmeticOverflow
9044 }
9045 error @ (crate::quant::RescoreError::ZeroCandidateLimit
9046 | crate::quant::RescoreError::CandidateLimitExceeded { .. }) => {
9047 crate::scan::ScanError::Rescore(error)
9048 }
9049 };
9050 QueryError::Scan(error)
9051}
9052
9053fn map_scan_error(error: crate::scan::ScanError) -> QueryError {
9054 match error {
9055 crate::scan::ScanError::Cancelled { partial } => QueryError::Cancelled { partial },
9056 crate::scan::ScanError::Timeout { partial } => QueryError::Timeout { partial },
9057 crate::scan::ScanError::ReadCancelled { partial } => QueryError::ReadCancelled { partial },
9058 error => QueryError::Scan(error),
9059 }
9060}
9061
9062#[allow(clippy::too_many_arguments)]
9063fn merge_store_outcome(
9064 outcome: crate::scan::ScanOutcome,
9065 source: crate::ingest::RowSource,
9066 document: impl Fn(usize) -> Result<Option<crate::ingest::DocumentVersion>, QueryError>,
9067 candidates: &mut Vec<crate::ingest::SearchCandidate>,
9068 dims_touched: &mut u64,
9069 bytes_read: &mut u64,
9070 worker_thread_ids: &mut Vec<std::thread::ThreadId>,
9071 exact_score: bool,
9072 #[cfg(any(test, feature = "test-seams"))] vector_fault_controller: Option<
9073 &crate::scan::vector_fault::VectorFaultController,
9074 >,
9075 #[cfg(any(test, feature = "test-seams"))]
9076 vector_fault_tier: crate::scan::vector_fault::VectorSearchTier,
9077) -> Result<(), QueryError> {
9078 *dims_touched = dims_touched
9079 .checked_add(outcome.stats.dims_touched)
9080 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
9081 *bytes_read = bytes_read
9082 .checked_add(outcome.stats.bytes_read)
9083 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
9084 for worker in outcome.stats.worker_thread_ids {
9085 if !worker_thread_ids.contains(&worker) {
9086 worker_thread_ids.push(worker);
9087 }
9088 }
9089 reserve_global_candidates(
9090 candidates,
9091 outcome.candidates.len(),
9092 #[cfg(any(test, feature = "test-seams"))]
9093 vector_fault_controller,
9094 #[cfg(any(test, feature = "test-seams"))]
9095 vector_fault_tier,
9096 )?;
9097 for candidate in outcome.candidates {
9098 let local_row = u32::try_from(candidate.row_id)
9099 .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
9100 candidates.push(crate::ingest::SearchCandidate::new(
9101 crate::ingest::GlobalRowId::new(source, local_row),
9102 document(candidate.row_id)?,
9103 candidate.score,
9104 exact_score,
9105 ));
9106 }
9107 Ok(())
9108}
9109
9110pub(crate) fn reserve_global_candidates(
9111 candidates: &mut Vec<crate::ingest::SearchCandidate>,
9112 additional: usize,
9113 #[cfg(any(test, feature = "test-seams"))] vector_fault_controller: Option<
9114 &crate::scan::vector_fault::VectorFaultController,
9115 >,
9116 #[cfg(any(test, feature = "test-seams"))]
9117 vector_fault_tier: crate::scan::vector_fault::VectorSearchTier,
9118) -> Result<(), QueryError> {
9119 if additional == 0 {
9120 return Ok(());
9121 }
9122 let items = u64::try_from(additional)
9123 .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
9124 let candidate_bytes = u64::try_from(std::mem::size_of::<crate::ingest::SearchCandidate>())
9125 .map_err(|_| QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
9126 let needed = items
9127 .checked_mul(candidate_bytes)
9128 .ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))?;
9129 #[cfg(any(test, feature = "test-seams"))]
9130 if vector_fault_controller.is_some_and(|controller| {
9131 controller.deny_global_candidate_allocation(vector_fault_tier, items, needed)
9132 }) {
9133 return Err(QueryError::Store(StoreError::AllocationFailed {
9134 needed,
9135 component: "vector search global candidates",
9136 }));
9137 }
9138 candidates.try_reserve_exact(additional).map_err(|_| {
9139 QueryError::Store(StoreError::AllocationFailed {
9140 needed,
9141 component: "vector search global candidates",
9142 })
9143 })
9144}
9145
9146struct AdmittedLexicalQuery<'a> {
9147 generation: u64,
9148 active: Arc<crate::ingest::ActiveSegment>,
9149 snapshot: snapshot::ReadSnapshot,
9150 active_query: ActiveQuery<'a>,
9151}
9152
9153struct AdmittedDocumentRead<'a> {
9154 generation: u64,
9155 active: Arc<crate::ingest::ActiveSegment>,
9156 snapshot: snapshot::ReadSnapshot,
9157 active_query: ActiveQuery<'a>,
9158}
9159
9160struct AdmittedVectorSearch<'a> {
9161 execution_options: SearchOptions,
9162 pool: Option<Arc<pool::QueryPool>>,
9163 snapshot: snapshot::ReadSnapshot,
9164 active_segment: Arc<crate::ingest::ActiveSegment>,
9165 generation: u64,
9166 _active_query: Option<ActiveQuery<'a>>,
9167}
9168
9169struct ActiveQuery<'a> {
9170 count: &'a AtomicU64,
9171}
9172
9173impl Drop for ActiveQuery<'_> {
9174 fn drop(&mut self) {
9175 self.count.fetch_sub(1, Ordering::Relaxed);
9176 }
9177}
9178
9179impl Drop for Store {
9180 fn drop(&mut self) {
9181 self.close_best_effort();
9182 }
9183}
9184
9185#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9186struct DocumentScanRow {
9187 source: DocumentScanSource,
9188 row: u32,
9189 timestamp: i64,
9190 doc_id: crate::ingest::DocId,
9191 attribute_key: Option<u64>,
9194}
9195
9196fn validate_scan_order_attribute(
9197 schema: &crate::meta::Schema,
9198 column: crate::meta::ColumnId,
9199) -> Result<(), QueryError> {
9200 use crate::meta::ColumnType;
9201
9202 let id = column.get();
9203 let detail = if column == crate::meta::TIMESTAMP_COLUMN {
9204 Some(format!(
9205 "scan order attribute {id} is the document timestamp; use a timestamp order"
9206 ))
9207 } else {
9208 match schema
9209 .column(column)
9210 .map(crate::meta::ColumnDefinition::column_type)
9211 {
9212 None => Some(format!(
9213 "scan order attribute {id} is not a declared attribute"
9214 )),
9215 Some(ColumnType::U64 | ColumnType::I64 | ColumnType::F64) => None,
9216 Some(other) => Some(format!(
9217 "scan order attribute {id} has type {other:?}; only u64, i64 and f64 attributes are orderable"
9218 )),
9219 }
9220 };
9221 detail.map_or(Ok(()), |detail| {
9222 Err(QueryError::Store(StoreError::InvalidScan { detail }))
9223 })
9224}
9225
9226fn attribute_order_key(column: &crate::meta::Column, row: u32) -> Result<Option<u64>, QueryError> {
9231 use crate::meta::Column;
9232 const SIGN: u64 = 1 << 63;
9233
9234 match column {
9235 Column::U64(values) => Ok(values.get(row)),
9236 Column::I64(values) => Ok(values.get(row).map(|value| value.cast_unsigned() ^ SIGN)),
9237 Column::F64(values) => Ok(values
9238 .get(row)
9239 .filter(|value| !value.is_nan())
9240 .map(|value| {
9241 let bits = (value + 0.0).to_bits();
9244 if bits & SIGN == 0 { bits | SIGN } else { !bits }
9245 })),
9246 Column::Id128(_) | Column::Bool(_) | Column::DictionaryString(_) | Column::RawString(_) => {
9247 Err(QueryError::Store(StoreError::InvalidScan {
9248 detail: "scan order attribute column is not numeric".to_owned(),
9249 }))
9250 }
9251 }
9252}
9253
9254fn keyed_scan_row(
9255 mut row: DocumentScanRow,
9256 columns: Option<(&crate::meta::ColumnStore, crate::meta::ColumnId)>,
9257) -> Result<DocumentScanRow, QueryError> {
9258 if let Some((columns, column)) = columns {
9259 let values = columns.column(column).ok_or_else(|| {
9260 QueryError::Store(StoreError::InvalidScan {
9261 detail: format!("scan order attribute {} has no stored column", column.get()),
9262 })
9263 })?;
9264 row.attribute_key = attribute_order_key(values, row.row)?;
9265 }
9266 Ok(row)
9267}
9268
9269fn document_scan_predicate(
9270 predicate: Option<&crate::meta::Predicate>,
9271 timestamp_range: Option<(i64, i64)>,
9272) -> Result<Option<crate::meta::Predicate>, QueryError> {
9273 use crate::meta::{Predicate, PredicateValue, RangeBound, RangePredicate, TIMESTAMP_COLUMN};
9274
9275 let timestamp = timestamp_range
9276 .map(|(start, end)| {
9277 if start > end {
9278 return Err(QueryError::Store(StoreError::InvalidScan {
9279 detail: format!("timestamp range start {start} exceeds end {end}"),
9280 }));
9281 }
9282 Ok(Predicate::Range(RangePredicate {
9283 column: TIMESTAMP_COLUMN,
9284 lower: Some(RangeBound::inclusive(PredicateValue::I64(start))),
9285 upper: Some(RangeBound::exclusive(PredicateValue::I64(end))),
9286 }))
9287 })
9288 .transpose()?;
9289 Ok(match (predicate, timestamp) {
9290 (Some(predicate), Some(timestamp)) => {
9291 Some(Predicate::And(vec![predicate.clone(), timestamp]))
9292 }
9293 (Some(predicate), None) => Some(predicate.clone()),
9294 (None, Some(timestamp)) => Some(timestamp),
9295 (None, None) => None,
9296 })
9297}
9298
9299fn scan_control_checkpoint(
9300 control: &QueryControl,
9301 lease: &SnapshotLease,
9302 work: &mut usize,
9303) -> Result<(), QueryError> {
9304 *work = work.saturating_add(1);
9305 if (*work).is_multiple_of(64) {
9306 control.checkpoint()?;
9307 lease.check_active().map_err(QueryError::Store)?;
9308 }
9309 Ok(())
9310}
9311
9312fn active_scan_columns(
9313 active: &crate::ingest::ActiveSegment,
9314 schema: &crate::meta::Schema,
9315) -> Result<crate::meta::ColumnStore, QueryError> {
9316 let mut builder = crate::meta::ColumnStoreBuilder::new(schema.clone());
9317 for (row, timestamp) in active.timestamps().iter().copied().enumerate() {
9318 let values = active.column_values(row).map_err(QueryError::Store)?;
9319 builder
9320 .push_row(timestamp, &crate::ingest::column_inputs(&values))
9321 .map_err(|error| {
9322 QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Columns(
9323 error.to_string(),
9324 )))
9325 })?;
9326 }
9327 builder.finish().map_err(|error| {
9328 QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Columns(
9329 error.to_string(),
9330 )))
9331 })
9332}
9333
9334fn sealed_scan_rows(
9335 segment: &crate::segment::reader::SegmentReader,
9336 predicate: Option<&crate::meta::Predicate>,
9337) -> Result<crate::meta::DocBitmap, QueryError> {
9338 if predicate.is_some_and(|predicate| {
9339 !crate::planner::segment_may_match(segment.meta().clustering_key_range, predicate)
9340 }) {
9341 return Ok(crate::meta::DocBitmap::new());
9342 }
9343 let alive = segment.query_alive().map_err(QueryError::Store)?;
9344 match predicate {
9345 Some(predicate) => {
9346 let columns = segment.query_columns().map_err(QueryError::Store)?;
9347 crate::meta::evaluate(predicate, &columns, &alive).map_err(|error| {
9348 QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Columns(
9349 error.to_string(),
9350 )))
9351 })
9352 }
9353 None => Ok(alive.alive_bitmap().clone()),
9354 }
9355}
9356
9357fn active_scan_rows(
9358 active: &crate::ingest::ActiveSegment,
9359 schema: &crate::meta::Schema,
9360 predicate: Option<&crate::meta::Predicate>,
9361) -> Result<crate::meta::DocBitmap, QueryError> {
9362 let alive = active.alive().map_err(QueryError::Store)?;
9363 match predicate {
9364 Some(predicate) => {
9365 let columns = active_scan_columns(active, schema)?;
9366 crate::meta::evaluate(predicate, &columns, &alive).map_err(|error| {
9367 QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Columns(
9368 error.to_string(),
9369 )))
9370 })
9371 }
9372 None => Ok(alive.alive_bitmap().clone()),
9373 }
9374}
9375
9376fn sealed_document_scan_row(
9377 segment: &crate::segment::reader::SegmentReader,
9378 row: u32,
9379) -> Result<DocumentScanRow, QueryError> {
9380 let version = segment
9381 .document_version(row as usize)
9382 .map_err(StoreError::Segment)
9383 .map_err(QueryError::Store)?
9384 .ok_or_else(|| {
9385 QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Geometry(
9386 format!("sealed document row {row} has no identity"),
9387 )))
9388 })?;
9389 let columns = segment.query_columns().map_err(QueryError::Store)?;
9390 let timestamp = columns.timestamp(row).ok_or_else(|| {
9391 QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Geometry(
9392 format!("sealed document row {row} has no timestamp"),
9393 )))
9394 })?;
9395 Ok(DocumentScanRow {
9396 source: DocumentScanSource::Sealed(segment.meta().id),
9397 row,
9398 timestamp,
9399 doc_id: version.doc_id(),
9400 attribute_key: None,
9401 })
9402}
9403
9404fn active_document_scan_row(
9405 active: &crate::ingest::ActiveSegment,
9406 row: u32,
9407) -> Result<DocumentScanRow, QueryError> {
9408 let index = row as usize;
9409 let version = active
9410 .document(index)
9411 .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
9412 let timestamp = active
9413 .timestamps()
9414 .get(index)
9415 .copied()
9416 .ok_or(QueryError::Store(StoreError::ActiveRowOverflow))?;
9417 Ok(DocumentScanRow {
9418 source: DocumentScanSource::Active,
9419 row,
9420 timestamp,
9421 doc_id: version.doc_id(),
9422 attribute_key: None,
9423 })
9424}
9425
9426#[allow(clippy::too_many_arguments)]
9427fn collect_storage_scan_rows(
9428 snapshot: &PublishedSnapshot,
9429 active: &crate::ingest::ActiveSegment,
9430 schema: &crate::meta::Schema,
9431 predicate: Option<&crate::meta::Predicate>,
9432 cursor: Option<DocumentScanCursor>,
9433 limit: usize,
9434 control: &QueryControl,
9435 lease: &SnapshotLease,
9436 work: &mut usize,
9437) -> Result<Vec<DocumentScanRow>, QueryError> {
9438 let capacity = limit.saturating_add(1);
9439 let mut result = Vec::new();
9440 result.try_reserve_exact(capacity).map_err(|_| {
9441 QueryError::Store(StoreError::AllocationFailed {
9442 needed: allocation_bytes::<DocumentScanRow>(capacity),
9443 component: "scan positions",
9444 })
9445 })?;
9446 let mut started = cursor.is_none();
9447 'segments: for segment in snapshot.segments() {
9448 let source = DocumentScanSource::Sealed(segment.meta().id);
9449 let rows = sealed_scan_rows(segment, predicate)?;
9450 for row in rows.iter() {
9451 scan_control_checkpoint(control, lease, work)?;
9452 if !started {
9453 started =
9454 cursor.is_some_and(|cursor| cursor.source == source && cursor.next_row == row);
9455 if !started {
9456 continue;
9457 }
9458 }
9459 result.push(sealed_document_scan_row(segment, row)?);
9460 if result.len() == capacity {
9461 break 'segments;
9462 }
9463 }
9464 }
9465 if result.len() < capacity {
9466 let source = DocumentScanSource::Active;
9467 let rows = active_scan_rows(active, schema, predicate)?;
9468 for row in rows.iter() {
9469 scan_control_checkpoint(control, lease, work)?;
9470 if !started {
9471 started =
9472 cursor.is_some_and(|cursor| cursor.source == source && cursor.next_row == row);
9473 if !started {
9474 continue;
9475 }
9476 }
9477 result.push(active_document_scan_row(active, row)?);
9478 if result.len() == capacity {
9479 break;
9480 }
9481 }
9482 }
9483 if !started {
9484 return Err(QueryError::Store(StoreError::InvalidScan {
9485 detail: "scan cursor does not name a matching row".to_owned(),
9486 }));
9487 }
9488 Ok(result)
9489}
9490
9491fn document_scan_row_cmp(
9492 left: &DocumentScanRow,
9493 right: &DocumentScanRow,
9494 order: ScanOrder,
9495) -> std::cmp::Ordering {
9496 let primary = match order {
9497 ScanOrder::Storage | ScanOrder::TimestampAscending => left.timestamp.cmp(&right.timestamp),
9498 ScanOrder::TimestampDescending => right.timestamp.cmp(&left.timestamp),
9499 ScanOrder::Attribute { direction, .. } => match (left.attribute_key, right.attribute_key) {
9500 (Some(left), Some(right)) => match direction {
9501 ScanDirection::Ascending => left.cmp(&right),
9502 ScanDirection::Descending => right.cmp(&left),
9503 },
9504 (Some(_), None) => std::cmp::Ordering::Less,
9505 (None, Some(_)) => std::cmp::Ordering::Greater,
9506 (None, None) => std::cmp::Ordering::Equal,
9507 },
9508 };
9509 primary
9510 .then_with(|| left.doc_id.cmp(&right.doc_id))
9511 .then_with(|| document_scan_source_cmp(left.source, right.source))
9512 .then_with(|| left.row.cmp(&right.row))
9513}
9514
9515fn document_scan_source_cmp(
9516 left: DocumentScanSource,
9517 right: DocumentScanSource,
9518) -> std::cmp::Ordering {
9519 match (left, right) {
9520 (DocumentScanSource::Sealed(left), DocumentScanSource::Sealed(right)) => left.cmp(&right),
9521 (DocumentScanSource::Sealed(_), DocumentScanSource::Active) => std::cmp::Ordering::Less,
9522 (DocumentScanSource::Active, DocumentScanSource::Sealed(_)) => std::cmp::Ordering::Greater,
9523 (DocumentScanSource::Active, DocumentScanSource::Active) => std::cmp::Ordering::Equal,
9524 }
9525}
9526
9527fn insert_timestamp_scan_row(
9528 rows: &mut Vec<DocumentScanRow>,
9529 candidate: DocumentScanRow,
9530 capacity: usize,
9531 order: ScanOrder,
9532) {
9533 let position = rows.partition_point(|row| {
9534 document_scan_row_cmp(row, &candidate, order) != std::cmp::Ordering::Greater
9535 });
9536 if position < capacity {
9537 rows.insert(position, candidate);
9538 if rows.len() > capacity {
9539 let _ = rows.pop();
9540 }
9541 }
9542}
9543
9544fn clustering_range_cmp(
9545 left: crate::segment::ClusteringKeyRange,
9546 right: crate::segment::ClusteringKeyRange,
9547 order: ScanOrder,
9548) -> std::cmp::Ordering {
9549 use crate::segment::ClusteringKeyRange::{Bounded, Empty, Unstamped};
9550
9551 match (left, right) {
9552 (Bounded { min_ts: left, .. }, Bounded { min_ts: right, .. })
9553 if order == ScanOrder::TimestampAscending =>
9554 {
9555 left.cmp(&right)
9556 }
9557 (Bounded { max_ts: left, .. }, Bounded { max_ts: right, .. }) => right.cmp(&left),
9558 (Unstamped, Unstamped) | (Empty, Empty) => std::cmp::Ordering::Equal,
9559 (Unstamped, _) => std::cmp::Ordering::Less,
9560 (_, Unstamped) => std::cmp::Ordering::Greater,
9561 (Empty, _) => std::cmp::Ordering::Greater,
9562 (_, Empty) => std::cmp::Ordering::Less,
9563 }
9564}
9565
9566#[allow(clippy::too_many_arguments)]
9570fn collect_sorted_scan_rows(
9571 snapshot: &PublishedSnapshot,
9572 active: &crate::ingest::ActiveSegment,
9573 schema: &crate::meta::Schema,
9574 predicate: Option<&crate::meta::Predicate>,
9575 cursor: Option<DocumentScanCursor>,
9576 limit: usize,
9577 order: ScanOrder,
9578 control: &QueryControl,
9579 lease: &SnapshotLease,
9580 work: &mut usize,
9581) -> Result<Vec<DocumentScanRow>, QueryError> {
9582 let capacity = limit.saturating_add(1);
9583 let mut result = Vec::new();
9584 result.try_reserve_exact(capacity).map_err(|_| {
9585 QueryError::Store(StoreError::AllocationFailed {
9586 needed: allocation_bytes::<DocumentScanRow>(capacity),
9587 component: "scan positions",
9588 })
9589 })?;
9590 let attribute = match order {
9591 ScanOrder::Attribute { column, .. } => Some(column),
9592 ScanOrder::Storage | ScanOrder::TimestampAscending | ScanOrder::TimestampDescending => None,
9593 };
9594 let active_columns = attribute
9595 .map(|_| active_scan_columns(active, schema))
9596 .transpose()?;
9597 let active_keys = active_columns.as_ref().zip(attribute);
9598 let sealed_columns = |segment: &crate::segment::reader::SegmentReader| {
9599 attribute
9600 .map(|_| segment.query_columns().map_err(QueryError::Store))
9601 .transpose()
9602 };
9603 let cursor_row = cursor
9604 .map(|cursor| match cursor.source {
9605 DocumentScanSource::Active => keyed_scan_row(
9606 active_document_scan_row(active, cursor.next_row)?,
9607 active_keys,
9608 ),
9609 DocumentScanSource::Sealed(segment_id) => {
9610 let segment = snapshot
9611 .segments()
9612 .iter()
9613 .find(|segment| segment.meta().id == segment_id)
9614 .ok_or_else(|| {
9615 QueryError::Store(StoreError::InvalidScan {
9616 detail: format!("scan cursor names absent segment {segment_id}"),
9617 })
9618 })?;
9619 let columns = sealed_columns(segment)?;
9620 keyed_scan_row(
9621 sealed_document_scan_row(segment, cursor.next_row)?,
9622 columns.as_deref().zip(attribute),
9623 )
9624 }
9625 })
9626 .transpose()?;
9627 let mut segments = snapshot.segments().iter().collect::<Vec<_>>();
9628 segments.sort_by(|left, right| {
9629 clustering_range_cmp(
9630 left.meta().clustering_key_range,
9631 right.meta().clustering_key_range,
9632 order,
9633 )
9634 .then_with(|| left.meta().id.cmp(&right.meta().id))
9635 });
9636 for segment in segments {
9637 let rows = sealed_scan_rows(segment, predicate)?;
9638 let columns = sealed_columns(segment)?;
9639 for row in rows.iter() {
9640 scan_control_checkpoint(control, lease, work)?;
9641 let candidate = keyed_scan_row(
9642 sealed_document_scan_row(segment, row)?,
9643 columns.as_deref().zip(attribute),
9644 )?;
9645 if cursor_row.as_ref().is_none_or(|cursor_row| {
9646 document_scan_row_cmp(&candidate, cursor_row, order) != std::cmp::Ordering::Less
9647 }) {
9648 insert_timestamp_scan_row(&mut result, candidate, capacity, order);
9649 }
9650 }
9651 }
9652 for row in active_scan_rows(active, schema, predicate)?.iter() {
9653 scan_control_checkpoint(control, lease, work)?;
9654 let candidate = keyed_scan_row(active_document_scan_row(active, row)?, active_keys)?;
9655 if cursor_row.as_ref().is_none_or(|cursor_row| {
9656 document_scan_row_cmp(&candidate, cursor_row, order) != std::cmp::Ordering::Less
9657 }) {
9658 insert_timestamp_scan_row(&mut result, candidate, capacity, order);
9659 }
9660 }
9661 if let Some(cursor) = cursor {
9662 let starts_at_cursor = result
9663 .first()
9664 .is_some_and(|row| row.source == cursor.source && row.row == cursor.next_row);
9665 if !starts_at_cursor {
9666 return Err(QueryError::Store(StoreError::InvalidScan {
9667 detail: "scan cursor does not name a matching row".to_owned(),
9668 }));
9669 }
9670 }
9671 Ok(result)
9672}
9673
9674fn materialize_active_document(
9675 active: &crate::ingest::ActiveSegment,
9676 row: usize,
9677 fields: DocumentFields,
9678) -> Result<StoredDocument, StoreError> {
9679 let version = active.document(row).ok_or(StoreError::ActiveRowOverflow)?;
9680 let timestamp = active
9681 .timestamps()
9682 .get(row)
9683 .copied()
9684 .ok_or(StoreError::ActiveRowOverflow)?;
9685 let vector = if fields.contains(DocumentFields::VECTOR) {
9686 let dimensions = active.dims().ok_or(StoreError::ActiveRowOverflow)?;
9687 let start = row
9688 .checked_mul(dimensions)
9689 .ok_or(StoreError::ActiveRowOverflow)?;
9690 let end = start
9691 .checked_add(dimensions)
9692 .ok_or(StoreError::ActiveRowOverflow)?;
9693 let values = active
9694 .vectors()
9695 .get(start..end)
9696 .ok_or(StoreError::ActiveRowOverflow)?;
9697 Some(copy_slice(values, "get vector")?)
9698 } else {
9699 None
9700 };
9701 let text = if fields.contains(DocumentFields::TEXT) {
9702 active
9703 .text(row)?
9704 .map(|value| copy_string(value, "get text"))
9705 .transpose()?
9706 } else {
9707 None
9708 };
9709 let metadata = if fields.contains(DocumentFields::METADATA) {
9710 active
9711 .metadata(row)
9712 .map(|value| copy_slice(value, "get metadata"))
9713 .transpose()?
9714 } else {
9715 None
9716 };
9717 let attributes = if fields.contains(DocumentFields::ATTRIBUTES) {
9718 Some(active.column_values(row)?)
9719 } else {
9720 None
9721 };
9722 Ok(StoredDocument {
9723 doc_id: version.doc_id(),
9724 revision: version.revision(),
9725 timestamp,
9726 vector,
9727 text,
9728 metadata,
9729 attributes,
9730 })
9731}
9732
9733fn materialize_sealed_document(
9734 segment: &crate::segment::reader::SegmentReader,
9735 row: usize,
9736 fields: DocumentFields,
9737) -> Result<StoredDocument, StoreError> {
9738 let row_u32 = u32::try_from(row).map_err(|_| {
9739 StoreError::Segment(crate::segment::SegmentError::Geometry(
9740 "sealed document row exceeds u32".to_owned(),
9741 ))
9742 })?;
9743 let version = segment
9744 .document_version(row)
9745 .map_err(StoreError::Segment)?
9746 .ok_or_else(|| {
9747 StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
9748 "sealed document row {row} has no identity"
9749 )))
9750 })?;
9751 let columns = segment.query_columns()?;
9752 let timestamp = columns.timestamp(row_u32).ok_or_else(|| {
9753 StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
9754 "sealed document row {row} has no timestamp"
9755 )))
9756 })?;
9757 let vector = if fields.contains(DocumentFields::VECTOR) {
9758 segment
9759 .validate_rescore_rows(&[row_u32])
9760 .map_err(StoreError::Segment)?;
9761 let dimensions = segment.meta().dims as usize;
9762 let start = row.checked_mul(dimensions).ok_or_else(|| {
9763 StoreError::Segment(crate::segment::SegmentError::Geometry(
9764 "sealed vector row offset overflow".to_owned(),
9765 ))
9766 })?;
9767 let end = start.checked_add(dimensions).ok_or_else(|| {
9768 StoreError::Segment(crate::segment::SegmentError::Geometry(
9769 "sealed vector row end overflow".to_owned(),
9770 ))
9771 })?;
9772 let rescore = segment.query_rescore_f32().map_err(StoreError::Segment)?;
9773 let values = rescore.get(start..end).ok_or_else(|| {
9774 StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
9775 "sealed vector row {row} is missing"
9776 )))
9777 })?;
9778 Some(copy_slice(values, "get vector")?)
9779 } else {
9780 None
9781 };
9782 let text = if fields.contains(DocumentFields::TEXT) {
9783 match segment.query_stored_text().map_err(StoreError::Segment)? {
9784 Some(rows) => rows
9785 .row(row)
9786 .ok_or_else(|| {
9787 StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
9788 "stored-text row {row} is invalid"
9789 )))
9790 })?
9791 .map(|value| copy_string(value, "get text"))
9792 .transpose()?,
9793 None => None,
9794 }
9795 } else {
9796 None
9797 };
9798 let metadata = if fields.contains(DocumentFields::METADATA) {
9799 match segment
9800 .query_stored_metadata()
9801 .map_err(StoreError::Segment)?
9802 {
9803 Some(rows) => Some(copy_slice(
9804 rows.row(row).ok_or_else(|| {
9805 StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
9806 "stored-metadata row {row} is invalid"
9807 )))
9808 })?,
9809 "get metadata",
9810 )?),
9811 None => None,
9812 }
9813 } else {
9814 None
9815 };
9816 let attributes = fields
9817 .contains(DocumentFields::ATTRIBUTES)
9818 .then(|| materialize_sealed_attributes(&columns, row_u32))
9819 .transpose()?;
9820 Ok(StoredDocument {
9821 doc_id: version.doc_id(),
9822 revision: version.revision(),
9823 timestamp,
9824 vector,
9825 text,
9826 metadata,
9827 attributes,
9828 })
9829}
9830
9831fn materialize_sealed_attributes(
9832 columns: &crate::meta::ColumnStore,
9833 row: u32,
9834) -> Result<Vec<(crate::meta::ColumnId, crate::meta::PredicateValue)>, StoreError> {
9835 let mut attributes = Vec::new();
9836 attributes
9837 .try_reserve_exact(columns.schema().user_column_count())
9838 .map_err(|_| StoreError::AllocationFailed {
9839 needed: allocation_bytes::<(crate::meta::ColumnId, crate::meta::PredicateValue)>(
9840 columns.schema().user_column_count(),
9841 ),
9842 component: "get attributes",
9843 })?;
9844 for definition in columns
9845 .schema()
9846 .columns()
9847 .iter()
9848 .filter(|definition| definition.id() != crate::meta::TIMESTAMP_COLUMN)
9849 {
9850 let column = columns.column(definition.id()).ok_or_else(|| {
9851 StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
9852 "sealed attribute column {} is missing",
9853 definition.id().get()
9854 )))
9855 })?;
9856 let value = predicate_value_for_row(column, row)?;
9857 if value.is_none() && !definition.is_nullable() {
9858 return Err(StoreError::Segment(crate::segment::SegmentError::Geometry(
9859 format!(
9860 "sealed required attribute {} is null at row {row}",
9861 definition.id().get()
9862 ),
9863 )));
9864 }
9865 if let Some(value) = value {
9866 attributes.push((definition.id(), value));
9867 }
9868 }
9869 Ok(attributes)
9870}
9871
9872fn predicate_value_for_row(
9873 column: &crate::meta::Column,
9874 row: u32,
9875) -> Result<Option<crate::meta::PredicateValue>, StoreError> {
9876 use crate::meta::{Column, PredicateValue};
9877
9878 match column {
9879 Column::Id128(values) => Ok(values.get(row).map(PredicateValue::Id128)),
9880 Column::U64(values) => Ok(values.get(row).map(PredicateValue::U64)),
9881 Column::I64(values) => Ok(values.get(row).map(PredicateValue::I64)),
9882 Column::F64(values) => Ok(values.get(row).map(PredicateValue::F64)),
9883 Column::Bool(values) => Ok(values.get(row).map(PredicateValue::Bool)),
9884 Column::DictionaryString(values) => values
9885 .get(row)
9886 .map(|value| copy_string(value, "get attribute string").map(PredicateValue::String))
9887 .transpose(),
9888 Column::RawString(values) => values
9889 .get(row)
9890 .map(|value| copy_string(value, "get attribute string").map(PredicateValue::String))
9891 .transpose(),
9892 }
9893}
9894
9895fn copy_slice<T: Copy>(values: &[T], component: &'static str) -> Result<Vec<T>, StoreError> {
9896 let mut copied = Vec::new();
9897 copied
9898 .try_reserve_exact(values.len())
9899 .map_err(|_| StoreError::AllocationFailed {
9900 needed: allocation_bytes::<T>(values.len()),
9901 component,
9902 })?;
9903 copied.extend_from_slice(values);
9904 Ok(copied)
9905}
9906
9907fn copy_string(value: &str, component: &'static str) -> Result<String, StoreError> {
9908 let mut copied = String::new();
9909 copied
9910 .try_reserve_exact(value.len())
9911 .map_err(|_| StoreError::AllocationFailed {
9912 needed: u64::try_from(value.len()).unwrap_or(u64::MAX),
9913 component,
9914 })?;
9915 copied.push_str(value);
9916 Ok(copied)
9917}
9918
9919fn allocation_bytes<T>(count: usize) -> u64 {
9920 count
9921 .checked_mul(std::mem::size_of::<T>())
9922 .and_then(|bytes| u64::try_from(bytes).ok())
9923 .unwrap_or(u64::MAX)
9924}
9925
9926#[cfg(test)]
9927mod lexical_assembly_tests;
9928
9929#[cfg(test)]
9930mod get_tests;
9931#[cfg(test)]
9932mod group_count_tests;
9933#[cfg(test)]
9934mod scan_attribute_order_tests;
9935#[cfg(test)]
9936mod scan_document_tests;
9937
9938#[cfg(test)]
9939#[allow(clippy::expect_used, clippy::panic)]
9940mod tests {
9941 use std::error::Error;
9942 use std::sync::Arc;
9943
9944 use tempfile::tempdir;
9945
9946 use crate::lifecycle::durability::DurabilityPolicy;
9947 use crate::manifest::io::commit_manifest;
9948 use crate::manifest::{Manifest, ManifestError};
9949 use crate::meta::Schema;
9950 use crate::vfs::StdVfs;
9951 #[cfg(feature = "graph-cypher")]
9952 use crate::vfs::Vfs;
9953 use crate::vfs::crash::MemoryVfs;
9954
9955 use super::durability::{CommitTier, DurabilityMode, DurabilityPolicyError};
9956 use super::{
9957 CancelToken, GraphSearchOptions, ManualMonotonicClock, OpenOptions, QueryControl,
9958 SearchOptions, SearchTier, Store, StoreError, StoreTestDependencies,
9959 resolve_hybrid_leg_results,
9960 };
9961
9962 #[cfg(feature = "graph-cypher")]
9963 #[test]
9964 fn graph_free_initial_manifest_bytes_survive_a_failed_writer_open_as_on_main() {
9965 use super::native_graph::tests::publication::{FaultPoint, RecordingVfs};
9966 let directory = tempfile::tempdir().expect("fixture");
9967 let schema = crate::meta::Schema::new(vec![crate::meta::ColumnDefinition::new(
9968 crate::meta::ColumnId::new(1),
9969 "extra",
9970 crate::meta::ColumnType::U64,
9971 true,
9972 )])
9973 .expect("fixture");
9974 let vfs = Arc::new(RecordingVfs::default());
9975 vfs.arm_fault(FaultPoint::OpenAppend);
9976 assert!(
9977 Store::open_with_test_dependencies(
9978 directory.path(),
9979 OpenOptions::default().with_schema(schema.clone()),
9980 StoreTestDependencies::new(vfs.clone(), Arc::new(super::SystemMonotonicClock)),
9981 )
9982 .is_err()
9983 );
9984 vfs.assert_fired_once();
9985 let expected = crate::manifest::encode_manifest(&crate::manifest::Manifest {
9986 graph: None,
9987 generation: 0,
9988 log_seq: 0,
9989 segments: Vec::new(),
9990 epochs: Vec::new(),
9991 epoch_alias: None,
9992 schema,
9993 })
9994 .expect("fixture");
9995 assert_eq!(
9996 StdVfs
9997 .read(&directory.path().join("manifest.ze"))
9998 .expect("main publishes the initial manifest before opening the writer"),
9999 expected
10000 );
10001 }
10002
10003 #[cfg(feature = "graph-cypher")]
10004 #[test]
10005 fn graph_free_schema_evolution_bytes_survive_a_failed_writer_open_as_on_main() {
10006 use super::native_graph::tests::publication::{FaultPoint, RecordingVfs};
10007 let directory = tempdir().expect("directory");
10008 let store = Store::open(directory.path(), OpenOptions::default()).expect("store");
10009 store
10010 .ingest(crate::ingest::IngestBatch::new(vec![
10011 crate::ingest::IngestDocument::new(
10012 crate::ingest::DocumentVersion::new(
10013 crate::ingest::DocId::new(91),
10014 crate::ingest::Revision::new(1),
10015 ),
10016 vec![1.0],
10017 ),
10018 ]))
10019 .expect("document");
10020 store.seal().expect("seal");
10021 store.close().expect("close");
10022 let schema = Schema::new(vec![crate::meta::ColumnDefinition::new(
10023 crate::meta::ColumnId::new(1),
10024 "extra",
10025 crate::meta::ColumnType::U64,
10026 true,
10027 )])
10028 .expect("schema");
10029 let mut expected = crate::manifest::io::load_manifest(
10030 &StdVfs,
10031 &directory.path().join("manifest.ze"),
10032 u64::MAX,
10033 )
10034 .expect("prior manifest");
10035 expected.generation += 1;
10036 expected.schema = schema.clone();
10037 let snapshot = || {
10038 std::fs::read_dir(directory.path())
10039 .expect("files")
10040 .map(|entry| {
10041 let entry = entry.expect("entry");
10042 (
10043 entry.file_name(),
10044 StdVfs.read(&entry.path()).expect("bytes"),
10045 )
10046 })
10047 .collect::<std::collections::BTreeMap<_, _>>()
10048 };
10049 let mut expected_files = snapshot();
10050 expected_files.insert(
10051 std::ffi::OsString::from("manifest.ze"),
10052 crate::manifest::encode_manifest(&expected).expect("main bytes"),
10053 );
10054 let vfs = Arc::new(RecordingVfs::default());
10055 vfs.arm_fault(FaultPoint::OpenAppend);
10056 assert!(
10057 Store::open_with_test_dependencies(
10058 directory.path(),
10059 OpenOptions::default().with_schema(schema),
10060 StoreTestDependencies::new(vfs.clone(), Arc::new(super::SystemMonotonicClock)),
10061 )
10062 .is_err()
10063 );
10064 vfs.assert_fired_once();
10065 assert_eq!(
10066 crate::manifest::io::load_manifest(
10067 &StdVfs,
10068 &directory.path().join("manifest.ze"),
10069 u64::MAX
10070 )
10071 .expect("published schema")
10072 .generation,
10073 expected.generation,
10074 "main commits schema before opening the writer"
10075 );
10076 assert_eq!(snapshot(), expected_files);
10077 }
10078
10079 #[cfg(feature = "graph-cypher")]
10080 #[test]
10081 pub(super) fn replacement_snapshot_backup_reopens_with_nonzero_watermarks() {
10082 use crate::ingest::{DocId, DocumentVersion, IngestBatch, IngestDocument, Revision};
10083 use crate::meta::{AliveSet, ColumnStoreBuilder, Schema};
10084 let directory = tempdir().expect("source");
10085 let parent = tempdir().expect("backup parent");
10086 let options =
10087 OpenOptions::new().with_durability(DurabilityMode::Durable, CommitTier::Durable);
10088 let store = Store::open(directory.path(), options.clone()).expect("open source");
10089 assert_eq!(store.enable_graph().expect("enable graph"), 1);
10090 let document = |id| {
10091 IngestDocument::new(
10092 DocumentVersion::new(DocId::new(id), Revision::new(1)),
10093 vec![1.0, 0.0],
10094 )
10095 };
10096 let ack = store
10097 .ingest(IngestBatch::new(vec![document(1)]))
10098 .expect("ingest");
10099 assert_eq!(ack.seq().get(), 1);
10100 assert_eq!(store.seal().expect("seal"), 3);
10101 let columns = ColumnStoreBuilder::new(Schema::timestamp_only())
10102 .finish()
10103 .expect("columns");
10104 let alive = AliveSet::new(0);
10105 let prepared = store
10106 .prepare_segment(super::InMemorySegment {
10107 id: crate::segment::SegmentId::new(1, [2; 10]),
10108 scheme: 4,
10109 dims: 2,
10110 codes: Vec::new(),
10111 factors: super::InMemorySegmentFactors::Bit4(Vec::new()),
10112 rescore: Vec::new(),
10113 columns: &columns,
10114 alive: &alive,
10115 })
10116 .expect("prepare empty replacement");
10117 assert_eq!(store.seal_snapshot(prepared).expect("replace snapshot"), 4);
10118 let target = parent.path().join("backup");
10119 assert_eq!(
10120 store.write_snapshot(&target).expect("acknowledge backup"),
10121 4
10122 );
10123 for access in [super::AccessMode::ReadOnly, super::AccessMode::ReadWrite] {
10125 let backup = Store::open(&target, options.clone().with_access_mode(access))
10126 .expect("acknowledged replacement backup must open");
10127 assert_eq!(backup.snapshot().expect("snapshot").generation(), 4);
10128 assert_eq!(backup.count_documents(None, None).expect("count").count, 0);
10129 assert!(backup.admit_native_read().is_ok());
10130 backup.close().expect("close backup");
10131 }
10132 let manifest = crate::manifest::io::load_manifest(&StdVfs, &target.join("manifest.ze"), 1)
10133 .expect("backup manifest");
10134 assert_eq!(manifest.log_seq, 1);
10135 assert_eq!(
10136 manifest
10137 .graph
10138 .expect("graph section")
10139 .graph_absorbed_through,
10140 1
10141 );
10142 let backup = Store::open(&target, options.clone()).expect("writable backup");
10143 let ack = backup
10144 .ingest(IngestBatch::new(vec![document(2)]))
10145 .expect("continue backup WAL");
10146 assert_eq!(ack.seq().get(), 2);
10147 assert_eq!(ack.generation(), 5);
10148 backup.close().expect("close continued backup");
10149 store.close().expect("close source");
10150 let source = Store::open(directory.path(), options).expect("reopen replacement source");
10151 assert_eq!(
10152 source
10153 .count_documents(None, None)
10154 .expect("source count")
10155 .count,
10156 0
10157 );
10158 assert_eq!(source.snapshot().expect("source snapshot").generation(), 4);
10159 }
10160
10161 #[cfg(feature = "graph-cypher")]
10162 #[test]
10163 fn enable_graph_retry_completes_a_failed_install() {
10164 enable_graph_retry_completes(
10165 super::native_graph::tests::publication::FaultPoint::Enumeration,
10166 );
10167 }
10168
10169 #[cfg(feature = "graph-cypher")]
10170 #[test]
10171 fn enable_graph_retry_completes_a_failed_directory_sync() {
10172 enable_graph_retry_completes(
10173 super::native_graph::tests::publication::FaultPoint::SelectorSync,
10174 );
10175 }
10176
10177 #[cfg(feature = "graph-cypher")]
10178 #[test]
10179 fn enable_graph_retry_survives_a_failed_writer_reopen() {
10180 enable_graph_retry_with_writer_reopen_failure(
10181 super::native_graph::tests::publication::FaultPoint::Enumeration,
10182 true,
10183 );
10184 }
10185
10186 #[cfg(feature = "graph-cypher")]
10187 pub(super) fn enable_graph_retry_completes(
10188 point: super::native_graph::tests::publication::FaultPoint,
10189 ) {
10190 enable_graph_retry_with_writer_reopen_failure(point, false)
10191 }
10192
10193 #[cfg(feature = "graph-cypher")]
10194 fn enable_graph_retry_with_writer_reopen_failure(
10195 point: super::native_graph::tests::publication::FaultPoint,
10196 fail_writer_reopen: bool,
10197 ) {
10198 use super::native_graph::tests::publication::{DurabilityEvent, RecordingVfs};
10199 let directory = tempdir().expect("directory");
10200 let vfs = Arc::new(RecordingVfs::default());
10201 let store = Store::open_with_test_dependencies(
10202 directory.path(),
10203 OpenOptions::new().with_max_resident_bytes(256 * 1024 * 1024),
10204 StoreTestDependencies::new(vfs.clone(), Arc::new(super::SystemMonotonicClock)),
10205 )
10206 .expect("open");
10207 let wal = StdVfs.read(&directory.path().join("wal.ze")).expect("WAL");
10208 vfs.arm_fault(point);
10209 assert!(store.enable_graph().is_err());
10210 vfs.assert_fired_once();
10211 assert!(
10212 store
10213 .ingest(crate::ingest::IngestBatch::new(vec![
10214 crate::ingest::IngestDocument::new(
10215 crate::ingest::DocumentVersion::new(
10216 crate::ingest::DocId::new(99),
10217 crate::ingest::Revision::new(1)
10218 ),
10219 vec![1.0, 0.0],
10220 )
10221 ]))
10222 .is_err(),
10223 "failed enable must fence document writes until the retry completes"
10224 );
10225 if fail_writer_reopen {
10226 vfs.arm_fault(super::native_graph::tests::publication::FaultPoint::OpenAppend);
10227 assert!(store.enable_graph().is_err());
10228 vfs.assert_fired_once();
10229 }
10230 vfs.clear_events();
10231 assert_eq!(store.enable_graph().expect("retry completes upgrade"), 1);
10232 assert_eq!(store.snapshot().expect("snapshot").generation(), 1);
10233 assert!(
10234 store.admit_native_read().is_ok(),
10235 "retry must install the bundle"
10236 );
10237 assert!(
10238 vfs.take().iter().any(|event| matches!(event,
10239 DurabilityEvent::Sync(path, crate::vfs::SyncKind::Full) if path == directory.path()
10240 )),
10241 "retry must complete the directory durability barrier"
10242 );
10243 assert_eq!(
10244 StdVfs.read(&directory.path().join("wal.ze")).expect("WAL"),
10245 wal
10246 );
10247 assert_eq!(store.enable_graph().expect("idempotent"), 1);
10248 let ack = store
10249 .ingest(crate::ingest::IngestBatch::new(vec![
10250 crate::ingest::IngestDocument::new(
10251 crate::ingest::DocumentVersion::new(
10252 crate::ingest::DocId::new(91),
10253 crate::ingest::Revision::new(1),
10254 ),
10255 vec![1.0, 0.0],
10256 ),
10257 ]))
10258 .expect("document writer repaired");
10259 assert_eq!(ack.generation(), 2);
10260 drop(store);
10261 let reopened = Store::open(
10262 directory.path(),
10263 OpenOptions::new().with_max_resident_bytes(256 * 1024 * 1024),
10264 )
10265 .expect("reopen");
10266 assert_eq!(reopened.snapshot().expect("snapshot").generation(), 2);
10267 assert!(reopened.admit_native_read().is_ok());
10268 }
10269
10270 #[test]
10271 fn astra_12_vocabulary_charge_survives_eviction_and_close() {
10272 use crate::fts::{index::DEFAULT_FIELD, query::LexicalQuery};
10273 use crate::ingest::{DocId, DocumentVersion, IngestBatch, IngestDocument, Revision};
10274 let directory = tempdir().expect("directory");
10275 let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
10276 store
10277 .ingest(IngestBatch::new(vec![
10278 IngestDocument::new(
10279 DocumentVersion::new(DocId::new(1), Revision::new(1)),
10280 vec![1.0, 0.0],
10281 )
10282 .with_text("alpha alphabet beta"),
10283 ]))
10284 .expect("ingest");
10285 store
10286 .search_lexical_structured(
10287 &LexicalQuery::prefix(b"alph".to_vec(), DEFAULT_FIELD),
10288 1,
10289 64,
10290 QueryControl::Cancel(CancelToken::new()),
10291 )
10292 .expect("build cache");
10293 let dictionary = {
10294 let entry = store.lexical_index_cache.entry.lock().expect("assembly");
10295 let vocabulary = entry
10296 .as_ref()
10297 .expect("assembly")
10298 .assembly
10299 .vocabulary
10300 .lock()
10301 .expect("vocabulary");
10302 Arc::clone(vocabulary.as_ref().expect("cached"))
10303 };
10304 let bytes = dictionary._memory.bytes();
10305 assert!(bytes > 17);
10306 let second_owner = Arc::clone(&dictionary);
10307 drop(
10308 store
10309 .lexical_index_cache
10310 .entry
10311 .lock()
10312 .expect("evict")
10313 .take(),
10314 );
10315 assert_eq!(store.stats().expect("held bytes").cache_bytes, bytes);
10316 store
10317 .search_lexical_structured(
10318 &LexicalQuery::prefix(b"alph".to_vec(), DEFAULT_FIELD),
10319 1,
10320 64,
10321 QueryControl::Cancel(CancelToken::new()),
10322 )
10323 .expect("replacement cache");
10324 assert!(store.stats().expect("both dictionaries").cache_bytes > bytes);
10325 store.close().expect("close");
10326 assert_eq!(
10327 store
10328 .accounting
10329 .audit()
10330 .expect("closed accounting")
10331 .cache_bytes,
10332 bytes
10333 );
10334 drop(dictionary);
10335 assert_eq!(
10336 store.accounting.audit().expect("second owner").cache_bytes,
10337 bytes
10338 );
10339 assert_eq!(
10340 second_owner.view.iter().collect::<Vec<_>>(),
10341 vec![b"alpha".as_slice(), b"alphabet", b"beta"]
10342 );
10343 drop(second_owner);
10344 assert_eq!(store.accounting.audit().expect("last owner").cache_bytes, 0);
10345 assert_eq!(
10346 store.accounting.audit().expect("scratch").temporary_bytes,
10347 0
10348 );
10349 }
10350
10351 #[test]
10352 fn astra_12_refused_vocabulary_is_not_published() {
10353 use crate::fts::{index::DEFAULT_FIELD, query::LexicalQuery};
10354 use crate::ingest::{
10355 DocId, DocumentVersion, IngestBatch, IngestDocument, Revision, StoreLexicalError,
10356 };
10357 let directory = tempdir().expect("directory");
10358 let writer = Store::open(directory.path(), OpenOptions::default()).expect("open");
10359 writer
10360 .ingest(IngestBatch::new(vec![
10361 IngestDocument::new(
10362 DocumentVersion::new(DocId::new(1), Revision::new(1)),
10363 vec![1.0, 0.0],
10364 )
10365 .with_text("alpha alphabet beta"),
10366 ]))
10367 .expect("ingest");
10368 writer.seal().expect("seal");
10369 writer.close().expect("close writer");
10370 let store = Store::open(
10371 directory.path(),
10372 OpenOptions::default().with_max_temp_bytes(1),
10373 )
10374 .expect("small budget");
10375 for _ in 0..2 {
10376 let result = store.search_lexical_structured(
10377 &LexicalQuery::prefix(b"alph".to_vec(), DEFAULT_FIELD),
10378 1,
10379 64,
10380 QueryControl::Cancel(CancelToken::new()),
10381 );
10382 assert!(
10383 matches!(
10384 result,
10385 Err(StoreLexicalError::Query(super::QueryError::Store(
10386 StoreError::BudgetExceeded {
10387 component: "temporary",
10388 ..
10389 }
10390 )))
10391 ),
10392 "{result:?}"
10393 );
10394 assert_eq!(store.stats().expect("released").temporary_bytes, 0);
10395 let entry = store.lexical_index_cache.entry.lock().expect("assembly");
10396 assert!(
10397 entry
10398 .as_ref()
10399 .expect("assembled")
10400 .assembly
10401 .vocabulary
10402 .lock()
10403 .expect("dictionary")
10404 .is_none()
10405 );
10406 }
10407 store.close().expect("close");
10408 assert_eq!(
10409 store
10410 .accounting
10411 .audit()
10412 .expect("released cache")
10413 .cache_bytes,
10414 0
10415 );
10416 }
10417
10418 #[test]
10419 fn astra_03_lexical_assembly_charge_survives_cache_eviction() {
10420 use crate::fts::index::DEFAULT_FIELD;
10421 use crate::fts::search::TermQuery;
10422 use crate::ingest::{DocId, DocumentVersion, IngestBatch, IngestDocument, Revision};
10423 let directory = tempdir().expect("directory");
10424 let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
10425 store
10426 .ingest(IngestBatch::new(vec![
10427 IngestDocument::new(
10428 DocumentVersion::new(DocId::new(1), Revision::new(1)),
10429 vec![1.0, 0.0],
10430 )
10431 .with_text("alpha beta"),
10432 ]))
10433 .expect("ingest");
10434 store
10435 .search_lexical(
10436 &TermQuery::flat(vec![b"alpha".to_vec()], &[DEFAULT_FIELD]),
10437 1,
10438 QueryControl::Cancel(CancelToken::new()),
10439 )
10440 .expect("build lexical cache");
10441 let (first, second) = {
10442 let entry = store.lexical_index_cache.entry.lock().expect("cache lock");
10443 let assembly = &entry.as_ref().expect("cached assembly").assembly;
10444 (Arc::clone(assembly), Arc::clone(assembly))
10445 };
10446 let charged = store.stats().expect("charged stats").cache_bytes;
10447 let evicted = store
10448 .lexical_index_cache
10449 .entry
10450 .lock()
10451 .expect("cache lock")
10452 .take();
10453 drop(evicted);
10454 assert_eq!(
10455 first.index.document_count(),
10456 1,
10457 "eviction left the query data alive"
10458 );
10459 assert_eq!(
10460 store.stats().expect("held stats").cache_bytes,
10461 charged,
10462 "cache eviction must not release a query-owned assembly's charge"
10463 );
10464 drop(first);
10465 assert_eq!(
10466 store.stats().expect("second owner stats").cache_bytes,
10467 charged
10468 );
10469 drop(second);
10470 let released = store.stats().expect("released stats").cache_bytes;
10471 assert_eq!(
10472 released, 0,
10473 "the final owner must release the full assembly charge"
10474 );
10475 println!("assembly cache bytes: retained={charged}, after final owner={released}");
10476 store.close().expect("close");
10477 }
10478
10479 const CEILING_ROWS: [[f32; 4]; 3] = [
10481 [0.5, -0.25, 0.75, 0.125],
10482 [3.0, 1.5, -2.5, 0.25],
10483 [0.0, 0.0, 0.0, 0.0],
10484 ];
10485
10486 fn publish_f32_segment(directory: &std::path::Path) {
10488 let created = Store::open(directory, OpenOptions::default()).expect("create f32 store");
10489 created.close().expect("close f32 store");
10490
10491 let schema = Schema::timestamp_only();
10492 let mut columns = crate::meta::ColumnStoreBuilder::new(schema.clone());
10493 for row in 0..CEILING_ROWS.len() {
10494 columns
10495 .push_row(i64::try_from(row).expect("row timestamp"), &[])
10496 .expect("timestamp row");
10497 }
10498 let columns = columns.finish().expect("columns");
10499 let alive =
10500 crate::meta::AliveSet::new(u32::try_from(CEILING_ROWS.len()).expect("row count"));
10501 let rescore = CEILING_ROWS.iter().flatten().copied().collect::<Vec<f32>>();
10502 let codes = rescore
10503 .iter()
10504 .flat_map(|value| value.to_bits().to_le_bytes())
10505 .collect::<Vec<u8>>();
10506 let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
10507 .expect("f32 fixture durability");
10508 let meta = crate::segment::writer::write_segment(
10509 &StdVfs,
10510 directory,
10511 crate::segment::writer::SegmentBuild {
10512 id: crate::segment::SegmentId::new(11, [7; 10]),
10513 scheme: 0,
10514 dims: 4,
10515 codes: &codes,
10516 factors: crate::segment::writer::SegmentFactors::F32,
10517 rescore: &rescore,
10518 columns: &columns,
10519 alive: &alive,
10520 },
10521 policy,
10522 )
10523 .expect("write scheme-0 segment");
10524 commit_manifest(
10525 &StdVfs,
10526 directory,
10527 &Manifest {
10528 #[cfg(feature = "graph-cypher")]
10529 graph: None,
10530 generation: 1,
10531 log_seq: 0,
10532 segments: vec![meta],
10533 epochs: Vec::new(),
10534 epoch_alias: None,
10535 schema,
10536 },
10537 policy,
10538 )
10539 .expect("publish scheme-0 segment");
10540 }
10541
10542 fn ingest_ceiling_rows(store: &Store, keep_active: usize, one_segment_per_row: bool) {
10545 use crate::ingest::{DocId, DocumentVersion, IngestBatch, IngestDocument, Revision};
10546
10547 let sealed = CEILING_ROWS.len() - keep_active;
10548 for (index, row) in CEILING_ROWS.iter().enumerate() {
10549 let version = DocumentVersion::new(
10550 DocId::new(u128::try_from(index).expect("document id")),
10551 Revision::new(1),
10552 );
10553 store
10554 .ingest(IngestBatch::new(vec![IngestDocument::new(
10555 version,
10556 row.to_vec(),
10557 )]))
10558 .expect("ingest ceiling row");
10559 let seal_here = if one_segment_per_row {
10560 index < sealed
10561 } else {
10562 index + 1 == sealed
10563 };
10564 if seal_here {
10565 store.seal().expect("seal ceiling rows");
10566 }
10567 }
10568 }
10569
10570 fn assert_remembered_ceiling_is_exact(store: &Store, expected_segments: usize) {
10573 let queries: [Vec<f32>; 3] = [
10574 vec![0.0, 0.0, 0.0, 0.0],
10575 vec![1.0, -1.0, 0.5, 2.0],
10576 vec![7.5, 0.25, -3.5, 0.125],
10577 ];
10578 let admitted = store
10579 .admit_vector_search(SearchOptions::default())
10580 .expect("admit ceiling query");
10581 let snapshot = &admitted.snapshot;
10582 let active = &admitted.active_segment;
10583 assert_eq!(snapshot.segments().len(), expected_segments);
10584 for segment in snapshot.segments() {
10585 assert!(
10586 segment.cached_vector_ceiling_norm_range().is_none(),
10587 "a segment remembered an enclosure before any query walked it"
10588 );
10589 }
10590
10591 for query in &queries {
10592 let cold = super::exact_vector_ceiling(snapshot, active, query).expect("cold ceiling");
10593 for segment in snapshot.segments() {
10594 let remembered = segment
10595 .cached_vector_ceiling_norm_range()
10596 .expect("a walked segment must remember its enclosure");
10597 let recomputed = super::compute_segment_vector_ceiling_norm_range(segment)
10598 .expect("recomputed enclosure");
10599 assert_eq!(
10600 remembered.endpoint_bits(),
10601 recomputed.endpoint_bits(),
10602 "remembered enclosure differs from the recomputed one"
10603 );
10604 }
10605 let warm = super::exact_vector_ceiling(snapshot, active, query).expect("warm ceiling");
10606 let recomputed = super::fold_exact_vector_ceiling(
10607 snapshot,
10608 active,
10609 query,
10610 super::compute_segment_vector_ceiling_norm_range,
10611 )
10612 .expect("recomputed ceiling");
10613 assert_eq!(
10614 cold.to_bits(),
10615 warm.to_bits(),
10616 "the remembered ceiling changed between queries"
10617 );
10618 assert_eq!(
10619 cold.to_bits(),
10620 recomputed.to_bits(),
10621 "the remembered ceiling is not the recomputed ceiling"
10622 );
10623 }
10624 }
10625
10626 #[test]
10627 fn astra_00_elapsed_includes_ceiling_work() {
10628 use std::time::{Duration, Instant};
10629 struct CeilingClock {
10630 base: Instant,
10631 snapshot: Arc<super::PublishedSnapshot>,
10632 }
10633 impl super::MonotonicClock for CeilingClock {
10634 fn now(&self) -> Instant {
10635 if self
10638 .snapshot
10639 .segments()
10640 .iter()
10641 .all(|segment| segment.cached_vector_ceiling_norm_range().is_some())
10642 {
10643 self.base + Duration::from_secs(7)
10644 } else {
10645 self.base
10646 }
10647 }
10648 }
10649 let directory = tempdir().expect("ceiling timer store");
10650 let mut store = Store::open(directory.path(), OpenOptions::default()).expect("open");
10651 ingest_ceiling_rows(&store, 0, false);
10652 let admitted = store
10653 .admit_vector_search(SearchOptions::default())
10654 .expect("snapshot");
10655 assert_eq!(admitted.snapshot.segments().len(), 1);
10656 assert!(
10657 admitted
10658 .snapshot
10659 .segments()
10660 .iter()
10661 .all(|segment| { segment.cached_vector_ceiling_norm_range().is_none() })
10662 );
10663 let snapshot = Arc::clone(&admitted.snapshot);
10664 drop(admitted);
10665 store.clock = Arc::new(CeilingClock {
10666 base: Instant::now(),
10667 snapshot,
10668 });
10669 let outcome = store
10670 .search(
10671 crate::ingest::SearchRequest::new(&[1.0, 0.0, 0.0, 0.0]),
10672 1,
10673 SearchOptions::default().with_tier(SearchTier::Exact),
10674 QueryControl::Cancel(CancelToken::new()),
10675 )
10676 .expect("exact query");
10677 assert_eq!(outcome.diagnostics.elapsed, Duration::from_secs(7));
10678 store.clock = Arc::new(super::SystemMonotonicClock);
10680 store.close().expect("close");
10681 }
10682
10683 #[test]
10684 fn the_remembered_vector_ceiling_is_the_recomputed_ceiling_for_every_scheme() {
10685 let bit4_directory = tempdir().expect("Bit4 ceiling store directory");
10686 let bit4 = Store::open(bit4_directory.path(), OpenOptions::default())
10687 .expect("open Bit4 ceiling store");
10688 ingest_ceiling_rows(&bit4, 0, false);
10689 assert_remembered_ceiling_is_exact(&bit4, 1);
10690 bit4.close().expect("close Bit4 ceiling store");
10691
10692 let split_directory = tempdir().expect("multi-segment ceiling store directory");
10693 let split = Store::open(split_directory.path(), OpenOptions::default())
10694 .expect("open multi-segment ceiling store");
10695 ingest_ceiling_rows(&split, 0, true);
10696 assert_remembered_ceiling_is_exact(&split, CEILING_ROWS.len());
10697 split.close().expect("close multi-segment ceiling store");
10698
10699 let int8_directory = tempdir().expect("Int8 ceiling store directory");
10700 let int8 = Store::open_with_test_dependencies(
10701 int8_directory.path(),
10702 OpenOptions::default(),
10703 StoreTestDependencies::new(Arc::new(StdVfs), Arc::new(super::SystemMonotonicClock))
10704 .with_vector_seal_scheme(crate::quant::QuantScheme::Int8),
10705 )
10706 .expect("open Int8 ceiling store");
10707 ingest_ceiling_rows(&int8, 0, false);
10708 assert_remembered_ceiling_is_exact(&int8, 1);
10709 int8.close().expect("close Int8 ceiling store");
10710
10711 let f32_directory = tempdir().expect("F32 ceiling store directory");
10712 publish_f32_segment(f32_directory.path());
10713 let f32_store = Store::open(f32_directory.path(), OpenOptions::default())
10714 .expect("open F32 ceiling store");
10715 assert_remembered_ceiling_is_exact(&f32_store, 1);
10716 f32_store.close().expect("close F32 ceiling store");
10717
10718 let active_directory = tempdir().expect("active ceiling store directory");
10719 let active = Store::open(active_directory.path(), OpenOptions::default())
10720 .expect("open active ceiling store");
10721 ingest_ceiling_rows(&active, 2, false);
10722 assert!(
10723 !active
10724 .admit_vector_search(SearchOptions::default())
10725 .expect("admit active ceiling query")
10726 .active_segment
10727 .is_empty(),
10728 "the active-segment case sealed every row"
10729 );
10730 assert_remembered_ceiling_is_exact(&active, 1);
10731 active.close().expect("close active ceiling store");
10732 }
10733
10734 #[test]
10735 fn exact_and_graph_tiers_preserve_quantizer_validation_errors() {
10736 let directory = tempdir().expect("store directory");
10737 let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
10738 let query = vec![0.0_f32; crate::kernels::MAX_DOT_I8_DIMENSION + 1];
10739
10740 for tier in [
10741 SearchTier::Exact,
10742 SearchTier::Graph(GraphSearchOptions::default()),
10743 ] {
10744 let error = store
10745 .search(
10746 crate::ingest::SearchRequest::new(&query),
10747 1,
10748 SearchOptions::default().with_tier(tier),
10749 QueryControl::Cancel(CancelToken::new()),
10750 )
10751 .expect_err("oversized quantized query must retain its error");
10752 assert!(matches!(
10753 error,
10754 super::QueryError::Scan(crate::scan::ScanError::Quant(
10755 crate::quant::QuantError::DimensionTooLarge { actual, maximum }
10756 )) if actual == query.len()
10757 && maximum == crate::kernels::MAX_DOT_I8_DIMENSION
10758 ));
10759 }
10760 }
10761
10762 #[cfg(feature = "allocation-audit")]
10763 #[test]
10764 fn exact_tier_query_does_not_prepare_quantized_forms() {
10765 let directory = tempdir().expect("store directory");
10766 let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
10767 let query = [0.25_f32; 128];
10768 let options = SearchOptions::default().with_tier(SearchTier::Exact);
10769 let warm = store
10770 .search(
10771 crate::ingest::SearchRequest::new(&query),
10772 1,
10773 options,
10774 QueryControl::Cancel(CancelToken::new()),
10775 )
10776 .expect("warm exact-tier search");
10777 assert!(warm.candidates.is_empty());
10778 let control = QueryControl::Cancel(CancelToken::new());
10779
10780 let (outcome, report) = crate::allocation_audit::audit_engine_path(|| {
10781 store.search(
10782 crate::ingest::SearchRequest::new(&query),
10783 1,
10784 options,
10785 control,
10786 )
10787 });
10788 let outcome = outcome.expect("audited exact-tier search");
10789 assert!(outcome.candidates.is_empty());
10790 assert_eq!(report.allocations, 0, "Exact tier prepared quantized forms");
10791 assert_eq!(report.attributed_bytes, 0);
10792 assert_eq!(report.unattributed_bytes, 0);
10793 }
10794
10795 #[test]
10796 fn astra_18_scoped_work_is_joined_before_cancel_returns() {
10797 use std::sync::atomic::{AtomicBool, Ordering};
10798 use std::sync::mpsc;
10799 let worker = super::pool::LexicalWorker::start().expect("worker");
10800 let token = CancelToken::new();
10801 let work_token = token.clone();
10802 let finished = AtomicBool::new(false);
10803 let mut borrowed_rows = Vec::new();
10804 let (started_tx, started_rx) = mpsc::sync_channel(0);
10805 let (release_tx, release_rx) = mpsc::sync_channel(0);
10806 let worker_rows = &mut borrowed_rows;
10807 let worker_finished = &finished;
10808 let (caller, lexical) = worker
10809 .run_scoped(
10810 move || {
10811 started_tx.send(()).expect("entered borrowed job");
10812 release_rx.recv().expect("caller has cancelled");
10813 assert!(work_token.is_cancelled());
10814 worker_rows.push(42_u32);
10815 worker_finished.store(true, Ordering::Release);
10816 Err::<(), _>(crate::fusion::FusionError::Cancelled { partial: false })
10817 },
10818 || {
10819 started_rx.recv().expect("worker entered");
10820 token.cancel();
10821 assert!(!finished.load(Ordering::Acquire));
10822 release_tx.send(()).expect("release borrowed job");
10823 Err::<(), _>(crate::fusion::FusionError::Cancelled { partial: false })
10824 },
10825 )
10826 .expect("scoped dispatch");
10827 assert!(finished.load(Ordering::Acquire));
10828 assert_eq!(borrowed_rows, [42]);
10829 assert_eq!(
10830 resolve_hybrid_leg_results(caller, lexical.expect("worker returned")),
10831 Err(crate::fusion::FusionError::Cancelled { partial: false }),
10832 );
10833 worker.stop_and_join().expect("join worker");
10834 }
10835
10836 #[test]
10837 fn astra_18_control_error_precedence_is_completion_order_independent() {
10838 use crate::fusion::{FusionError, FusionLeg, LegFailureKind};
10839 use std::sync::mpsc;
10840 let failures = [
10841 FusionError::Leg {
10842 leg: FusionLeg::Vector,
10843 kind: LegFailureKind::Scan,
10844 detail: "scan".to_owned(),
10845 },
10846 FusionError::Cancelled { partial: false },
10847 FusionError::Timeout { partial: false },
10848 FusionError::ReadCancelled { partial: false },
10849 ];
10850 let worker = super::pool::LexicalWorker::start().expect("worker");
10851 for (a, vector) in failures.iter().enumerate() {
10852 for (b, lexical) in failures.iter().enumerate() {
10853 for lexical_first in [false, true] {
10854 let (lexical_tx, lexical_rx) = mpsc::sync_channel(1);
10855 let (vector_tx, vector_rx) = mpsc::sync_channel(1);
10856 let (vector_result, lexical_result) = worker
10857 .run_scoped(
10858 move || {
10859 if !lexical_first {
10860 vector_rx.recv().expect("vector completed");
10861 }
10862 if lexical_first {
10863 lexical_tx.send(()).expect("lexical completed");
10864 }
10865 Err::<(), _>(lexical.clone())
10866 },
10867 || {
10868 if lexical_first {
10869 lexical_rx.recv().expect("lexical completed");
10870 }
10871 if !lexical_first {
10872 vector_tx.send(()).expect("vector completed");
10873 }
10874 Err::<(), _>(vector.clone())
10875 },
10876 )
10877 .expect("dispatch");
10878 assert_eq!(
10879 resolve_hybrid_leg_results(vector_result, lexical_result.expect("joined")),
10880 Err(failures.get(a.max(b)).expect("known failure index").clone()),
10881 "vector={a} lexical={b} lexical_first={lexical_first}",
10882 );
10883 }
10884 }
10885 }
10886 worker.stop_and_join().expect("join worker");
10887 }
10888
10889 #[test]
10890 fn hybrid_dual_failure_precedence_is_independent_of_leg_completion_order() {
10891 use crate::fusion::{FusionError, FusionLeg, LegFailureKind};
10892
10893 let vector_failure = FusionError::Leg {
10894 leg: FusionLeg::Vector,
10895 kind: LegFailureKind::Scan,
10896 detail: "vector failure".to_owned(),
10897 };
10898 let lexical_failure = FusionError::Leg {
10899 leg: FusionLeg::Lexical,
10900 kind: LegFailureKind::Lexical,
10901 detail: "lexical failure".to_owned(),
10902 };
10903 assert_eq!(
10904 resolve_hybrid_leg_results::<(), ()>(
10905 Err(vector_failure.clone()),
10906 Err(lexical_failure.clone()),
10907 ),
10908 Err(vector_failure.clone()),
10909 );
10910 assert_eq!(
10911 resolve_hybrid_leg_results::<(), ()>(
10912 Err(vector_failure.clone()),
10913 Err(FusionError::Cancelled { partial: false }),
10914 ),
10915 Err(FusionError::Cancelled { partial: false }),
10916 );
10917 assert_eq!(
10918 resolve_hybrid_leg_results::<(), ()>(
10919 Err(FusionError::Timeout { partial: false }),
10920 Err(FusionError::ReadCancelled { partial: false }),
10921 ),
10922 Err(FusionError::ReadCancelled { partial: false }),
10923 );
10924 assert_eq!(
10925 resolve_hybrid_leg_results::<(), ()>(
10926 Err(FusionError::Cancelled { partial: false }),
10927 Err(FusionError::Timeout { partial: false }),
10928 ),
10929 Err(FusionError::Timeout { partial: false }),
10930 );
10931 assert_eq!(
10932 resolve_hybrid_leg_results::<(), ()>(
10933 Err(FusionError::ReadCancelled { partial: false }),
10934 Err(lexical_failure),
10935 ),
10936 Err(FusionError::ReadCancelled { partial: false }),
10937 );
10938 }
10939
10940 #[test]
10941 fn accounting_errors_report_the_typed_context() {
10942 let statistics = StoreError::Statistics {
10943 component: "mapped resident bytes",
10944 source: std::io::Error::other("mincore failed"),
10945 };
10946 assert_eq!(
10947 statistics.to_string(),
10948 "store statistics mapped resident bytes: mincore failed"
10949 );
10950 assert_eq!(
10951 statistics.source().map(ToString::to_string),
10952 Some("mincore failed".to_owned())
10953 );
10954
10955 let budget = StoreError::BudgetExceeded {
10956 needed: 65,
10957 budget: 64,
10958 component: "wal",
10959 };
10960 assert_eq!(
10961 budget.to_string(),
10962 "store wal allocation needs 65 bytes, budget is 64 bytes"
10963 );
10964 assert!(budget.source().is_none());
10965
10966 let allocation = StoreError::AllocationFailed {
10967 needed: 23,
10968 component: "snapshot",
10969 };
10970 assert_eq!(
10971 allocation.to_string(),
10972 "store snapshot allocator rejected 23 bytes"
10973 );
10974 assert!(allocation.source().is_none());
10975 }
10976
10977 #[test]
10978 fn open_options_resolve_durability_before_filesystem_mutation() {
10979 let parent = tempdir().expect("parent directory");
10980 let store_path = parent.path().join("not-created");
10981 let options =
10982 OpenOptions::new().with_durability(DurabilityMode::Attached, CommitTier::Durable);
10983
10984 let error = match Store::open(&store_path, options) {
10985 Ok(store) => {
10986 drop(store);
10987 panic!("attached durability unexpectedly opened")
10988 }
10989 Err(error) => error,
10990 };
10991
10992 assert!(matches!(
10993 error,
10994 StoreError::Durability(DurabilityPolicyError::AttachedNotYetSupported)
10995 ));
10996 assert!(!store_path.exists(), "rejected open created store files");
10997 }
10998
10999 #[test]
11000 fn memory_vfs_writable_open_requires_host_lease_directory() {
11001 let parent = tempdir().expect("parent directory");
11002 let store_path = parent.path().join("memory-only-store");
11003 let dependencies = StoreTestDependencies::new(
11004 Arc::new(MemoryVfs::new()),
11005 Arc::new(ManualMonotonicClock::new()),
11006 );
11007 let result =
11008 Store::open_with_test_dependencies(&store_path, OpenOptions::default(), dependencies);
11009 assert!(
11010 matches!(result, Err(StoreError::Lock(super::lock::StoreLockError::Io { ref source, .. })) if source.kind() == std::io::ErrorKind::NotFound),
11011 "writable open must refuse the missing host lease directory"
11012 );
11013 assert!(!store_path.exists());
11014 }
11015
11016 #[test]
11017 fn memory_vfs_store_retains_real_reader_lease() {
11018 let directory = tempdir().expect("host lease directory");
11019 let dependencies = StoreTestDependencies::new(
11020 Arc::new(MemoryVfs::new()),
11021 Arc::new(ManualMonotonicClock::new()),
11022 );
11023 let store = Store::open_with_test_dependencies(
11024 directory.path(),
11025 OpenOptions::default(),
11026 dependencies,
11027 )
11028 .expect("MemoryVfs store with host lease");
11029 let snapshot = store.open_snapshot().expect("snapshot view");
11030 assert!(matches!(
11031 super::lock::StoreLock::reclaim_exclusive(directory.path()),
11032 Err(super::lock::StoreLockError::Io { source, .. })
11033 if source.kind() == std::io::ErrorKind::WouldBlock
11034 ));
11035 drop(store);
11036 assert!(matches!(
11037 super::lock::StoreLock::reclaim_exclusive(directory.path()),
11038 Err(super::lock::StoreLockError::Io { source, .. })
11039 if source.kind() == std::io::ErrorKind::WouldBlock
11040 ));
11041 drop(snapshot);
11042 drop(super::lock::StoreLock::reclaim_exclusive(directory.path()).expect("released lease"));
11043 }
11044
11045 #[test]
11046 fn open_refuses_a_manifest_ahead_of_the_checked_wal() {
11047 let directory = tempdir().expect("store directory");
11048 let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::Ordered)
11049 .expect("derived policy");
11050 commit_manifest(
11051 &StdVfs,
11052 directory.path(),
11053 &Manifest {
11054 #[cfg(feature = "graph-cypher")]
11055 graph: None,
11056 generation: 1,
11057 log_seq: 1,
11058 segments: Vec::new(),
11059 epochs: Vec::new(),
11060 epoch_alias: None,
11061 schema: Schema::new(Vec::new()).expect("schema"),
11062 },
11063 policy,
11064 )
11065 .expect("manifest");
11066
11067 let error = match Store::open(directory.path(), OpenOptions::default()) {
11068 Ok(store) => {
11069 drop(store);
11070 panic!("manifest ahead of missing WAL unexpectedly opened")
11071 }
11072 Err(error) => error,
11073 };
11074
11075 assert!(matches!(
11076 error,
11077 StoreError::Manifest(ManifestError::AheadOfLog {
11078 snapshot: 1,
11079 durable: 0
11080 })
11081 ));
11082 }
11083}
11084
11085#[cfg(test)]
11086mod shared_bound_tests;
11087
11088#[cfg(test)]
11089mod hybrid_overlap_tests;