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