1use crate::graph::block::{CheckedNodeId, GraphNodeBlocks, GraphNodeError};
4use crate::kernels::{Bit4Rows4, GatherShapeError, score_bit4_ptrs};
5use crate::lifecycle::QueryCancellation;
6use crate::meta::DocBitmap;
7use crate::quant::{
8 Bit4Query, QuantError, RescoreError, RescoreMetric, RescorePool, prepare_bit4_query,
9 rescore_top_k,
10};
11use crate::scan::ScanError;
12
13#[cfg(any(test, feature = "test-support"))]
16#[derive(Default)]
17#[doc(hidden)]
18pub struct GraphQueryTestObservations {
19 pub traversals: usize,
21 pub preparations: Vec<(usize, usize, u64)>,
23 pub exact_rows: Vec<usize>,
25 pub validated_pools: Vec<Vec<u32>>,
27}
28
29#[cfg(any(test, feature = "test-support"))]
30pub(crate) fn observe_exact_score(row: usize) {
31 GRAPH_QUERY_TEST_OBSERVATIONS.with(|observations| {
32 if let Some(observations) = observations.borrow_mut().as_mut() {
33 observations.exact_rows.push(row);
34 }
35 });
36}
37
38#[cfg(any(test, feature = "test-support"))]
39thread_local! {
40 static GRAPH_QUERY_TEST_OBSERVATIONS: std::cell::RefCell<Option<GraphQueryTestObservations>> =
41 const { std::cell::RefCell::new(None) };
42}
43
44#[cfg(any(test, feature = "test-support"))]
46#[doc(hidden)]
47pub fn begin_graph_query_test_observations() {
48 GRAPH_QUERY_TEST_OBSERVATIONS.with(|observations| {
49 *observations.borrow_mut() = Some(GraphQueryTestObservations::default());
50 });
51}
52
53#[cfg(any(test, feature = "test-support"))]
55#[doc(hidden)]
56pub fn take_graph_query_test_observations() -> GraphQueryTestObservations {
57 GRAPH_QUERY_TEST_OBSERVATIONS.with(|observations| observations.take().unwrap_or_default())
58}
59
60pub(crate) struct PreparedGraphQuery {
62 query: Bit4Query,
63 norm: f64,
64}
65
66impl PreparedGraphQuery {
67 pub(crate) fn new(
68 query: &[f32],
69 padded_dimensions: usize,
70 seed: u64,
71 ) -> Result<Self, GraphSearchError> {
72 if padded_dimensions < query.len() {
73 return Err(GraphSearchError::Geometry(
74 "query padding is smaller than its original dimensions".to_owned(),
75 ));
76 }
77 let padded = if padded_dimensions == query.len() {
78 None
79 } else {
80 let mut values = Vec::with_capacity(padded_dimensions);
81 values.extend_from_slice(query);
82 values.resize(padded_dimensions, 0.0);
83 Some(values)
84 };
85 let prepared = prepare_bit4_query(padded.as_deref().unwrap_or(query), seed)?;
86 #[cfg(any(test, feature = "test-support"))]
87 GRAPH_QUERY_TEST_OBSERVATIONS.with(|observations| {
88 if let Some(observations) = observations.borrow_mut().as_mut() {
89 observations
90 .preparations
91 .push((query.len(), padded_dimensions, seed));
92 }
93 });
94 Ok(Self {
95 query: prepared,
96 norm: squared_norm(query),
97 })
98 }
99
100 pub(crate) fn resident_bytes(&self) -> usize {
101 self.query.resident_bytes()
102 }
103
104 pub(crate) fn bit4(&self) -> &Bit4Query {
105 &self.query
106 }
107}
108
109const SIFT_RESEARCH_FLOOR_TENTHS: usize = 14;
111const EF_TENTHS_DENOMINATOR: usize = 10;
113const SIFT_RESEARCH_ABSOLUTE_FLOOR: usize = 140;
117const SIFT_SHIPPED_EF_PER_K: usize = 2;
121const ANGULAR_EF_PER_K: usize = 4;
126pub const UNIT_NORM_SQUARED_TOLERANCE: f64 = 1.0e-3;
128
129pub(crate) fn non_unit_squared_norm(vector: &[f32]) -> Option<f64> {
130 if vector.is_empty() || vector.iter().any(|value| !value.is_finite()) {
131 return None;
132 }
133 let squared_norm = vector
134 .iter()
135 .map(|value| f64::from(*value) * f64::from(*value))
136 .sum::<f64>();
137 ((squared_norm - 1.0).abs() > UNIT_NORM_SQUARED_TOLERANCE).then_some(squared_norm)
138}
139
140#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
142pub enum GraphSearchProfile {
143 #[default]
145 SiftClass,
146 Angular,
148}
149
150#[derive(Clone, Copy, Debug, Eq, PartialEq)]
152pub enum GraphDistanceMetric {
153 SquaredL2,
155}
156
157impl std::fmt::Display for GraphDistanceMetric {
158 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 match self {
160 Self::SquaredL2 => formatter.write_str("squared-l2"),
161 }
162 }
163}
164
165#[derive(Clone, Copy, Debug, Eq, PartialEq)]
167pub struct GraphProfileShape {
168 pub document_dims: u32,
170 pub document_normalization: crate::epoch::Normalization,
172 pub query_dims: u32,
174 pub query_normalization: crate::epoch::Normalization,
176 pub metric: GraphDistanceMetric,
178}
179
180#[derive(Clone, Copy, Debug, Eq, PartialEq)]
182pub enum EpochGraphProfile {
183 SiftClass,
185 AngularClass,
187}
188
189impl EpochGraphProfile {
190 #[must_use]
192 pub const fn search_profile(self) -> GraphSearchProfile {
193 match self {
194 Self::SiftClass => GraphSearchProfile::SiftClass,
195 Self::AngularClass => GraphSearchProfile::Angular,
196 }
197 }
198
199 #[must_use]
201 pub const fn build_params(self) -> crate::graph::GraphParams {
202 match self {
203 Self::SiftClass => crate::graph::GraphParams::sift_1m(),
204 Self::AngularClass => crate::graph::GraphParams::angular(),
205 }
206 }
207}
208
209#[derive(Clone, Copy, Debug, Eq, PartialEq)]
211pub enum GraphProfileError {
212 EpochUnstamped,
214 EpochNotRegistered {
216 epoch: crate::epoch::EpochIdentity,
218 },
219 UnrecognizedEpochShape {
221 epoch: crate::epoch::EpochIdentity,
223 shape: GraphProfileShape,
225 },
226}
227
228impl std::fmt::Display for GraphProfileError {
229 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230 match self {
231 Self::EpochUnstamped => formatter.write_str(
232 "graph profile requires a persisted epoch registry, but the store is unstamped",
233 ),
234 Self::EpochNotRegistered { epoch } => write!(
235 formatter,
236 "graph profile epoch ({}, {}) is absent from the pinned registry",
237 epoch.embedding, epoch.tokenizer
238 ),
239 Self::UnrecognizedEpochShape { epoch, shape } => write!(
240 formatter,
241 "graph profile epoch ({}, {}) has unrecognized shape: document dims {}, document normalization {:?}, query dims {}, query normalization {:?}, metric {}",
242 epoch.embedding,
243 epoch.tokenizer,
244 shape.document_dims,
245 shape.document_normalization,
246 shape.query_dims,
247 shape.query_normalization,
248 shape.metric
249 ),
250 }
251 }
252}
253
254impl std::error::Error for GraphProfileError {}
255
256pub fn select_epoch_graph_profile(
258 epoch: &crate::manifest::EpochMeta,
259 metric: GraphDistanceMetric,
260) -> Result<EpochGraphProfile, GraphProfileError> {
261 let identity = crate::epoch::EpochIdentity {
262 embedding: epoch.id,
263 tokenizer: epoch.tokenizer,
264 };
265 let shape = GraphProfileShape {
266 document_dims: epoch.embedding.document.dims,
267 document_normalization: epoch.embedding.document.normalization,
268 query_dims: epoch.embedding.query.dims,
269 query_normalization: epoch.embedding.query.normalization,
270 metric,
271 };
272 if shape.document_dims == shape.query_dims && shape.metric == GraphDistanceMetric::SquaredL2 {
285 match (shape.document_normalization, shape.query_normalization) {
286 (crate::epoch::Normalization::None, crate::epoch::Normalization::None) => {
287 return Ok(EpochGraphProfile::SiftClass);
288 }
289 (crate::epoch::Normalization::L2, crate::epoch::Normalization::L2) => {
290 return Ok(EpochGraphProfile::AngularClass);
293 }
294 _ => {}
295 }
296 }
297 Err(GraphProfileError::UnrecognizedEpochShape {
298 epoch: identity,
299 shape,
300 })
301}
302
303#[derive(Clone, Copy, Debug, Eq, PartialEq)]
305pub enum AdaptiveEfError {
306 ZeroK,
308 KExceedsRows {
310 k: usize,
312 rows: usize,
314 },
315 ExplicitBelowK {
317 k: usize,
319 ef: usize,
321 },
322 ExplicitExceedsRows {
324 ef: usize,
326 rows: usize,
328 },
329 ArithmeticOverflow,
331}
332
333impl std::fmt::Display for AdaptiveEfError {
334 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335 match self {
336 Self::ZeroK => formatter.write_str("adaptive graph top-k must be positive"),
337 Self::KExceedsRows { k, rows } => {
338 write!(formatter, "graph top-k {k} exceeds graph row count {rows}")
339 }
340 Self::ExplicitBelowK { k, ef } => {
341 write!(formatter, "explicit graph ef {ef} is below top-k {k}")
342 }
343 Self::ExplicitExceedsRows { ef, rows } => {
344 write!(
345 formatter,
346 "explicit graph ef {ef} exceeds graph row count {rows}"
347 )
348 }
349 Self::ArithmeticOverflow => formatter.write_str("adaptive graph ef overflowed usize"),
350 }
351 }
352}
353
354impl std::error::Error for AdaptiveEfError {}
355
356#[derive(Clone, Copy, Debug)]
358pub struct GraphSearchRequest<'a> {
359 query: &'a [f32],
360 k: usize,
361 ef: Option<usize>,
362 profile: GraphSearchProfile,
363 seed: u64,
364 trace_candidates: bool,
365 observed_core_class: QueryCoreClass,
366 prefetch: TraversalPrefetch,
367}
368
369impl<'a> GraphSearchRequest<'a> {
370 #[must_use]
372 pub const fn new(query: &'a [f32], k: usize, seed: u64) -> Self {
373 Self {
374 query,
375 k,
376 ef: None,
377 profile: GraphSearchProfile::SiftClass,
378 seed,
379 trace_candidates: false,
380 observed_core_class: QueryCoreClass::Unverified,
381 prefetch: TraversalPrefetch::Enabled,
382 }
383 }
384
385 #[must_use]
387 pub const fn with_profile(mut self, profile: GraphSearchProfile) -> Self {
388 self.profile = profile;
389 self
390 }
391
392 #[must_use]
394 pub const fn with_ef(mut self, ef: usize) -> Self {
395 self.ef = Some(ef);
396 self
397 }
398
399 pub fn research_floor_ef(self) -> Result<usize, AdaptiveEfError> {
405 if self.k == 0 {
406 return Err(AdaptiveEfError::ZeroK);
407 }
408 match self.profile {
409 GraphSearchProfile::SiftClass => self
410 .k
411 .checked_mul(SIFT_RESEARCH_FLOOR_TENTHS)
412 .map(|value| value.div_ceil(EF_TENTHS_DENOMINATOR))
413 .map(|value| value.max(SIFT_RESEARCH_ABSOLUTE_FLOOR))
414 .ok_or(AdaptiveEfError::ArithmeticOverflow),
415 GraphSearchProfile::Angular => self
416 .k
417 .checked_mul(ANGULAR_EF_PER_K)
418 .ok_or(AdaptiveEfError::ArithmeticOverflow),
419 }
420 }
421
422 pub fn effective_ef(self, rows: usize) -> Result<usize, AdaptiveEfError> {
429 if self.k == 0 {
430 return Err(AdaptiveEfError::ZeroK);
431 }
432 if self.k > rows {
433 return Err(AdaptiveEfError::KExceedsRows { k: self.k, rows });
434 }
435 if let Some(ef) = self.ef {
436 if ef < self.k {
437 return Err(AdaptiveEfError::ExplicitBelowK { k: self.k, ef });
438 }
439 if ef > rows {
440 return Err(AdaptiveEfError::ExplicitExceedsRows { ef, rows });
441 }
442 return Ok(ef);
443 }
444 let floor = self.research_floor_ef()?;
445 let target = match self.profile {
446 GraphSearchProfile::SiftClass => self
447 .k
448 .checked_mul(SIFT_SHIPPED_EF_PER_K)
449 .map(|ef| ef.max(floor))
450 .ok_or(AdaptiveEfError::ArithmeticOverflow)?,
451 GraphSearchProfile::Angular => floor,
452 };
453 Ok(target.min(rows))
454 }
455
456 #[must_use]
458 pub const fn with_candidate_trace(mut self) -> Self {
459 self.trace_candidates = true;
460 self
461 }
462
463 #[must_use]
465 pub const fn with_observed_core_class(mut self, core_class: QueryCoreClass) -> Self {
466 self.observed_core_class = core_class;
467 self
468 }
469
470 #[must_use]
472 pub const fn with_prefetch(mut self, prefetch: TraversalPrefetch) -> Self {
473 self.prefetch = prefetch;
474 self
475 }
476}
477
478#[derive(Clone, Copy, Debug, Eq, PartialEq)]
480pub enum TraversalPrefetch {
481 Enabled,
483 Disabled,
485}
486
487impl TraversalPrefetch {
488 const fn is_enabled(self) -> bool {
489 matches!(self, Self::Enabled)
490 }
491}
492
493#[derive(Clone, Copy, Debug, PartialEq)]
495pub struct GraphSearchCandidate {
496 row_id: u32,
497 distance: f64,
498}
499
500impl GraphSearchCandidate {
501 #[must_use]
503 pub const fn row_id(self) -> u32 {
504 self.row_id
505 }
506
507 #[must_use]
509 pub const fn distance(self) -> f64 {
510 self.distance
511 }
512}
513
514#[derive(Clone, Copy, Debug, Eq, PartialEq)]
516pub enum QueryQosClass {
517 UserInteractive,
519 UserInitiated,
521 Default,
523 Utility,
525 Background,
527 Unspecified,
529 Unavailable,
531 Unknown,
533}
534
535#[derive(Clone, Copy, Debug, Eq, PartialEq)]
537pub enum QueryCoreClass {
538 Performance,
540 Efficiency,
542 Unverified,
544}
545
546#[derive(Clone, Copy, Debug, Eq, PartialEq)]
548pub struct GraphSearchCounters {
549 effective_ef: usize,
550 hops: usize,
551 candidates_scored: usize,
552 candidates_rescored: usize,
553 pushes: usize,
554 visited: usize,
555 visited_epoch_cleared: bool,
556 dims_touched: u64,
557 bytes_read: u64,
558 qos_class: QueryQosClass,
559 qos_relative_priority: i32,
560 core_class: QueryCoreClass,
561}
562
563#[derive(Clone, Copy, Debug, Eq, PartialEq)]
565pub struct DeterministicGraphWork {
566 pub effective_ef: usize,
568 pub hops: usize,
570 pub candidates_scored: usize,
572 pub candidates_rescored: usize,
574 pub pushes: usize,
576 pub visited: usize,
578}
579
580impl GraphSearchCounters {
581 #[must_use]
583 pub const fn deterministic_work(self) -> DeterministicGraphWork {
584 DeterministicGraphWork {
585 effective_ef: self.effective_ef,
586 hops: self.hops,
587 candidates_scored: self.candidates_scored,
588 candidates_rescored: self.candidates_rescored,
589 pushes: self.pushes,
590 visited: self.visited,
591 }
592 }
593
594 #[must_use]
596 pub const fn effective_ef(self) -> usize {
597 self.effective_ef
598 }
599
600 #[must_use]
602 pub const fn hops(self) -> usize {
603 self.hops
604 }
605
606 #[must_use]
608 pub const fn candidates_scored(self) -> usize {
609 self.candidates_scored
610 }
611
612 #[must_use]
614 pub const fn candidates_rescored(self) -> usize {
615 self.candidates_rescored
616 }
617
618 #[must_use]
620 pub const fn pushes(self) -> usize {
621 self.pushes
622 }
623
624 #[must_use]
626 pub const fn visited(self) -> usize {
627 self.visited
628 }
629
630 #[must_use]
632 pub const fn visited_epoch_cleared(self) -> bool {
633 self.visited_epoch_cleared
634 }
635
636 #[must_use]
638 pub const fn dims_touched(self) -> u64 {
639 self.dims_touched
640 }
641
642 #[must_use]
644 pub const fn bytes_read(self) -> u64 {
645 self.bytes_read
646 }
647
648 #[must_use]
650 pub const fn qos_class(self) -> QueryQosClass {
651 self.qos_class
652 }
653
654 #[must_use]
656 pub const fn qos_relative_priority(self) -> i32 {
657 self.qos_relative_priority
658 }
659
660 #[must_use]
662 pub const fn core_class(self) -> QueryCoreClass {
663 self.core_class
664 }
665}
666
667#[derive(Clone, Debug, PartialEq)]
669pub struct GraphSearchResult {
670 candidates: Vec<GraphSearchCandidate>,
671 counters: GraphSearchCounters,
672 candidate_sequence: Option<Vec<u32>>,
673}
674
675impl GraphSearchResult {
676 #[must_use]
678 pub fn candidates(&self) -> &[GraphSearchCandidate] {
679 &self.candidates
680 }
681
682 #[must_use]
684 pub const fn counters(&self) -> GraphSearchCounters {
685 self.counters
686 }
687
688 #[must_use]
690 pub fn candidate_sequence(&self) -> Option<&[u32]> {
691 self.candidate_sequence.as_deref()
692 }
693
694 #[must_use]
696 pub fn recall_against(&self, truth: &[u32]) -> usize {
697 self.candidates
698 .iter()
699 .filter(|candidate| truth.contains(&candidate.row_id))
700 .count()
701 }
702}
703
704#[derive(Clone, Debug, PartialEq)]
706pub enum FilteredGraphSearchOutcome {
707 Traversed(GraphSearchResult),
709 VisitedBudgetExceeded {
711 visited: usize,
713 budget: usize,
715 counters: GraphSearchCounters,
717 },
718}
719
720impl FilteredGraphSearchOutcome {
721 #[must_use]
723 pub const fn counters(&self) -> GraphSearchCounters {
724 match self {
725 Self::Traversed(result) => result.counters,
726 Self::VisitedBudgetExceeded { counters, .. } => *counters,
727 }
728 }
729}
730
731enum TraversalOutcome {
732 Retained {
733 counters: GraphSearchCounters,
734 candidate_sequence: Option<Vec<u32>>,
735 },
736 Budget(SearchInnerOutcome),
737}
738
739enum SearchInnerOutcome {
740 Traversed(GraphSearchResult),
741 VisitedBudgetExceeded {
742 visited: usize,
743 budget: usize,
744 counters: GraphSearchCounters,
745 },
746}
747
748#[derive(Clone, Debug, PartialEq)]
750pub enum GraphSearchError {
751 Geometry(String),
753 Profile(GraphProfileError),
755 Graph(GraphNodeError),
757 Quant(QuantError),
759 Gather(GatherShapeError),
761 AdaptiveEf(AdaptiveEfError),
763 Rescore(RescoreError),
765 FilteredCandidateShortfall {
767 k: usize,
769 candidates: usize,
771 counters: GraphSearchCounters,
773 },
774 ExactRescoreUnavailable(String),
776 VisitedCapExceeded {
778 visited: usize,
780 cap: usize,
782 },
783 Cancelled {
785 partial: bool,
787 },
788 Timeout {
790 partial: bool,
792 },
793 ReadCancelled {
795 partial: bool,
797 },
798 Scan(ScanError),
800}
801
802impl std::fmt::Display for GraphSearchError {
803 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
804 match self {
805 Self::Geometry(detail) => {
806 write!(formatter, "graph search geometry is invalid: {detail}")
807 }
808 Self::Profile(error) => error.fmt(formatter),
809 Self::Graph(error) => error.fmt(formatter),
810 Self::Quant(error) => error.fmt(formatter),
811 Self::Gather(error) => error.fmt(formatter),
812 Self::AdaptiveEf(error) => error.fmt(formatter),
813 Self::Rescore(error) => error.fmt(formatter),
814 Self::FilteredCandidateShortfall { k, candidates, .. } => write!(
815 formatter,
816 "filtered graph search retained {candidates} candidates for requested k={k}"
817 ),
818 Self::ExactRescoreUnavailable(detail) => formatter.write_str(detail),
819 Self::VisitedCapExceeded { visited, cap } => write!(
820 formatter,
821 "graph search visited {visited} nodes, exceeding the fail-closed cap {cap}"
822 ),
823 Self::Cancelled { partial } => {
824 write!(formatter, "graph search was cancelled (partial={partial})")
825 }
826 Self::Timeout { partial } => {
827 write!(
828 formatter,
829 "graph search deadline expired (partial={partial})"
830 )
831 }
832 Self::ReadCancelled { partial } => write!(
833 formatter,
834 "store close cancelled graph search (partial={partial})"
835 ),
836 Self::Scan(error) => error.fmt(formatter),
837 }
838 }
839}
840
841impl std::error::Error for GraphSearchError {
842 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
843 match self {
844 Self::Profile(error) => Some(error),
845 Self::Graph(error) => Some(error),
846 Self::Quant(error) => Some(error),
847 Self::Gather(error) => Some(error),
848 Self::AdaptiveEf(error) => Some(error),
849 Self::Rescore(error) => Some(error),
850 Self::Scan(error) => Some(error),
851 Self::Geometry(_)
852 | Self::ExactRescoreUnavailable(_)
853 | Self::FilteredCandidateShortfall { .. }
854 | Self::VisitedCapExceeded { .. }
855 | Self::Cancelled { .. }
856 | Self::Timeout { .. }
857 | Self::ReadCancelled { .. } => None,
858 }
859 }
860}
861
862impl From<GraphNodeError> for GraphSearchError {
863 fn from(error: GraphNodeError) -> Self {
864 Self::Graph(error)
865 }
866}
867
868impl From<QuantError> for GraphSearchError {
869 fn from(error: QuantError) -> Self {
870 Self::Quant(error)
871 }
872}
873
874impl From<GatherShapeError> for GraphSearchError {
875 fn from(error: GatherShapeError) -> Self {
876 Self::Gather(error)
877 }
878}
879
880impl From<AdaptiveEfError> for GraphSearchError {
881 fn from(error: AdaptiveEfError) -> Self {
882 Self::AdaptiveEf(error)
883 }
884}
885
886impl From<RescoreError> for GraphSearchError {
887 fn from(error: RescoreError) -> Self {
888 Self::Rescore(error)
889 }
890}
891
892#[derive(Clone, Copy, Debug)]
893struct ScoredNode {
894 row_id: CheckedNodeId,
895 distance: f64,
896}
897
898#[derive(Clone, Copy, Debug)]
904pub(crate) struct GraphSegmentNormRange {
905 minimum: f64,
906 maximum: f64,
907}
908
909impl GraphSegmentNormRange {
910 pub(crate) fn from_factors(factors: &[crate::quant::Bit4Factors]) -> Self {
911 let mut minimum = f64::INFINITY;
912 let mut maximum = 0.0_f64;
913 for factors in factors {
914 let [scale, normalized_norm, _] = factors.persisted_fields();
915 if scale < 0.0 || normalized_norm < 0.0 {
916 return Self::unbounded();
917 }
918 let center = f64::from(scale) * f64::from(normalized_norm);
919 let normalized_ulp = f64::from(next_up_f32(normalized_norm) - normalized_norm);
920 let uncertainty = f64::from(scale) * normalized_ulp;
921 minimum = minimum.min((center - uncertainty).max(0.0));
922 maximum = maximum.max(center + uncertainty);
923 }
924 if !minimum.is_finite() || !maximum.is_finite() {
925 return Self::unbounded();
926 }
927 Self { minimum, maximum }
928 }
929
930 pub(crate) fn from_exact_rows(rows: &[f32], dimensions: usize) -> Self {
931 if dimensions == 0 || rows.is_empty() || !rows.len().is_multiple_of(dimensions) {
932 return Self::unbounded();
933 }
934 let relative_error = accumulation_relative_error(dimensions);
935 let mut maximum = 0.0_f64;
936 for row in rows.chunks_exact(dimensions) {
937 let squared_norm = row
938 .iter()
939 .map(|value| {
940 let value = f64::from(*value);
941 value * value
942 })
943 .sum::<f64>();
944 let norm = next_up_f64((squared_norm / (1.0 - relative_error)).max(0.0).sqrt());
945 maximum = maximum.max(norm);
946 }
947 if !maximum.is_finite() {
948 return Self::unbounded();
949 }
950 Self {
951 minimum: 0.0,
952 maximum,
953 }
954 }
955
956 pub(crate) fn from_graph(
957 graph: GraphNodeBlocks<'_>,
958 cancellation: Option<&QueryCancellation<'_>>,
959 ) -> Result<Self, GraphSearchError> {
960 let mut minimum = f64::INFINITY;
961 let mut maximum = 0.0_f64;
962 for raw_row_id in 0..graph.node_count() {
963 if raw_row_id.is_multiple_of(256) {
964 check_cancellation(cancellation)?;
965 }
966 let row_id = graph.checked_node_id(raw_row_id)?;
967 let (_, factors) = graph.score_row_checked(row_id)?;
968 let [scale, normalized_norm, _] = factors.persisted_fields();
969 if scale < 0.0 || normalized_norm < 0.0 {
970 return Ok(Self::unbounded());
971 }
972 let center = f64::from(scale) * f64::from(normalized_norm);
973 let normalized_ulp = f64::from(next_up_f32(normalized_norm) - normalized_norm);
974 let uncertainty = f64::from(scale) * normalized_ulp;
975 minimum = minimum.min((center - uncertainty).max(0.0));
976 maximum = maximum.max(center + uncertainty);
977 }
978 check_cancellation(cancellation)?;
979 if !minimum.is_finite() || !maximum.is_finite() {
980 return Ok(Self::unbounded());
981 }
982 Ok(Self { minimum, maximum })
983 }
984
985 const fn unbounded() -> Self {
986 Self {
987 minimum: 0.0,
988 maximum: f64::INFINITY,
989 }
990 }
991
992 #[cfg(test)]
994 pub(crate) fn endpoint_bits(self) -> (u64, u64) {
995 (self.minimum.to_bits(), self.maximum.to_bits())
996 }
997
998 pub(crate) fn squared_l2_lower_bound(self, query: &[f32]) -> f64 {
1006 if !self.maximum.is_finite() || query.is_empty() {
1007 return 0.0;
1008 }
1009 let squared_norm = query
1010 .iter()
1011 .map(|value| {
1012 let value = f64::from(*value);
1013 value * value
1014 })
1015 .sum::<f64>();
1016 let relative_error = accumulation_relative_error(query.len());
1017 let query_minimum = next_down_f64((squared_norm / (1.0 + relative_error)).max(0.0).sqrt());
1018 let query_maximum = next_up_f64((squared_norm / (1.0 - relative_error)).max(0.0).sqrt());
1019 let gap = if query_maximum < self.minimum {
1020 self.minimum - query_maximum
1021 } else if query_minimum > self.maximum {
1022 query_minimum - self.maximum
1023 } else {
1024 0.0
1025 };
1026 next_down_f64(gap * gap * (1.0 - relative_error))
1027 }
1028
1029 pub(crate) fn squared_l2_upper_bound(self, query: &[f32]) -> f64 {
1032 if !self.maximum.is_finite() || query.is_empty() {
1033 return f64::INFINITY;
1034 }
1035 let squared_norm = query
1036 .iter()
1037 .map(|value| {
1038 let value = f64::from(*value);
1039 value * value
1040 })
1041 .sum::<f64>();
1042 let relative_error = accumulation_relative_error(query.len());
1043 let query_maximum = next_up_f64((squared_norm / (1.0 - relative_error)).max(0.0).sqrt());
1044 next_up_f64(
1045 (query_maximum + self.maximum)
1046 * (query_maximum + self.maximum)
1047 * (1.0 + relative_error),
1048 )
1049 }
1050}
1051
1052fn accumulation_relative_error(terms: usize) -> f64 {
1053 let scaled = terms as f64 * f64::EPSILON;
1054 (scaled / (1.0 - scaled)) * 2.0
1055}
1056
1057fn next_up_f32(value: f32) -> f32 {
1058 if value == f32::INFINITY {
1059 return value;
1060 }
1061 if value == -0.0 {
1062 return f32::from_bits(1);
1063 }
1064 if value >= 0.0 {
1065 f32::from_bits(value.to_bits().saturating_add(1))
1066 } else {
1067 f32::from_bits(value.to_bits().saturating_sub(1))
1068 }
1069}
1070
1071fn next_up_f64(value: f64) -> f64 {
1072 if value == f64::INFINITY {
1073 return value;
1074 }
1075 if value == -0.0 {
1076 return f64::from_bits(1);
1077 }
1078 if value >= 0.0 {
1079 f64::from_bits(value.to_bits().saturating_add(1))
1080 } else {
1081 f64::from_bits(value.to_bits().saturating_sub(1))
1082 }
1083}
1084
1085fn next_down_f64(value: f64) -> f64 {
1086 if value <= 0.0 {
1087 return 0.0;
1088 }
1089 f64::from_bits(value.to_bits().saturating_sub(1))
1090}
1091
1092#[derive(Debug)]
1094pub struct GraphSearchScratch {
1095 node_count: u32,
1096 max_degree: u8,
1097 visited: Vec<u8>,
1098 epoch: u8,
1099 pool: Vec<ScoredNode>,
1100 frontier: Vec<ScoredNode>,
1101 hop_candidates: Vec<CheckedNodeId>,
1102 rescore_row_ids: Vec<u32>,
1103 rescore_coarse_scores: Vec<f32>,
1104}
1105
1106impl GraphSearchScratch {
1107 pub fn new(node_count: u32, max_degree: u8) -> Result<Self, GraphSearchError> {
1109 Self::with_ef_capacity(node_count, max_degree, 0)
1110 }
1111
1112 pub(crate) fn with_ef_capacity(
1113 node_count: u32,
1114 max_degree: u8,
1115 ef: usize,
1116 ) -> Result<Self, GraphSearchError> {
1117 let node_count_usize = node_count as usize;
1118 let mut visited = Vec::new();
1119 visited
1120 .try_reserve_exact(node_count_usize)
1121 .map_err(|error| {
1122 GraphSearchError::Geometry(format!("visited allocation failed: {error}"))
1123 })?;
1124 visited.resize(node_count_usize, 0);
1125 let mut pool = Vec::new();
1126 pool.try_reserve_exact(ef).map_err(|error| {
1127 GraphSearchError::Geometry(format!("pool allocation failed: {error}"))
1128 })?;
1129 let mut frontier = Vec::new();
1130 frontier.try_reserve_exact(ef).map_err(|error| {
1131 GraphSearchError::Geometry(format!("frontier allocation failed: {error}"))
1132 })?;
1133 let mut hop_candidates = Vec::new();
1134 hop_candidates
1135 .try_reserve_exact(usize::from(max_degree))
1136 .map_err(|error| {
1137 GraphSearchError::Geometry(format!("hop-candidate allocation failed: {error}"))
1138 })?;
1139 let mut rescore_row_ids = Vec::new();
1140 rescore_row_ids.try_reserve_exact(ef).map_err(|error| {
1141 GraphSearchError::Geometry(format!("rescore row-id allocation failed: {error}"))
1142 })?;
1143 let mut rescore_coarse_scores = Vec::new();
1144 rescore_coarse_scores
1145 .try_reserve_exact(ef)
1146 .map_err(|error| {
1147 GraphSearchError::Geometry(format!("rescore score allocation failed: {error}"))
1148 })?;
1149 Ok(Self {
1150 node_count,
1151 max_degree,
1152 visited,
1153 epoch: 0,
1154 pool,
1155 frontier,
1156 hop_candidates,
1157 rescore_row_ids,
1158 rescore_coarse_scores,
1159 })
1160 }
1161
1162 pub fn allocation_bytes(
1164 node_count: u32,
1165 max_degree: u8,
1166 ef: usize,
1167 ) -> Result<usize, GraphSearchError> {
1168 let scored = ef
1169 .checked_mul(std::mem::size_of::<ScoredNode>())
1170 .and_then(|bytes| bytes.checked_mul(2))
1171 .ok_or_else(|| GraphSearchError::Geometry("scratch byte count overflow".to_owned()))?;
1172 let hops = usize::from(max_degree)
1173 .checked_mul(std::mem::size_of::<CheckedNodeId>())
1174 .ok_or_else(|| GraphSearchError::Geometry("scratch byte count overflow".to_owned()))?;
1175 let rescore = ef
1176 .checked_mul(std::mem::size_of::<u32>() + std::mem::size_of::<f32>())
1177 .ok_or_else(|| GraphSearchError::Geometry("scratch byte count overflow".to_owned()))?;
1178 (node_count as usize)
1179 .checked_add(scored)
1180 .and_then(|bytes| bytes.checked_add(hops))
1181 .and_then(|bytes| bytes.checked_add(rescore))
1182 .ok_or_else(|| GraphSearchError::Geometry("scratch byte count overflow".to_owned()))
1183 }
1184
1185 pub fn resident_bytes(&self) -> Result<usize, GraphSearchError> {
1187 let widths = [
1188 (self.visited.capacity(), std::mem::size_of::<u8>()),
1189 (self.pool.capacity(), std::mem::size_of::<ScoredNode>()),
1190 (self.frontier.capacity(), std::mem::size_of::<ScoredNode>()),
1191 (
1192 self.hop_candidates.capacity(),
1193 std::mem::size_of::<CheckedNodeId>(),
1194 ),
1195 (self.rescore_row_ids.capacity(), std::mem::size_of::<u32>()),
1196 (
1197 self.rescore_coarse_scores.capacity(),
1198 std::mem::size_of::<f32>(),
1199 ),
1200 ];
1201 widths.iter().try_fold(0_usize, |total, (capacity, width)| {
1202 capacity
1203 .checked_mul(*width)
1204 .and_then(|bytes| total.checked_add(bytes))
1205 .ok_or_else(|| GraphSearchError::Geometry("scratch byte count overflow".to_owned()))
1206 })
1207 }
1208
1209 pub(crate) fn supports(&self, node_count: u32, max_degree: u8, ef: usize) -> bool {
1210 self.node_count == node_count
1211 && self.max_degree == max_degree
1212 && self.pool.capacity() >= ef
1213 && self.frontier.capacity() >= ef
1214 && self.rescore_row_ids.capacity() >= ef
1215 && self.rescore_coarse_scores.capacity() >= ef
1216 }
1217}
1218
1219pub struct GraphSearcher<'a> {
1221 graph: GraphNodeBlocks<'a>,
1222 rescore: &'a [f32],
1223 rescore_validator: Option<&'a dyn RescoreValidator>,
1224 entries: [CheckedNodeId; 4],
1225 scratch: &'a mut GraphSearchScratch,
1226 #[cfg(any(test, feature = "test-support"))]
1227 vector_fault: Option<(
1228 &'a crate::scan::vector_fault::VectorFaultController,
1229 crate::scan::vector_fault::VectorRowSource,
1230 crate::scan::vector_fault::VectorSearchTier,
1231 )>,
1232 #[cfg(any(test, feature = "test-support"))]
1233 hop_cancellation: Option<TestHopCancellation>,
1234}
1235
1236pub(crate) trait RescoreValidator {
1237 fn validate_rows(&self, rows: &[u32]) -> Result<(), String>;
1238}
1239
1240#[cfg(any(test, feature = "test-support"))]
1241#[derive(Debug)]
1242struct TestHopCancellation {
1243 after_hops: usize,
1244 token: crate::lifecycle::CancelToken,
1245 observed_hops: std::rc::Rc<std::cell::Cell<usize>>,
1246}
1247
1248impl<'a> GraphSearcher<'a> {
1249 pub fn new(
1251 graph: GraphNodeBlocks<'a>,
1252 rescore: &'a [f32],
1253 scratch: &'a mut GraphSearchScratch,
1254 ) -> Result<Self, GraphSearchError> {
1255 let entries = Self::discover_entry_row_ids(graph)?;
1256 Self::with_entry_row_ids(graph, rescore, entries, scratch)
1257 }
1258
1259 pub fn discover_entry_row_ids(
1261 graph: GraphNodeBlocks<'_>,
1262 ) -> Result<[u32; 4], GraphSearchError> {
1263 let mut entries = Vec::with_capacity(4);
1264 for row_id in 0..graph.node_count() {
1265 if graph.block(row_id)?.flags() & 1 != 0 {
1266 entries.push(row_id);
1267 }
1268 }
1269 let entry_count = entries.len();
1270 entries.try_into().map_err(|_: Vec<u32>| {
1271 GraphSearchError::Geometry(format!(
1272 "graph contains {entry_count} persisted entry seeds, expected medoid plus three refined seeds"
1273 ))
1274 })
1275 }
1276
1277 pub fn with_entry_row_ids(
1279 graph: GraphNodeBlocks<'a>,
1280 rescore: &'a [f32],
1281 entry_row_ids: [u32; 4],
1282 scratch: &'a mut GraphSearchScratch,
1283 ) -> Result<Self, GraphSearchError> {
1284 let dimensions = graph.layout().dims() as usize;
1285 let expected_rescore = (graph.node_count() as usize)
1286 .checked_mul(dimensions)
1287 .ok_or_else(|| GraphSearchError::Geometry("rescore length overflow".to_owned()))?;
1288 if rescore.len() != expected_rescore {
1289 return Err(GraphSearchError::Geometry(format!(
1290 "rescore has {} values, expected {expected_rescore}",
1291 rescore.len()
1292 )));
1293 }
1294 if scratch.node_count != graph.node_count()
1295 || scratch.max_degree != graph.layout().max_degree()
1296 {
1297 return Err(GraphSearchError::Geometry(format!(
1298 "scratch geometry nodes/degree {}/{}, graph {}/{}",
1299 scratch.node_count,
1300 scratch.max_degree,
1301 graph.node_count(),
1302 graph.layout().max_degree()
1303 )));
1304 }
1305 let mut entries = [None; 4];
1306 for (position, row_id) in entry_row_ids.into_iter().enumerate() {
1307 let block = graph.block(row_id)?;
1308 if block.flags() & 1 == 0 {
1309 return Err(GraphSearchError::Geometry(format!(
1310 "cached entry row {row_id} does not carry the persisted entry flag"
1311 )));
1312 }
1313 let destination = entries.get_mut(position).ok_or_else(|| {
1314 GraphSearchError::Geometry("cached entry position overflow".to_owned())
1315 })?;
1316 *destination = Some(graph.checked_node_id(row_id)?);
1317 }
1318 let entries = entries.map(|entry| {
1319 entry.ok_or_else(|| GraphSearchError::Geometry("cached entry is absent".to_owned()))
1320 });
1321 let [first, second, third, fourth] = entries;
1322 Ok(Self {
1323 graph,
1324 rescore,
1325 rescore_validator: None,
1326 entries: [first?, second?, third?, fourth?],
1327 scratch,
1328 #[cfg(any(test, feature = "test-support"))]
1329 vector_fault: None,
1330 #[cfg(any(test, feature = "test-support"))]
1331 hop_cancellation: None,
1332 })
1333 }
1334
1335 pub(crate) fn with_rescore_validator(mut self, validator: &'a dyn RescoreValidator) -> Self {
1336 self.rescore_validator = Some(validator);
1337 self
1338 }
1339
1340 #[cfg(any(test, feature = "test-support"))]
1341 pub(crate) fn with_vector_fault_controller(
1342 mut self,
1343 controller: &'a crate::scan::vector_fault::VectorFaultController,
1344 source: crate::scan::vector_fault::VectorRowSource,
1345 tier: crate::scan::vector_fault::VectorSearchTier,
1346 ) -> Self {
1347 self.vector_fault = Some((controller, source, tier));
1348 self
1349 }
1350
1351 #[cfg(any(test, feature = "test-support"))]
1352 pub(crate) fn cancel_after_hops(
1353 &mut self,
1354 after_hops: usize,
1355 token: crate::lifecycle::CancelToken,
1356 ) -> std::rc::Rc<std::cell::Cell<usize>> {
1357 let observed_hops = std::rc::Rc::new(std::cell::Cell::new(0));
1358 self.hop_cancellation = Some(TestHopCancellation {
1359 after_hops,
1360 token,
1361 observed_hops: std::rc::Rc::clone(&observed_hops),
1362 });
1363 observed_hops
1364 }
1365
1366 #[cfg(any(test, feature = "test-support"))]
1367 fn observe_hop_for_cancellation(&self, hops: usize) {
1368 let Some(source) = &self.hop_cancellation else {
1369 return;
1370 };
1371 source.observed_hops.set(hops);
1372 if hops == source.after_hops {
1373 source.token.cancel();
1374 }
1375 }
1376
1377 pub fn search(
1379 &mut self,
1380 request: GraphSearchRequest<'_>,
1381 cancellation: Option<&QueryCancellation<'_>>,
1382 ) -> Result<GraphSearchResult, GraphSearchError> {
1383 match self.search_inner(request, None, None, cancellation)? {
1384 SearchInnerOutcome::Traversed(result) => Ok(result),
1385 SearchInnerOutcome::VisitedBudgetExceeded { .. } => Err(GraphSearchError::Geometry(
1386 "unfiltered traversal produced a filtered-budget disposition".to_owned(),
1387 )),
1388 }
1389 }
1390
1391 pub fn search_filtered(
1398 &mut self,
1399 request: GraphSearchRequest<'_>,
1400 allow_list: &DocBitmap,
1401 filtered_visited_budget: usize,
1402 cancellation: Option<&QueryCancellation<'_>>,
1403 ) -> Result<FilteredGraphSearchOutcome, GraphSearchError> {
1404 match self.search_inner(
1405 request,
1406 Some(allow_list),
1407 Some(filtered_visited_budget),
1408 cancellation,
1409 )? {
1410 SearchInnerOutcome::Traversed(result) => {
1411 Ok(FilteredGraphSearchOutcome::Traversed(result))
1412 }
1413 SearchInnerOutcome::VisitedBudgetExceeded {
1414 visited,
1415 budget,
1416 counters,
1417 } => Ok(FilteredGraphSearchOutcome::VisitedBudgetExceeded {
1418 visited,
1419 budget,
1420 counters,
1421 }),
1422 }
1423 }
1424
1425 fn search_inner(
1426 &mut self,
1427 request: GraphSearchRequest<'_>,
1428 allow_list: Option<&DocBitmap>,
1429 filtered_visited_budget: Option<usize>,
1430 cancellation: Option<&QueryCancellation<'_>>,
1431 ) -> Result<SearchInnerOutcome, GraphSearchError> {
1432 let entry = self.begin_traversal(request, cancellation)?;
1433 let prepared = PreparedGraphQuery::new(
1434 request.query,
1435 self.graph.layout().padded_dims() as usize,
1436 request.seed,
1437 )?;
1438 match self.traverse_prepared(
1439 request,
1440 allow_list,
1441 filtered_visited_budget,
1442 cancellation,
1443 entry,
1444 &prepared,
1445 )? {
1446 TraversalOutcome::Retained {
1447 counters,
1448 candidate_sequence,
1449 } => self.finalize_traversal(
1450 request,
1451 allow_list,
1452 cancellation,
1453 counters,
1454 candidate_sequence,
1455 ),
1456 TraversalOutcome::Budget(outcome) => Ok(outcome),
1457 }
1458 }
1459
1460 pub(crate) fn search_reusing(
1461 &mut self,
1462 request: GraphSearchRequest<'_>,
1463 cancellation: Option<&QueryCancellation<'_>>,
1464 context: &mut crate::lifecycle::prepared::PreparedVectorQuery<'_>,
1465 segment: &crate::segment::reader::SegmentReader,
1466 source: crate::ingest::RowSource,
1467 ) -> Result<GraphSearchResult, crate::lifecycle::QueryError> {
1468 use crate::lifecycle::map_graph_error;
1469 let entry = self
1470 .begin_traversal(request, cancellation)
1471 .map_err(map_graph_error)?;
1472 let prepared = context.graph(
1473 request.query,
1474 self.graph.layout().padded_dims() as usize,
1475 request.seed,
1476 )?;
1477 let traversal = self
1478 .traverse_prepared(request, None, None, cancellation, entry, prepared)
1479 .map_err(map_graph_error)?;
1480 let TraversalOutcome::Retained {
1481 counters,
1482 candidate_sequence,
1483 } = traversal
1484 else {
1485 return Err(map_graph_error(GraphSearchError::Geometry(
1486 "unfiltered traversal produced a filtered-budget disposition".to_owned(),
1487 )));
1488 };
1489 let rows = self.rescore;
1492 let dimensions = self.graph.layout().dims() as usize;
1493 let pool = self
1494 .prepare_rescore(request, cancellation, counters)
1495 .map_err(map_graph_error)?;
1496 let rescored = context.rescore_graph(
1497 request.query,
1498 rows,
1499 dimensions,
1500 pool,
1501 request.k,
1502 segment,
1503 source,
1504 )?;
1505 match self
1506 .finish_rescore(
1507 request,
1508 None,
1509 cancellation,
1510 counters,
1511 candidate_sequence,
1512 Ok(rescored),
1513 )
1514 .map_err(map_graph_error)?
1515 {
1516 SearchInnerOutcome::Traversed(result) => Ok(result),
1517 SearchInnerOutcome::VisitedBudgetExceeded { .. } => {
1518 Err(map_graph_error(GraphSearchError::Geometry(
1519 "rescore produced a filtered-budget disposition".to_owned(),
1520 )))
1521 }
1522 }
1523 }
1524
1525 fn begin_traversal(
1526 &self,
1527 request: GraphSearchRequest<'_>,
1528 cancellation: Option<&QueryCancellation<'_>>,
1529 ) -> Result<(usize, QueryQosClass, i32), GraphSearchError> {
1530 let ef = self.validate_request(request)?;
1531 check_cancellation(cancellation)?;
1532 #[cfg(any(test, feature = "test-support"))]
1533 GRAPH_QUERY_TEST_OBSERVATIONS.with(|observations| {
1534 if let Some(observations) = observations.borrow_mut().as_mut() {
1535 observations.traversals += 1;
1536 }
1537 });
1538 let (qos_class, qos_relative_priority) = observed_qos();
1539 Ok((ef, qos_class, qos_relative_priority))
1540 }
1541
1542 #[allow(clippy::too_many_arguments)]
1543 fn traverse_prepared(
1544 &mut self,
1545 request: GraphSearchRequest<'_>,
1546 allow_list: Option<&DocBitmap>,
1547 filtered_visited_budget: Option<usize>,
1548 cancellation: Option<&QueryCancellation<'_>>,
1549 entry: (usize, QueryQosClass, i32),
1550 prepared: &PreparedGraphQuery,
1551 ) -> Result<TraversalOutcome, GraphSearchError> {
1552 let (ef, qos_class, qos_relative_priority) = entry;
1553 let query_norm = prepared.norm;
1554 let prepared = prepared.bit4();
1555 let visited_cap = ef
1556 .checked_mul(usize::from(self.graph.layout().max_degree()))
1557 .and_then(|value| value.checked_mul(4))
1558 .ok_or_else(|| GraphSearchError::Geometry("visited cap overflow".to_owned()))?;
1559 let visited_epoch_cleared = self.next_epoch();
1560 let mut counters = GraphSearchCounters {
1561 effective_ef: ef,
1562 candidates_rescored: 0,
1563 hops: 0,
1564 candidates_scored: 0,
1565 pushes: 0,
1566 visited: 0,
1567 visited_epoch_cleared,
1568 dims_touched: 0,
1569 bytes_read: 0,
1570 qos_class,
1571 qos_relative_priority,
1572 core_class: request.observed_core_class,
1573 };
1574 self.scratch.pool.clear();
1575 self.scratch.frontier.clear();
1576 reserve_exact_scratch(&mut self.scratch.pool, ef, "pool")?;
1577 reserve_exact_scratch(&mut self.scratch.frontier, ef, "frontier")?;
1578 let mut candidate_sequence = request.trace_candidates.then(|| Vec::with_capacity(ef));
1579 let entries = self.entries;
1580 if request.prefetch.is_enabled() {
1581 for entry in &entries {
1582 self.graph.prefetch_line0(*entry);
1583 }
1584 }
1585 let mut seed_group = [None; 4];
1586 let mut seed_count = 0_usize;
1587 for entry in entries {
1588 if mark_visited(&mut self.scratch.visited, self.scratch.epoch, entry)? {
1589 counters.visited += 1;
1590 ensure_visited_cap(counters.visited, visited_cap)?;
1591 if filtered_visited_budget.is_some_and(|budget| counters.visited > budget) {
1592 return self.filtered_budget_outcome(counters, filtered_visited_budget);
1593 }
1594 let Some(slot) = seed_group.get_mut(seed_count) else {
1595 return Err(GraphSearchError::Geometry(
1596 "entry seed count exceeds fixed seed group".to_owned(),
1597 ));
1598 };
1599 *slot = Some(entry);
1600 seed_count += 1;
1601 }
1602 }
1603 self.score_and_push_group(
1604 prepared,
1605 query_norm,
1606 &seed_group,
1607 seed_count,
1608 ef,
1609 allow_list,
1610 request.prefetch,
1611 &mut counters,
1612 &mut candidate_sequence,
1613 )?;
1614 loop {
1615 check_cancellation(cancellation)?;
1616 let Some(candidate) = min_heap_pop(&mut self.scratch.frontier)? else {
1617 break;
1618 };
1619 if self.scratch.pool.len() >= ef
1620 && self
1621 .scratch
1622 .pool
1623 .first()
1624 .is_some_and(|worst| candidate.distance > worst.distance)
1625 {
1626 break;
1627 }
1628 counters.hops += 1;
1629 #[cfg(any(test, feature = "test-support"))]
1630 self.observe_hop_for_cancellation(counters.hops);
1631 let (degree, neighbors) = self.graph.adjacency_checked(candidate.row_id)?;
1632 let degree = usize::from(degree);
1633 self.scratch.hop_candidates.clear();
1634 for raw_neighbor in neighbors.take(degree) {
1635 let neighbor = self.graph.checked_node_id(raw_neighbor)?;
1636 if !mark_visited(&mut self.scratch.visited, self.scratch.epoch, neighbor)? {
1637 continue;
1638 }
1639 counters.visited += 1;
1640 ensure_visited_cap(counters.visited, visited_cap)?;
1641 if filtered_visited_budget.is_some_and(|budget| counters.visited > budget) {
1642 return self.filtered_budget_outcome(counters, filtered_visited_budget);
1643 }
1644 if request.prefetch.is_enabled() {
1645 self.graph.prefetch_line0(neighbor);
1646 }
1647 self.scratch.hop_candidates.push(neighbor);
1648 }
1649 let mut group_start = 0_usize;
1650 let hop_candidate_count = self.scratch.hop_candidates.len();
1651 while group_start < hop_candidate_count {
1652 check_cancellation(cancellation)?;
1653 let group_count = (hop_candidate_count - group_start).min(4);
1654 let mut group = [None; 4];
1655 for lane in 0..group_count {
1656 let source = self.scratch.hop_candidates.get(group_start + lane).copied();
1657 let Some(destination) = group.get_mut(lane) else {
1658 return Err(GraphSearchError::Geometry(
1659 "candidate gather lane is unavailable".to_owned(),
1660 ));
1661 };
1662 *destination = source;
1663 }
1664 self.score_and_push_group(
1665 prepared,
1666 query_norm,
1667 &group,
1668 group_count,
1669 ef,
1670 allow_list,
1671 request.prefetch,
1672 &mut counters,
1673 &mut candidate_sequence,
1674 )?;
1675 group_start += group_count;
1676 }
1677 }
1678 Ok(TraversalOutcome::Retained {
1679 counters,
1680 candidate_sequence,
1681 })
1682 }
1683
1684 fn finalize_traversal(
1685 &mut self,
1686 request: GraphSearchRequest<'_>,
1687 allow_list: Option<&DocBitmap>,
1688 cancellation: Option<&QueryCancellation<'_>>,
1689 counters: GraphSearchCounters,
1690 candidate_sequence: Option<Vec<u32>>,
1691 ) -> Result<SearchInnerOutcome, GraphSearchError> {
1692 let rows = self.rescore;
1693 let dimensions = self.graph.layout().dims() as usize;
1694 let pool = self.prepare_rescore(request, cancellation, counters)?;
1695 let rescored = rescore_top_k(request.query, rows, dimensions, pool, request.k);
1696 self.finish_rescore(
1697 request,
1698 allow_list,
1699 cancellation,
1700 counters,
1701 candidate_sequence,
1702 rescored,
1703 )
1704 }
1705
1706 fn prepare_rescore(
1707 &mut self,
1708 request: GraphSearchRequest<'_>,
1709 cancellation: Option<&QueryCancellation<'_>>,
1710 counters: GraphSearchCounters,
1711 ) -> Result<RescorePool<'_>, GraphSearchError> {
1712 check_cancellation(cancellation)?;
1713 self.scratch.rescore_row_ids.clear();
1714 self.scratch.rescore_coarse_scores.clear();
1715 let retained = self.scratch.pool.len();
1716 reserve_exact_scratch(
1717 &mut self.scratch.rescore_row_ids,
1718 retained,
1719 "rescore row-id",
1720 )?;
1721 reserve_exact_scratch(
1722 &mut self.scratch.rescore_coarse_scores,
1723 retained,
1724 "rescore score",
1725 )?;
1726 for candidate in &self.scratch.pool {
1727 self.scratch.rescore_row_ids.push(candidate.row_id.raw());
1728 let score = -(candidate.distance as f32);
1729 if !score.is_finite() {
1730 return Err(GraphSearchError::Geometry(format!(
1731 "coarse distance for row {} is not representable as f32",
1732 candidate.row_id.raw()
1733 )));
1734 }
1735 self.scratch.rescore_coarse_scores.push(score);
1736 }
1737 if let Some(validator) = self.rescore_validator {
1738 validator
1739 .validate_rows(&self.scratch.rescore_row_ids)
1740 .map_err(GraphSearchError::ExactRescoreUnavailable)?;
1741 #[cfg(any(test, feature = "test-support"))]
1742 GRAPH_QUERY_TEST_OBSERVATIONS.with(|observations| {
1743 if let Some(observations) = observations.borrow_mut().as_mut() {
1744 observations
1745 .validated_pools
1746 .push(self.scratch.rescore_row_ids.clone());
1747 }
1748 });
1749 }
1750 let coarse_bytes_per_row = self
1751 .graph
1752 .layout()
1753 .code_bytes()
1754 .checked_add(12)
1755 .ok_or_else(|| GraphSearchError::Geometry("coarse row bytes overflow".to_owned()))?;
1756 let pool = RescorePool::retained(
1757 &self.scratch.rescore_row_ids,
1758 &self.scratch.rescore_coarse_scores,
1759 RescoreMetric::SquaredL2,
1760 counters.candidates_scored,
1761 coarse_bytes_per_row,
1762 )
1763 .with_prefetch(request.prefetch.is_enabled());
1764 Ok(pool)
1765 }
1766
1767 #[allow(clippy::too_many_arguments)]
1768 fn finish_rescore(
1769 &self,
1770 _request: GraphSearchRequest<'_>,
1771 allow_list: Option<&DocBitmap>,
1772 cancellation: Option<&QueryCancellation<'_>>,
1773 mut counters: GraphSearchCounters,
1774 candidate_sequence: Option<Vec<u32>>,
1775 rescored: Result<crate::quant::RescoreResult, RescoreError>,
1776 ) -> Result<SearchInnerOutcome, GraphSearchError> {
1777 let coarse_bytes_per_row = self
1778 .graph
1779 .layout()
1780 .code_bytes()
1781 .checked_add(12)
1782 .ok_or_else(|| GraphSearchError::Geometry("coarse row bytes overflow".to_owned()))?;
1783 let rescored = match rescored {
1784 Ok(rescored) => rescored,
1785 Err(RescoreError::InsufficientCandidates { k, candidates }) if allow_list.is_some() => {
1786 counters.candidates_rescored = 0;
1787 counters.dims_touched = u64::try_from(counters.candidates_scored)
1788 .ok()
1789 .and_then(|rows| rows.checked_mul(u64::from(self.graph.layout().dims())))
1790 .ok_or_else(|| {
1791 GraphSearchError::Geometry("dimension counter overflow".to_owned())
1792 })?;
1793 counters.bytes_read = counters
1794 .candidates_scored
1795 .checked_mul(coarse_bytes_per_row)
1796 .and_then(|bytes| u64::try_from(bytes).ok())
1797 .ok_or_else(|| {
1798 GraphSearchError::Geometry("byte counter exceeds u64".to_owned())
1799 })?;
1800 return Err(GraphSearchError::FilteredCandidateShortfall {
1801 k,
1802 candidates,
1803 counters,
1804 });
1805 }
1806 Err(error) => return Err(GraphSearchError::Rescore(error)),
1807 };
1808 check_cancellation(cancellation)?;
1809 counters.candidates_rescored = rescored.candidates_rescored;
1810 let touched_rows = counters
1811 .candidates_scored
1812 .checked_add(counters.candidates_rescored)
1813 .ok_or_else(|| GraphSearchError::Geometry("dimension counter overflow".to_owned()))?;
1814 counters.dims_touched = u64::try_from(touched_rows)
1815 .ok()
1816 .and_then(|rows| rows.checked_mul(u64::from(self.graph.layout().dims())))
1817 .ok_or_else(|| GraphSearchError::Geometry("dimension counter overflow".to_owned()))?;
1818 counters.bytes_read = u64::try_from(rescored.bytes.total())
1819 .map_err(|_| GraphSearchError::Geometry("byte counter exceeds u64".to_owned()))?;
1820 let candidates = rescored
1821 .hits
1822 .into_iter()
1823 .map(|hit| {
1824 let row_id = u32::try_from(hit.row_index).map_err(|_| {
1825 GraphSearchError::Geometry("rescored row id exceeds u32".to_owned())
1826 })?;
1827 Ok(GraphSearchCandidate {
1828 row_id,
1829 distance: (-hit.score).max(0.0),
1830 })
1831 })
1832 .collect::<Result<Vec<_>, GraphSearchError>>()?;
1833 Ok(SearchInnerOutcome::Traversed(GraphSearchResult {
1834 candidates,
1835 counters,
1836 candidate_sequence,
1837 }))
1838 }
1839
1840 fn filtered_budget_outcome(
1841 &self,
1842 mut counters: GraphSearchCounters,
1843 filtered_visited_budget: Option<usize>,
1844 ) -> Result<TraversalOutcome, GraphSearchError> {
1845 let budget = filtered_visited_budget.ok_or_else(|| {
1846 GraphSearchError::Geometry(
1847 "filtered budget disposition has no configured budget".to_owned(),
1848 )
1849 })?;
1850 counters.dims_touched = u64::try_from(counters.candidates_scored)
1851 .ok()
1852 .and_then(|rows| rows.checked_mul(u64::from(self.graph.layout().dims())))
1853 .ok_or_else(|| GraphSearchError::Geometry("dimension counter overflow".to_owned()))?;
1854 let coarse_bytes_per_row = self
1855 .graph
1856 .layout()
1857 .code_bytes()
1858 .checked_add(12)
1859 .ok_or_else(|| GraphSearchError::Geometry("coarse row bytes overflow".to_owned()))?;
1860 counters.bytes_read = counters
1861 .candidates_scored
1862 .checked_mul(coarse_bytes_per_row)
1863 .and_then(|bytes| u64::try_from(bytes).ok())
1864 .ok_or_else(|| GraphSearchError::Geometry("byte counter exceeds u64".to_owned()))?;
1865 Ok(TraversalOutcome::Budget(
1866 SearchInnerOutcome::VisitedBudgetExceeded {
1867 visited: counters.visited,
1868 budget,
1869 counters,
1870 },
1871 ))
1872 }
1873
1874 fn validate_request(&self, request: GraphSearchRequest<'_>) -> Result<usize, GraphSearchError> {
1875 let dimensions = self.graph.layout().dims() as usize;
1876 if request.query.len() != dimensions {
1877 return Err(GraphSearchError::Geometry(format!(
1878 "query has {} dimensions, expected {dimensions}",
1879 request.query.len()
1880 )));
1881 }
1882 request
1883 .effective_ef(self.graph.node_count() as usize)
1884 .map_err(Into::into)
1885 }
1886
1887 fn next_epoch(&mut self) -> bool {
1888 self.scratch.epoch = self.scratch.epoch.wrapping_add(1);
1889 if self.scratch.epoch == 0 {
1890 self.scratch.visited.fill(0);
1891 self.scratch.epoch = 1;
1892 return true;
1893 }
1894 false
1895 }
1896
1897 #[allow(clippy::too_many_arguments)]
1898 fn score_and_push_group(
1899 &mut self,
1900 query: &Bit4Query,
1901 query_norm: f64,
1902 row_ids: &[Option<CheckedNodeId>; 4],
1903 row_count: usize,
1904 ef: usize,
1905 allow_list: Option<&DocBitmap>,
1906 prefetch: TraversalPrefetch,
1907 counters: &mut GraphSearchCounters,
1908 candidate_sequence: &mut Option<Vec<u32>>,
1909 ) -> Result<(), GraphSearchError> {
1910 let scored = score_group(&self.graph, query, query_norm, row_ids, row_count)?;
1911 counters.candidates_scored = counters
1912 .candidates_scored
1913 .checked_add(row_count)
1914 .ok_or_else(|| GraphSearchError::Geometry("candidate counter overflow".to_owned()))?;
1915 for candidate in scored.into_iter().flatten() {
1916 let allowed = allow_list.is_none_or(|mask| mask.contains(candidate.row_id.raw()));
1917 #[cfg(any(test, feature = "test-support"))]
1918 if allowed && let Some((controller, source, tier)) = self.vector_fault {
1919 let local_row = usize::try_from(candidate.row_id.raw()).map_err(|_| {
1920 GraphSearchError::Geometry("graph row id exceeds usize".to_owned())
1921 })?;
1922 if controller.after_eligible_row(source, tier, local_row) {
1923 return Err(GraphSearchError::Cancelled { partial: false });
1924 }
1925 }
1926 if let Some(sequence) = candidate_sequence.as_mut() {
1927 sequence.push(candidate.row_id.raw());
1928 }
1929 let mut pushed = false;
1930 if allowed && max_heap_would_keep(&self.scratch.pool, candidate, ef) {
1931 max_heap_insert_bounded(&mut self.scratch.pool, candidate, ef)?;
1932 min_heap_push(&mut self.scratch.frontier, candidate)?;
1933 counters.pushes = counters.pushes.checked_add(1).ok_or_else(|| {
1934 GraphSearchError::Geometry("push counter overflow".to_owned())
1935 })?;
1936 pushed = true;
1937 } else if !allowed
1938 && (self.scratch.pool.len() < ef
1939 || self
1940 .scratch
1941 .pool
1942 .first()
1943 .is_some_and(|worst| candidate.distance <= worst.distance))
1944 {
1945 min_heap_push(&mut self.scratch.frontier, candidate)?;
1946 counters.pushes = counters.pushes.checked_add(1).ok_or_else(|| {
1947 GraphSearchError::Geometry("push counter overflow".to_owned())
1948 })?;
1949 pushed = true;
1950 }
1951 if pushed
1952 && prefetch.is_enabled()
1953 && let Some(head) = self.scratch.frontier.first()
1954 {
1955 self.graph.prefetch_head_block(head.row_id);
1956 }
1957 }
1958 Ok(())
1959 }
1960}
1961
1962fn reserve_exact_scratch<T>(
1963 buffer: &mut Vec<T>,
1964 needed: usize,
1965 what: &'static str,
1966) -> Result<(), GraphSearchError> {
1967 if buffer.capacity() < needed {
1968 buffer.try_reserve_exact(needed).map_err(|error| {
1969 GraphSearchError::Geometry(format!("{what} allocation failed: {error}"))
1970 })?;
1971 }
1972 Ok(())
1973}
1974
1975fn mark_visited(
1976 visited: &mut [u8],
1977 epoch: u8,
1978 row_id: CheckedNodeId,
1979) -> Result<bool, GraphSearchError> {
1980 let marker = visited.get_mut(row_id.raw() as usize).ok_or_else(|| {
1981 GraphSearchError::Geometry(format!("row id {} is unavailable", row_id.raw()))
1982 })?;
1983 if *marker == epoch {
1984 return Ok(false);
1985 }
1986 *marker = epoch;
1987 Ok(true)
1988}
1989
1990fn ensure_visited_cap(visited: usize, cap: usize) -> Result<(), GraphSearchError> {
1991 if visited > cap {
1992 return Err(GraphSearchError::VisitedCapExceeded { visited, cap });
1993 }
1994 Ok(())
1995}
1996
1997fn check_cancellation(
1998 cancellation: Option<&QueryCancellation<'_>>,
1999) -> Result<(), GraphSearchError> {
2000 let Some(cancellation) = cancellation else {
2001 return Ok(());
2002 };
2003 match cancellation.check_graph() {
2004 Ok(()) => Ok(()),
2005 Err(ScanError::Cancelled { partial }) => Err(GraphSearchError::Cancelled { partial }),
2006 Err(ScanError::Timeout { partial }) => Err(GraphSearchError::Timeout { partial }),
2007 Err(ScanError::ReadCancelled { partial }) => {
2008 Err(GraphSearchError::ReadCancelled { partial })
2009 }
2010 Err(error) => Err(GraphSearchError::Scan(error)),
2011 }
2012}
2013
2014fn scored_best_first(left: &ScoredNode, right: &ScoredNode) -> std::cmp::Ordering {
2015 left.distance
2016 .total_cmp(&right.distance)
2017 .then_with(|| left.row_id.raw().cmp(&right.row_id.raw()))
2018}
2019
2020fn score_group(
2021 graph: &GraphNodeBlocks<'_>,
2022 query: &Bit4Query,
2023 query_norm: f64,
2024 row_ids: &[Option<CheckedNodeId>; 4],
2025 row_count: usize,
2026) -> Result<[Option<ScoredNode>; 4], GraphSearchError> {
2027 let first_id = row_ids
2028 .first()
2029 .copied()
2030 .flatten()
2031 .ok_or_else(|| GraphSearchError::Geometry("empty gather group".to_owned()))?;
2032 let (first_row, first_factors) = graph.score_row_checked(first_id)?;
2033 let mut rows = [first_row; 4];
2034 let mut factors = [first_factors; 4];
2035 for lane in 1..row_count {
2036 let row_id =
2037 row_ids.get(lane).copied().flatten().ok_or_else(|| {
2038 GraphSearchError::Geometry(format!("gather lane {lane} is empty"))
2039 })?;
2040 let (row, factors_for_row) = graph.score_row_checked(row_id)?;
2041 let row_slot = rows
2042 .get_mut(lane)
2043 .ok_or_else(|| GraphSearchError::Geometry("gather row lane overflow".to_owned()))?;
2044 *row_slot = row;
2045 let factor_slot = factors
2046 .get_mut(lane)
2047 .ok_or_else(|| GraphSearchError::Geometry("gather factor lane overflow".to_owned()))?;
2048 *factor_slot = factors_for_row;
2049 }
2050 let gathered = Bit4Rows4::from_rows(rows, graph.layout().code_bytes())?;
2051 let (query_codes, query_sum, query_scale_half) = query.kernel_parts();
2052 let mut dots = [0.0_f32; 4];
2053 score_bit4_ptrs(
2054 query_codes,
2055 query_sum,
2056 query_scale_half,
2057 &gathered,
2058 &factors,
2059 &mut dots,
2060 )?;
2061 let mut scored = [None; 4];
2062 for lane in 0..row_count {
2063 let row_id =
2064 row_ids.get(lane).copied().flatten().ok_or_else(|| {
2065 GraphSearchError::Geometry(format!("scored lane {lane} is empty"))
2066 })?;
2067 let dot = f64::from(
2068 *dots
2069 .get(lane)
2070 .ok_or_else(|| GraphSearchError::Geometry("dot lane overflow".to_owned()))?,
2071 );
2072 let norm = factors.get(lane).copied().unwrap_or(first_factors).norm();
2073 let destination = scored
2074 .get_mut(lane)
2075 .ok_or_else(|| GraphSearchError::Geometry("score lane overflow".to_owned()))?;
2076 *destination = Some(ScoredNode {
2077 row_id,
2078 distance: (query_norm + norm * norm - 2.0 * dot).max(0.0),
2079 });
2080 }
2081 Ok(scored)
2082}
2083
2084fn max_heap_would_keep(pool: &[ScoredNode], node: ScoredNode, limit: usize) -> bool {
2085 pool.len() < limit
2086 || pool
2087 .first()
2088 .is_some_and(|worst| scored_best_first(&node, worst).is_lt())
2089}
2090
2091fn max_heap_insert_bounded(
2092 pool: &mut Vec<ScoredNode>,
2093 node: ScoredNode,
2094 limit: usize,
2095) -> Result<(), GraphSearchError> {
2096 if limit == 0 {
2097 return Err(GraphSearchError::Geometry(
2098 "bounded pool capacity is zero".to_owned(),
2099 ));
2100 }
2101 if pool.len() < limit {
2102 pool.push(node);
2103 let child = pool.len().saturating_sub(1);
2104 max_heap_sift_up(pool, child)?;
2105 return Ok(());
2106 }
2107 let root = pool
2108 .first_mut()
2109 .ok_or_else(|| GraphSearchError::Geometry("full pool has no root".to_owned()))?;
2110 *root = node;
2111 max_heap_sift_down(pool, 0)
2112}
2113
2114fn min_heap_push(frontier: &mut Vec<ScoredNode>, node: ScoredNode) -> Result<(), GraphSearchError> {
2115 frontier.push(node);
2116 let child = frontier.len().saturating_sub(1);
2117 min_heap_sift_up(frontier, child)
2118}
2119
2120fn min_heap_pop(frontier: &mut Vec<ScoredNode>) -> Result<Option<ScoredNode>, GraphSearchError> {
2121 let Some(last) = frontier.pop() else {
2122 return Ok(None);
2123 };
2124 let Some(root) = frontier.first_mut() else {
2125 return Ok(Some(last));
2126 };
2127 let result = *root;
2128 *root = last;
2129 min_heap_sift_down(frontier, 0)?;
2130 Ok(Some(result))
2131}
2132
2133fn min_heap_sift_up(heap: &mut [ScoredNode], mut child: usize) -> Result<(), GraphSearchError> {
2134 let start = child;
2135 let moving = heap_node(heap, child)?;
2136 while child != 0 {
2137 let parent = child.saturating_sub(1) / 2;
2138 let parent_node = heap_node(heap, parent)?;
2139 if !scored_best_first(&moving, &parent_node).is_lt() {
2140 break;
2141 }
2142 heap_store(heap, child, parent_node)?;
2143 child = parent;
2144 }
2145 if child != start {
2146 heap_store(heap, child, moving)?;
2147 }
2148 Ok(())
2149}
2150
2151fn max_heap_sift_up(heap: &mut [ScoredNode], mut child: usize) -> Result<(), GraphSearchError> {
2152 let start = child;
2153 let moving = heap_node(heap, child)?;
2154 while child != 0 {
2155 let parent = child.saturating_sub(1) / 2;
2156 let parent_node = heap_node(heap, parent)?;
2157 if !scored_best_first(&moving, &parent_node).is_gt() {
2158 break;
2159 }
2160 heap_store(heap, child, parent_node)?;
2161 child = parent;
2162 }
2163 if child != start {
2164 heap_store(heap, child, moving)?;
2165 }
2166 Ok(())
2167}
2168
2169fn min_heap_sift_down(heap: &mut [ScoredNode], mut parent: usize) -> Result<(), GraphSearchError> {
2170 let start = parent;
2171 let moving = heap_node(heap, parent)?;
2172 loop {
2173 let left = parent
2174 .checked_mul(2)
2175 .and_then(|value| value.checked_add(1))
2176 .ok_or_else(|| GraphSearchError::Geometry("min-heap child overflow".to_owned()))?;
2177 if left >= heap.len() {
2178 break;
2179 }
2180 let right = left.saturating_add(1);
2181 let best_child = if right < heap.len()
2182 && scored_best_first(&heap_node(heap, right)?, &heap_node(heap, left)?).is_lt()
2183 {
2184 right
2185 } else {
2186 left
2187 };
2188 let child_node = heap_node(heap, best_child)?;
2189 if !scored_best_first(&child_node, &moving).is_lt() {
2190 break;
2191 }
2192 heap_store(heap, parent, child_node)?;
2193 parent = best_child;
2194 }
2195 if parent != start {
2196 heap_store(heap, parent, moving)?;
2197 }
2198 Ok(())
2199}
2200
2201fn max_heap_sift_down(heap: &mut [ScoredNode], mut parent: usize) -> Result<(), GraphSearchError> {
2202 let start = parent;
2203 let moving = heap_node(heap, parent)?;
2204 loop {
2205 let left = parent
2206 .checked_mul(2)
2207 .and_then(|value| value.checked_add(1))
2208 .ok_or_else(|| GraphSearchError::Geometry("max-heap child overflow".to_owned()))?;
2209 if left >= heap.len() {
2210 break;
2211 }
2212 let right = left.saturating_add(1);
2213 let worst_child = if right < heap.len()
2214 && scored_best_first(&heap_node(heap, right)?, &heap_node(heap, left)?).is_gt()
2215 {
2216 right
2217 } else {
2218 left
2219 };
2220 let child_node = heap_node(heap, worst_child)?;
2221 if !scored_best_first(&child_node, &moving).is_gt() {
2222 break;
2223 }
2224 heap_store(heap, parent, child_node)?;
2225 parent = worst_child;
2226 }
2227 if parent != start {
2228 heap_store(heap, parent, moving)?;
2229 }
2230 Ok(())
2231}
2232
2233fn heap_node(heap: &[ScoredNode], index: usize) -> Result<ScoredNode, GraphSearchError> {
2234 heap.get(index)
2235 .copied()
2236 .ok_or_else(|| GraphSearchError::Geometry(format!("heap slot {index} is unavailable")))
2237}
2238
2239fn heap_store(
2240 heap: &mut [ScoredNode],
2241 index: usize,
2242 value: ScoredNode,
2243) -> Result<(), GraphSearchError> {
2244 let slot = heap
2245 .get_mut(index)
2246 .ok_or_else(|| GraphSearchError::Geometry(format!("heap slot {index} is unavailable")))?;
2247 *slot = value;
2248 Ok(())
2249}
2250
2251fn squared_norm(values: &[f32]) -> f64 {
2252 values
2253 .iter()
2254 .map(|value| f64::from(*value) * f64::from(*value))
2255 .sum()
2256}
2257
2258#[cfg(target_vendor = "apple")]
2259fn observed_qos() -> (QueryQosClass, i32) {
2260 let mut class = libc::qos_class_t::QOS_CLASS_UNSPECIFIED;
2261 let mut priority = 0_i32;
2262 let status =
2264 unsafe { libc::pthread_get_qos_class_np(libc::pthread_self(), &mut class, &mut priority) };
2265 if status != 0 {
2266 return (QueryQosClass::Unknown, 0);
2267 }
2268 let class = match class {
2269 libc::qos_class_t::QOS_CLASS_USER_INTERACTIVE => QueryQosClass::UserInteractive,
2270 libc::qos_class_t::QOS_CLASS_USER_INITIATED => QueryQosClass::UserInitiated,
2271 libc::qos_class_t::QOS_CLASS_DEFAULT => QueryQosClass::Default,
2272 libc::qos_class_t::QOS_CLASS_UTILITY => QueryQosClass::Utility,
2273 libc::qos_class_t::QOS_CLASS_BACKGROUND => QueryQosClass::Background,
2274 libc::qos_class_t::QOS_CLASS_UNSPECIFIED => QueryQosClass::Unspecified,
2275 };
2276 (class, priority)
2277}
2278
2279#[cfg(not(target_vendor = "apple"))]
2280const fn observed_qos() -> (QueryQosClass, i32) {
2281 (QueryQosClass::Unavailable, 0)
2282}
2283
2284#[cfg(test)]
2285#[allow(
2286 clippy::expect_used,
2287 clippy::indexing_slicing,
2288 clippy::panic,
2289 clippy::unwrap_used
2290)]
2291mod tests {
2292 use std::time::Duration;
2293
2294 use proptest::prelude::*;
2295 use proptest::test_runner::{Config, RngSeed, TestRunner};
2296 use rand::RngCore;
2297
2298 use super::{
2299 AdaptiveEfError, FilteredGraphSearchOutcome, GraphSearchError, GraphSearchProfile,
2300 GraphSearchRequest, GraphSearchScratch, GraphSearcher, GraphSegmentNormRange,
2301 QueryCoreClass, TraversalPrefetch, next_down_f64, next_up_f32, next_up_f64,
2302 };
2303 use crate::graph::block::{
2304 GraphNodeBlockBuild, GraphNodeBlockInput, GraphNodeLayout, decode_node_blocks,
2305 encode_node_blocks,
2306 };
2307 use crate::kernels::GatherShapeError;
2308 use crate::lifecycle::{CancelToken, OpenOptions, QueryCancellation, QueryControl, Store};
2309 use crate::meta::DocBitmap;
2310 use crate::quant::{Bit4Factors, QuantError, RescoreError, quantize_bit4};
2311 use crate::scan::ScanError;
2312
2313 const DIMS: usize = 128;
2314
2315 #[test]
2316 fn adaptive_defaults_follow_the_measured_profile_rules() {
2317 let query = [0.0_f32; DIMS];
2318
2319 let sift = GraphSearchRequest::new(&query, 100, 0x19_05);
2320 assert_eq!(sift.research_floor_ef(), Ok(140));
2321 assert_eq!(sift.effective_ef(1_000_000), Ok(200));
2322 assert_eq!(sift.effective_ef(180), Ok(180));
2323
2324 let angular =
2325 GraphSearchRequest::new(&query, 100, 0x19_05).with_profile(GraphSearchProfile::Angular);
2326 assert_eq!(angular.research_floor_ef(), Ok(400));
2327 assert_eq!(angular.effective_ef(1_000_000), Ok(400));
2328
2329 let explicit = GraphSearchRequest::new(&query, 100, 0x19_05).with_ef(300);
2330 assert_eq!(explicit.effective_ef(1_000_000), Ok(300));
2331 }
2332
2333 #[test]
2334 fn adaptive_ef_overflow_is_typed() {
2335 let request = GraphSearchRequest::new(&[], usize::MAX, 0x19_05);
2336 assert_eq!(
2337 request.effective_ef(usize::MAX),
2338 Err(AdaptiveEfError::ArithmeticOverflow)
2339 );
2340 }
2341
2342 #[test]
2343 fn request_and_search_geometry_rejections_preserve_the_invalid_values() {
2344 let query = vector(0.0);
2345 assert_eq!(
2346 GraphSearchRequest::new(&query, 0, 7).effective_ef(9),
2347 Err(AdaptiveEfError::ZeroK)
2348 );
2349 assert_eq!(
2350 GraphSearchRequest::new(&query, 10, 7).effective_ef(9),
2351 Err(AdaptiveEfError::KExceedsRows { k: 10, rows: 9 })
2352 );
2353 assert_eq!(
2354 GraphSearchRequest::new(&query, 4, 7)
2355 .with_ef(3)
2356 .effective_ef(9),
2357 Err(AdaptiveEfError::ExplicitBelowK { k: 4, ef: 3 })
2358 );
2359 assert_eq!(
2360 GraphSearchRequest::new(&query, 4, 7)
2361 .with_ef(10)
2362 .effective_ef(9),
2363 Err(AdaptiveEfError::ExplicitExceedsRows { ef: 10, rows: 9 })
2364 );
2365
2366 let (encoded, rescore) = best_first_graph_fixture();
2367 let graph = decode_node_blocks(encoded.as_bytes()).expect("fixture graph is valid");
2368 let mut wrong_scratch =
2369 GraphSearchScratch::new(graph.node_count() + 1, graph.layout().max_degree())
2370 .expect("mismatched scratch allocates");
2371 assert!(matches!(
2372 GraphSearcher::new(graph, &rescore, &mut wrong_scratch),
2373 Err(GraphSearchError::Geometry(detail))
2374 if detail.contains("scratch geometry")
2375 ));
2376
2377 let mut scratch = GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2378 .expect("fixture scratch");
2379 assert!(matches!(
2380 GraphSearcher::new(graph, &rescore[..rescore.len() - 1], &mut scratch),
2381 Err(GraphSearchError::Geometry(detail))
2382 if detail.contains("rescore has") && detail.contains("expected")
2383 ));
2384 assert!(matches!(
2385 GraphSearcher::with_entry_row_ids(graph, &rescore, [0, 1, 2, 4], &mut scratch),
2386 Err(GraphSearchError::Geometry(detail))
2387 if detail.contains("cached entry row 4")
2388 ));
2389
2390 let mut searcher =
2391 GraphSearcher::new(graph, &rescore, &mut scratch).expect("valid searcher");
2392 assert!(matches!(
2393 searcher.search(
2394 GraphSearchRequest::new(&query[..DIMS - 1], 1, 7).with_ef(4),
2395 None,
2396 ),
2397 Err(GraphSearchError::Geometry(detail))
2398 if detail.contains("127 dimensions") && detail.contains("expected 128")
2399 ));
2400 let mut non_finite = query.clone();
2401 non_finite[17] = f32::NAN;
2402 assert_eq!(
2403 searcher.search(GraphSearchRequest::new(&non_finite, 1, 7).with_ef(4), None,),
2404 Err(GraphSearchError::Quant(QuantError::NonFinite { index: 17 }))
2405 );
2406 let recovered = searcher
2407 .search(GraphSearchRequest::new(&query, 1, 7).with_ef(4), None)
2408 .expect("a rejected request leaves reusable scratch valid");
2409 assert_eq!(recovered.candidates()[0].row_id(), 8);
2410
2411 let one_entry = factor_graph_fixture(Bit4Factors::from_persisted(1.0, 1.0, 0.0), 1);
2412 let one_entry = decode_node_blocks(one_entry.as_bytes()).expect("one-entry graph is valid");
2413 assert!(matches!(
2414 GraphSearcher::discover_entry_row_ids(one_entry),
2415 Err(GraphSearchError::Geometry(detail)) if detail.contains("contains 1 persisted entry")
2416 ));
2417 }
2418
2419 #[test]
2420 fn segment_norm_bounds_round_outward_or_fail_open() {
2421 let bounded = factor_graph_fixture(Bit4Factors::from_persisted(2.0, 10.0, 0.0), 0);
2422 let bounded = decode_node_blocks(bounded.as_bytes()).expect("bounded graph is valid");
2423 let bounded = GraphSegmentNormRange::from_graph(bounded, None).expect("norm scan succeeds");
2424 let zero_query = vec![0.0_f32; DIMS];
2425 assert!(bounded.squared_l2_lower_bound(&zero_query) <= 400.0);
2426 assert!(bounded.squared_l2_upper_bound(&zero_query) >= 400.0);
2427 assert!(bounded.squared_l2_lower_bound(&zero_query) > 0.0);
2428 assert_eq!(bounded.squared_l2_lower_bound(&[]), 0.0);
2429 assert_eq!(bounded.squared_l2_upper_bound(&[]), f64::INFINITY);
2430
2431 for factors in [
2432 Bit4Factors::from_persisted(-1.0, 1.0, 0.0),
2433 Bit4Factors::from_persisted(f32::MAX, f32::MAX, 0.0),
2434 ] {
2435 let encoded = factor_graph_fixture(factors, 0);
2436 let graph = decode_node_blocks(encoded.as_bytes()).expect("finite factors decode");
2437 let range = GraphSegmentNormRange::from_graph(graph, None).expect("norm scan succeeds");
2438 assert_eq!(range.squared_l2_lower_bound(&zero_query), 0.0);
2439 assert_eq!(range.squared_l2_upper_bound(&zero_query), f64::INFINITY);
2440 }
2441 }
2442
2443 #[test]
2444 fn norm_bound_successors_are_exact_ieee_neighbours() {
2445 assert_eq!(next_up_f32(f32::INFINITY), f32::INFINITY);
2446 assert_eq!(next_up_f32(-0.0).to_bits(), 1);
2447 assert_eq!(next_up_f32(-1.0).to_bits(), (-1.0_f32).to_bits() - 1);
2448 assert_eq!(next_up_f64(f64::INFINITY), f64::INFINITY);
2449 assert_eq!(next_up_f64(-0.0).to_bits(), 1);
2450 assert_eq!(next_up_f64(-1.0).to_bits(), (-1.0_f64).to_bits() - 1);
2451 assert_eq!(next_down_f64(0.0), 0.0);
2452 assert_eq!(next_down_f64(1.0).to_bits(), 1.0_f64.to_bits() - 1);
2453 }
2454
2455 #[test]
2456 fn graph_search_errors_keep_typed_sources_and_actionable_messages() {
2457 let sourced = [
2458 GraphSearchError::from(crate::graph::block::GraphNodeError::InvalidHeader(
2459 "bad graph header".to_owned(),
2460 )),
2461 GraphSearchError::from(QuantError::EmptyVector),
2462 GraphSearchError::from(GatherShapeError::RowBytes {
2463 expected: 64,
2464 actual: 63,
2465 }),
2466 GraphSearchError::from(AdaptiveEfError::ExplicitBelowK { k: 4, ef: 3 }),
2467 GraphSearchError::from(AdaptiveEfError::ZeroK),
2468 GraphSearchError::from(AdaptiveEfError::KExceedsRows { k: 10, rows: 9 }),
2469 GraphSearchError::from(AdaptiveEfError::ExplicitExceedsRows { ef: 10, rows: 9 }),
2470 GraphSearchError::from(AdaptiveEfError::ArithmeticOverflow),
2471 GraphSearchError::from(RescoreError::InsufficientCandidates {
2472 k: 4,
2473 candidates: 3,
2474 }),
2475 GraphSearchError::Scan(ScanError::ZeroDimension),
2476 ];
2477 for error in sourced {
2478 assert!(!error.to_string().is_empty());
2479 assert!(std::error::Error::source(&error).is_some());
2480 }
2481
2482 let unsourced = [
2483 GraphSearchError::Geometry("bad request shape".to_owned()),
2484 GraphSearchError::VisitedCapExceeded {
2485 visited: 17,
2486 cap: 16,
2487 },
2488 GraphSearchError::Cancelled { partial: false },
2489 GraphSearchError::Timeout { partial: false },
2490 GraphSearchError::ReadCancelled { partial: false },
2491 ];
2492 let messages = unsourced
2493 .iter()
2494 .map(ToString::to_string)
2495 .collect::<Vec<_>>();
2496 assert!(messages[0].contains("bad request shape"));
2497 assert!(messages[1].contains("17") && messages[1].contains("16"));
2498 assert!(messages[2].contains("cancelled") && messages[2].contains("partial=false"));
2499 assert!(messages[3].contains("deadline") && messages[3].contains("partial=false"));
2500 assert!(messages[4].contains("store close") && messages[4].contains("partial=false"));
2501 assert!(
2502 unsourced
2503 .iter()
2504 .all(|error| std::error::Error::source(error).is_none())
2505 );
2506 }
2507
2508 #[test]
2509 fn adaptive_ef_stays_between_k_and_rows_under_adversarial_shapes() {
2510 let mut seeded = crate::test_support::seeded_rng(
2511 "graph::search::tests::adaptive_ef_stays_between_k_and_rows_under_adversarial_shapes",
2512 );
2513 let mut runner = TestRunner::new(Config {
2514 cases: 256,
2515 rng_seed: RngSeed::Fixed(seeded.next_u64()),
2516 ..Config::default()
2517 });
2518 let result = runner.run(
2519 &(1_usize..1_000_001, 1_usize..1_000_001, any::<bool>()),
2520 |(rows, raw_k, angular)| {
2521 let k = raw_k.min(rows);
2522 let profile = if angular {
2523 GraphSearchProfile::Angular
2524 } else {
2525 GraphSearchProfile::SiftClass
2526 };
2527 let request = GraphSearchRequest::new(&[], k, 0x19_05).with_profile(profile);
2528 let ef = request.effective_ef(rows).expect("valid shape resolves");
2529 prop_assert!(ef >= k);
2530 prop_assert!(ef <= rows);
2531 Ok(())
2532 },
2533 );
2534
2535 assert!(result.is_ok(), "property result: {result:?}");
2536 }
2537
2538 #[test]
2539 fn traversal_returns_the_same_topk_as_brute_force_at_high_ef() {
2540 let (encoded, rescore) = complete_graph_fixture(12);
2541 let graph = decode_node_blocks(encoded.as_bytes()).expect("fixture graph is valid");
2542 let query = vector(4.25);
2543 let expected = brute_force_topk(&rescore, &query, 5);
2544 let mut scratch = GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2545 .expect("fixture scratch");
2546 let mut searcher =
2547 GraphSearcher::new(graph, &rescore, &mut scratch).expect("fixture geometry is valid");
2548
2549 let result = searcher
2550 .search(GraphSearchRequest::new(&query, 5, 0x19_04), None)
2551 .expect("high-ef traversal succeeds");
2552 let actual = result
2553 .candidates()
2554 .iter()
2555 .map(|candidate| candidate.row_id())
2556 .collect::<Vec<_>>();
2557
2558 assert_eq!(actual, expected);
2559 assert_eq!(result.counters().effective_ef(), 12);
2560 assert_eq!(result.counters().candidates_rescored(), 12);
2561 }
2562
2563 #[test]
2564 fn an_unfiltered_traversal_is_byte_identical_with_the_mask_parameter_absent() {
2565 let (encoded, rescore) = complete_graph_fixture(12);
2566 let graph = decode_node_blocks(encoded.as_bytes()).expect("fixture graph is valid");
2567 let query = vector(4.25);
2568 let request = GraphSearchRequest::new(&query, 5, 0x19_06).with_ef(12);
2569 let mut unfiltered_scratch =
2570 GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2571 .expect("unfiltered scratch");
2572 let mut full_mask_scratch =
2573 GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2574 .expect("full-mask scratch");
2575 let mut unfiltered = GraphSearcher::new(graph, &rescore, &mut unfiltered_scratch)
2576 .expect("unfiltered searcher");
2577 let mut full_mask = GraphSearcher::new(graph, &rescore, &mut full_mask_scratch)
2578 .expect("full-mask searcher");
2579
2580 let expected = unfiltered.search(request, None).expect("unfiltered search");
2581 let actual = full_mask
2582 .search_filtered(
2583 request,
2584 &DocBitmap::full(graph.node_count()),
2585 usize::MAX,
2586 None,
2587 )
2588 .expect("full-mask search");
2589 let FilteredGraphSearchOutcome::Traversed(actual) = actual else {
2590 panic!("an unbounded full mask must traverse");
2591 };
2592
2593 assert_eq!(actual.candidates(), expected.candidates());
2594 assert_eq!(
2595 actual.counters().deterministic_work(),
2596 expected.counters().deterministic_work()
2597 );
2598 assert_eq!(actual.candidate_sequence(), expected.candidate_sequence());
2599 }
2600
2601 #[test]
2602 fn every_filtered_graph_result_satisfies_the_predicate_and_is_alive() {
2603 let (encoded, rescore) = complete_graph_fixture(12);
2604 let graph = decode_node_blocks(encoded.as_bytes()).expect("fixture graph is valid");
2605 let query = vector(4.0);
2606 let allowed = DocBitmap::from_ids([1, 2, 6, 9, 11]);
2607 let mut scratch = GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2608 .expect("filtered scratch");
2609 let mut searcher =
2610 GraphSearcher::new(graph, &rescore, &mut scratch).expect("filtered searcher");
2611
2612 let outcome = searcher
2613 .search_filtered(
2614 GraphSearchRequest::new(&query, 5, 0x19_06).with_ef(12),
2615 &allowed,
2616 usize::MAX,
2617 None,
2618 )
2619 .expect("filtered graph search");
2620 let FilteredGraphSearchOutcome::Traversed(result) = outcome else {
2621 panic!("an unbounded complete graph must traverse");
2622 };
2623
2624 assert_eq!(
2625 result
2626 .candidates()
2627 .iter()
2628 .map(|candidate| candidate.row_id())
2629 .collect::<Vec<_>>(),
2630 vec![2, 6, 1, 9, 11]
2631 );
2632 assert!(
2633 result
2634 .candidates()
2635 .iter()
2636 .all(|candidate| allowed.contains(candidate.row_id()))
2637 );
2638 assert!(
2639 result
2640 .candidates()
2641 .iter()
2642 .all(|candidate| candidate.row_id() != 4),
2643 "the exact nearest row is planted outside the effective alive-and-filter mask"
2644 );
2645 assert_eq!(
2646 result.counters().candidates_rescored(),
2647 allowed.cardinality() as usize
2648 );
2649
2650 let mut seeded = crate::test_support::seeded_rng(
2651 "graph::search::tests::every_filtered_graph_result_satisfies_the_predicate_and_is_alive",
2652 );
2653 let mut runner = TestRunner::new(Config {
2654 cases: 1_024,
2655 rng_seed: RngSeed::Fixed(seeded.next_u64()),
2656 ..Config::default()
2657 });
2658 let property = runner.run(
2659 &(
2660 prop::collection::vec(any::<bool>(), 12),
2661 prop::collection::vec(any::<bool>(), 12),
2662 ),
2663 |(predicate_matches, alive_rows)| {
2664 let effective = DocBitmap::from_ids(
2665 predicate_matches
2666 .iter()
2667 .zip(&alive_rows)
2668 .enumerate()
2669 .filter_map(|(row, (matches, alive))| {
2670 (*matches && *alive).then_some(row as u32)
2671 }),
2672 );
2673 prop_assume!(!effective.is_empty());
2674 let k = effective.cardinality() as usize;
2675 let mut scratch =
2676 GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2677 .expect("property scratch");
2678 let mut searcher =
2679 GraphSearcher::new(graph, &rescore, &mut scratch).expect("property searcher");
2680 let outcome = searcher
2681 .search_filtered(
2682 GraphSearchRequest::new(&query, k, 0x19_0602).with_ef(12),
2683 &effective,
2684 usize::MAX,
2685 None,
2686 )
2687 .expect("property filtered traversal");
2688 let FilteredGraphSearchOutcome::Traversed(result) = outcome else {
2689 return Err(TestCaseError::fail("unbounded complete graph abandoned"));
2690 };
2691 prop_assert_eq!(result.candidates().len(), k);
2692 let all_valid = result.candidates().iter().all(|candidate| {
2693 let row = candidate.row_id() as usize;
2694 effective.contains(candidate.row_id())
2695 && predicate_matches[row]
2696 && alive_rows[row]
2697 });
2698 prop_assert!(all_valid);
2699 Ok(())
2700 },
2701 );
2702 assert!(property.is_ok(), "property result: {property:?}");
2703 }
2704
2705 #[test]
2706 fn vector_row_checkpoint_counts_only_filter_accepted_graph_rows() {
2707 let (encoded, rescore) = complete_graph_fixture(12);
2708 let graph = decode_node_blocks(encoded.as_bytes()).expect("fixture graph is valid");
2709 let query = vector(4.0);
2710 let request = GraphSearchRequest::new(&query, 1, 0x27_24)
2711 .with_ef(12)
2712 .with_candidate_trace();
2713 let mut trace_scratch =
2714 GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2715 .expect("trace scratch");
2716 let mut trace =
2717 GraphSearcher::new(graph, &rescore, &mut trace_scratch).expect("trace searcher");
2718 let trace_result = trace.search(request, None).expect("trace traversal");
2719 let sequence = trace_result.candidate_sequence().expect("candidate trace");
2720 let first = *sequence.first().expect("first scored row");
2721 let accepted = *sequence
2722 .iter()
2723 .find(|&&row| row != first)
2724 .expect("later filter-accepted row");
2725 let allow_list = DocBitmap::from_ids([accepted]);
2726 let controller = crate::scan::vector_fault::VectorFaultController::armed(
2727 crate::scan::vector_fault::VectorFault::CancelAfterRows {
2728 source: crate::scan::vector_fault::VectorRowSource::Sealed([0x27; 16]),
2729 requested_rows: 1,
2730 tier: crate::scan::vector_fault::VectorSearchTier::Graph,
2731 },
2732 27_240,
2733 );
2734 let mut scratch = GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2735 .expect("fault scratch");
2736 let mut searcher = GraphSearcher::new(graph, &rescore, &mut scratch)
2737 .expect("fault searcher")
2738 .with_vector_fault_controller(
2739 &controller,
2740 crate::scan::vector_fault::VectorRowSource::Sealed([0x27; 16]),
2741 crate::scan::vector_fault::VectorSearchTier::Graph,
2742 );
2743 let error = searcher
2744 .search_filtered(request, &allow_list, usize::MAX, None)
2745 .expect_err("first filter-accepted row must cancel");
2746 assert_eq!(error, GraphSearchError::Cancelled { partial: false });
2747 controller.finalize_search(false);
2748 let receipts = controller.take_typed_receipts();
2749 assert_eq!(receipts.len(), 1);
2750 assert!(
2751 matches!(
2752 receipts[0].effect(),
2753 crate::scan::vector_fault::VectorFaultEffect::CancelledAfterRows {
2754 local_row,
2755 requested_rows: 1,
2756 observed_rows: 1,
2757 ..
2758 } if *local_row == accepted
2759 ),
2760 "filtered-out row {first} incorrectly consumed the eligible-row checkpoint: {:?}",
2761 receipts[0].effect()
2762 );
2763 }
2764
2765 #[test]
2766 fn traversal_expands_the_nearest_frontier_node_before_insertion_order() {
2767 let (encoded, rescore) = best_first_graph_fixture();
2768 let graph = decode_node_blocks(encoded.as_bytes()).expect("fixture graph is valid");
2769 let query = vector(0.0);
2770 let mut scratch = GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2771 .expect("fixture scratch");
2772 let mut searcher =
2773 GraphSearcher::new(graph, &rescore, &mut scratch).expect("fixture geometry is valid");
2774
2775 let result = searcher
2776 .search(
2777 GraphSearchRequest::new(&query, 1, 0x19_04_bf)
2778 .with_ef(4)
2779 .with_observed_core_class(QueryCoreClass::Performance)
2780 .with_candidate_trace(),
2781 None,
2782 )
2783 .expect("best-first traversal reaches the hidden nearest row");
2784
2785 assert_eq!(
2786 result
2787 .candidates()
2788 .iter()
2789 .map(|candidate| candidate.row_id())
2790 .collect::<Vec<_>>(),
2791 vec![8]
2792 );
2793 assert_eq!(result.candidate_sequence(), Some(&[0, 1, 2, 3, 8][..]));
2794 assert_eq!(result.counters().hops(), 4);
2795 assert_eq!(result.counters().pushes(), 5);
2796 assert_eq!(result.counters().core_class(), QueryCoreClass::Performance);
2797 assert_eq!(result.counters().candidates_rescored(), 4);
2798 assert!(result.counters().candidates_scored() < 9);
2799 }
2800
2801 #[test]
2802 fn visited_epoch_wraps_correctly_at_255_queries() {
2803 let (encoded, rescore) = complete_graph_fixture(12);
2804 let graph = decode_node_blocks(encoded.as_bytes()).expect("fixture graph is valid");
2805 let query = vector(4.25);
2806 let mut scratch = GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2807 .expect("fixture scratch");
2808 let mut searcher =
2809 GraphSearcher::new(graph, &rescore, &mut scratch).expect("fixture geometry is valid");
2810
2811 for query_index in 0..255 {
2812 let result = searcher
2813 .search(
2814 GraphSearchRequest::new(&query, 5, query_index).with_ef(12),
2815 None,
2816 )
2817 .expect("epoch before wrap succeeds");
2818 assert!(!result.counters().visited_epoch_cleared());
2819 }
2820 let wrapped = searcher
2821 .search(GraphSearchRequest::new(&query, 5, 255).with_ef(12), None)
2822 .expect("first query after 255 epochs succeeds");
2823
2824 assert!(wrapped.counters().visited_epoch_cleared());
2825 assert_eq!(
2826 wrapped
2827 .candidates()
2828 .iter()
2829 .map(|candidate| candidate.row_id())
2830 .collect::<Vec<_>>(),
2831 brute_force_topk(&rescore, &query, 5)
2832 );
2833 }
2834
2835 #[test]
2836 fn no_node_is_scored_twice_within_one_query() {
2837 let (encoded, rescore) = complete_graph_fixture(12);
2838 let graph = decode_node_blocks(encoded.as_bytes()).expect("fixture graph is valid");
2839 let query = vector(4.25);
2840 let mut scratch = GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2841 .expect("fixture scratch");
2842 let mut searcher =
2843 GraphSearcher::new(graph, &rescore, &mut scratch).expect("fixture geometry is valid");
2844
2845 let result = searcher
2846 .search(
2847 GraphSearchRequest::new(&query, 5, 0x19_04)
2848 .with_ef(12)
2849 .with_candidate_trace(),
2850 None,
2851 )
2852 .expect("traced traversal succeeds");
2853 let sequence = result
2854 .candidate_sequence()
2855 .expect("candidate tracing was requested");
2856 let mut seen = [false; 12];
2857 for row_id in sequence {
2858 let slot = &mut seen[*row_id as usize];
2859 assert!(!*slot, "row {row_id} was scored twice");
2860 *slot = true;
2861 }
2862
2863 assert_eq!(sequence.len(), result.counters().candidates_scored());
2864 assert_eq!(sequence.len(), result.counters().visited());
2865 assert_eq!(sequence, &(0_u32..12).collect::<Vec<_>>());
2866 }
2867
2868 #[test]
2869 fn traversal_is_deterministic_under_seed() {
2870 let (encoded, rescore) = complete_graph_fixture(12);
2871 let graph = decode_node_blocks(encoded.as_bytes()).expect("fixture graph is valid");
2872 let query = vector(4.25);
2873 let request = GraphSearchRequest::new(&query, 5, 0x19_0400)
2874 .with_ef(9)
2875 .with_candidate_trace();
2876 let mut first_scratch =
2877 GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2878 .expect("first scratch");
2879 let mut second_scratch =
2880 GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2881 .expect("second scratch");
2882 let mut first = GraphSearcher::new(graph, &rescore, &mut first_scratch)
2883 .expect("fixture geometry is valid");
2884 let mut second = GraphSearcher::new(graph, &rescore, &mut second_scratch)
2885 .expect("fixture geometry is valid");
2886
2887 let first_result = first
2888 .search(request, None)
2889 .expect("first traversal succeeds");
2890 let second_result = second
2891 .search(request, None)
2892 .expect("second traversal succeeds");
2893
2894 assert_eq!(
2895 first_result.candidate_sequence(),
2896 second_result.candidate_sequence()
2897 );
2898 assert_eq!(
2899 first_result.counters().deterministic_work(),
2900 second_result.counters().deterministic_work()
2901 );
2902 assert_eq!(first_result.candidates(), second_result.candidates());
2903 }
2904
2905 #[test]
2906 fn prefetch_switch_preserves_results_work_and_candidate_sequence() {
2907 let (encoded, rescore) = complete_graph_fixture(12);
2908 let graph = decode_node_blocks(encoded.as_bytes()).expect("fixture graph is valid");
2909 let query = vector(4.25);
2910 let mut enabled_scratch =
2911 GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2912 .expect("enabled scratch");
2913 let mut disabled_scratch =
2914 GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2915 .expect("disabled scratch");
2916 let mut enabled =
2917 GraphSearcher::new(graph, &rescore, &mut enabled_scratch).expect("enabled searcher");
2918 let mut disabled =
2919 GraphSearcher::new(graph, &rescore, &mut disabled_scratch).expect("disabled searcher");
2920 let enabled_result = enabled
2921 .search(
2922 GraphSearchRequest::new(&query, 5, 0x19_0400)
2923 .with_ef(9)
2924 .with_candidate_trace(),
2925 None,
2926 )
2927 .expect("prefetch-enabled traversal succeeds");
2928 let disabled_result = disabled
2929 .search(
2930 GraphSearchRequest::new(&query, 5, 0x19_0400)
2931 .with_ef(9)
2932 .with_candidate_trace()
2933 .with_prefetch(TraversalPrefetch::Disabled),
2934 None,
2935 )
2936 .expect("prefetch-disabled traversal succeeds");
2937
2938 assert_eq!(
2939 enabled_result.candidate_sequence(),
2940 disabled_result.candidate_sequence()
2941 );
2942 assert_eq!(
2943 enabled_result.counters().deterministic_work(),
2944 disabled_result.counters().deterministic_work()
2945 );
2946 assert_eq!(enabled_result.candidates(), disabled_result.candidates());
2947 }
2948
2949 #[test]
2950 fn visited_cap_returns_a_typed_error_not_a_wrong_answer() {
2951 let (encoded, rescore) = chain_graph_fixture(10);
2952 let graph = decode_node_blocks(encoded.as_bytes()).expect("fixture graph is valid");
2953 let query = vector(9.0);
2954 let mut scratch = GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2955 .expect("fixture scratch");
2956 let mut searcher =
2957 GraphSearcher::new(graph, &rescore, &mut scratch).expect("fixture geometry is valid");
2958
2959 let error = searcher
2960 .search(
2961 GraphSearchRequest::new(&query, 1, 0x19_0405).with_ef(1),
2962 None,
2963 )
2964 .expect_err("the visited guard must fail closed");
2965
2966 assert_eq!(
2967 error,
2968 super::GraphSearchError::VisitedCapExceeded { visited: 5, cap: 4 }
2969 );
2970 }
2971
2972 #[test]
2973 fn filtered_budget_and_fail_closed_visited_cap_fire_independently() {
2974 let (encoded, rescore) = chain_graph_fixture(10);
2975 let graph = decode_node_blocks(encoded.as_bytes()).expect("fixture graph is valid");
2976 let query = vector(9.0);
2977 let allowed = DocBitmap::full(graph.node_count());
2978 let request = GraphSearchRequest::new(&query, 1, 0x19_0605).with_ef(1);
2979
2980 let mut budget_scratch =
2981 GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2982 .expect("budget scratch");
2983 let mut budget_searcher =
2984 GraphSearcher::new(graph, &rescore, &mut budget_scratch).expect("budget searcher");
2985 let budget_outcome = budget_searcher
2986 .search_filtered(request, &allowed, 2, None)
2987 .expect("selectivity budget is a normal filtered disposition");
2988 assert!(matches!(
2989 budget_outcome,
2990 FilteredGraphSearchOutcome::VisitedBudgetExceeded {
2991 visited: 3,
2992 budget: 2,
2993 ..
2994 }
2995 ));
2996
2997 let mut invariant_scratch =
2998 GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
2999 .expect("invariant scratch");
3000 let mut invariant_searcher = GraphSearcher::new(graph, &rescore, &mut invariant_scratch)
3001 .expect("invariant searcher");
3002 let error = invariant_searcher
3003 .search_filtered(request, &allowed, usize::MAX, None)
3004 .expect_err("removing the selectivity budget must expose the invariant guard");
3005 assert_eq!(
3006 error,
3007 GraphSearchError::VisitedCapExceeded { visited: 5, cap: 4 }
3008 );
3009 }
3010
3011 #[test]
3012 fn cancellation_is_visible_across_threads_during_traversal() {
3013 let (encoded, rescore) = long_chain_graph_fixture(40_000);
3014 let graph = decode_node_blocks(encoded.as_bytes()).expect("fixture graph is valid");
3015 let query = vec![0.0_f32; DIMS];
3016 let mut scratch = GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
3017 .expect("fixture scratch");
3018 let mut searcher =
3019 GraphSearcher::new(graph, &rescore, &mut scratch).expect("fixture geometry is valid");
3020 let directory = tempfile::tempdir().expect("temporary store directory");
3021 let store = Store::open(directory.path(), OpenOptions::default()).expect("store opens");
3022 let lease = store.snapshot().expect("snapshot lease");
3023 let token = CancelToken::new();
3024 let control = QueryControl::Cancel(token.clone());
3025 let cancellation = QueryCancellation::new(&control, &lease);
3026 let error = std::thread::scope(|scope| {
3027 let canceller = scope.spawn(|| {
3028 std::thread::sleep(Duration::from_millis(2));
3029 token.cancel();
3030 });
3031 let result = searcher.search(
3032 GraphSearchRequest::new(&query, 1, 0x19_0406).with_ef(200),
3033 Some(&cancellation),
3034 );
3035 canceller.join().expect("canceller thread joins");
3036 result
3037 })
3038 .expect_err("in-flight traversal must stop");
3039
3040 assert_eq!(error, super::GraphSearchError::Cancelled { partial: false });
3041 }
3042
3043 #[test]
3044 fn pre_cancelled_token_is_rejected_at_search_entry() {
3045 let (encoded, rescore) = long_chain_graph_fixture(16);
3046 let graph = decode_node_blocks(encoded.as_bytes()).expect("fixture graph is valid");
3047 let query = vec![0.0_f32; DIMS];
3048 let mut scratch = GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
3049 .expect("fixture scratch");
3050 let mut searcher =
3051 GraphSearcher::new(graph, &rescore, &mut scratch).expect("fixture geometry is valid");
3052 let directory = tempfile::tempdir().expect("temporary store directory");
3053 let store = Store::open(directory.path(), OpenOptions::default()).expect("store opens");
3054 let lease = store.snapshot().expect("snapshot lease");
3055 let token = CancelToken::new();
3056 token.cancel();
3057 let control = QueryControl::Cancel(token);
3058 let cancellation = QueryCancellation::new(&control, &lease);
3059
3060 let error = searcher
3061 .search(
3062 GraphSearchRequest::new(&query, 1, 0x19_0406).with_ef(16),
3063 Some(&cancellation),
3064 )
3065 .expect_err("pre-cancelled search must stop at entry");
3066
3067 assert_eq!(error, GraphSearchError::Cancelled { partial: false });
3068 }
3069
3070 #[test]
3071 fn counting_token_cancels_after_exact_number_of_hops() {
3072 let (encoded, rescore) = long_chain_graph_fixture(40_000);
3073 let graph = decode_node_blocks(encoded.as_bytes()).expect("fixture graph is valid");
3074 let query = vec![0.0_f32; DIMS];
3075 let mut scratch = GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
3076 .expect("fixture scratch");
3077 let mut searcher =
3078 GraphSearcher::new(graph, &rescore, &mut scratch).expect("fixture geometry is valid");
3079 let directory = tempfile::tempdir().expect("temporary store directory");
3080 let store = Store::open(directory.path(), OpenOptions::default()).expect("store opens");
3081 let lease = store.snapshot().expect("snapshot lease");
3082 let token = CancelToken::new();
3083 let observed_hops = searcher.cancel_after_hops(7, token.clone());
3084 let control = QueryControl::Cancel(token);
3085 let cancellation = QueryCancellation::new(&control, &lease);
3086
3087 let error = searcher
3088 .search(
3089 GraphSearchRequest::new(&query, 1, 0x19_0406).with_ef(200),
3090 Some(&cancellation),
3091 )
3092 .expect_err("counting token must cancel during traversal");
3093
3094 assert_eq!(error, GraphSearchError::Cancelled { partial: false });
3095 assert_eq!(observed_hops.get(), 7);
3096 }
3097
3098 #[test]
3099 fn recall_is_monotonic_in_ef() {
3100 let (encoded, rescore) = complete_graph_fixture(12);
3101 let graph = decode_node_blocks(encoded.as_bytes()).expect("fixture graph is valid");
3102 let mut seeded =
3103 crate::test_support::seeded_rng("graph::search::tests::recall_is_monotonic_in_ef");
3104 let mut runner = TestRunner::new(Config {
3105 cases: 64,
3106 rng_seed: RngSeed::Fixed(seeded.next_u64()),
3107 ..Config::default()
3108 });
3109 let result = runner.run(&(5_usize..12, 1_usize..8, -1.0_f32..13.0), |input| {
3110 let (ef1, delta, query_value) = input;
3111 let ef2 = ef1.saturating_add(delta).min(12);
3112 prop_assume!(ef1 < ef2);
3113 let query = vector(query_value);
3114 let truth = brute_force_topk(&rescore, &query, 5);
3115 let mut first_scratch =
3116 GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
3117 .expect("first scratch");
3118 let mut second_scratch =
3119 GraphSearchScratch::new(graph.node_count(), graph.layout().max_degree())
3120 .expect("second scratch");
3121 let mut first =
3122 GraphSearcher::new(graph, &rescore, &mut first_scratch).expect("first searcher");
3123 let mut second =
3124 GraphSearcher::new(graph, &rescore, &mut second_scratch).expect("second searcher");
3125 let result1 = first
3126 .search(
3127 GraphSearchRequest::new(&query, 5, 0x19_0408).with_ef(ef1),
3128 None,
3129 )
3130 .expect("ef1 traversal");
3131 let result2 = second
3132 .search(
3133 GraphSearchRequest::new(&query, 5, 0x19_0408).with_ef(ef2),
3134 None,
3135 )
3136 .expect("ef2 traversal");
3137 prop_assert!(result2.recall_against(&truth) >= result1.recall_against(&truth));
3138 Ok(())
3139 });
3140
3141 assert!(result.is_ok(), "property result: {result:?}");
3142 }
3143
3144 fn complete_graph_fixture(rows: usize) -> (crate::graph::block::EncodedNodeBlocks, Vec<f32>) {
3145 let layout = GraphNodeLayout::new(DIMS as u32, DIMS as u32, (rows - 1) as u8)
3146 .expect("fixture layout is valid");
3147 let rescore = (0..rows)
3148 .flat_map(|row| vector(row as f32))
3149 .collect::<Vec<_>>();
3150 let mut codes = vec![0_u8; rows * DIMS.div_ceil(2)];
3151 let mut factors = Vec::with_capacity(rows);
3152 for (row, destination) in rescore
3153 .chunks_exact(DIMS)
3154 .zip(codes.chunks_exact_mut(DIMS.div_ceil(2)))
3155 {
3156 factors.push(quantize_bit4(row, destination).expect("fixture row is finite"));
3157 }
3158 let neighbors = (0..rows)
3159 .map(|row| {
3160 (0..rows)
3161 .filter(|candidate| *candidate != row)
3162 .map(|candidate| candidate as u32)
3163 .collect::<Vec<_>>()
3164 })
3165 .collect::<Vec<_>>();
3166 let nodes = (0..rows)
3167 .map(|row| GraphNodeBlockInput {
3168 codes: &codes[row * DIMS.div_ceil(2)..(row + 1) * DIMS.div_ceil(2)],
3169 factors: factors[row],
3170 flags: u8::from(row < 4),
3171 neighbors: &neighbors[row],
3172 })
3173 .collect::<Vec<_>>();
3174 let encoded = encode_node_blocks(GraphNodeBlockBuild {
3175 layout,
3176 nodes: &nodes,
3177 })
3178 .expect("fixture graph encodes");
3179 (encoded, rescore)
3180 }
3181
3182 fn chain_graph_fixture(rows: usize) -> (crate::graph::block::EncodedNodeBlocks, Vec<f32>) {
3183 let layout =
3184 GraphNodeLayout::new(DIMS as u32, DIMS as u32, 1).expect("fixture layout is valid");
3185 let rescore = (0..rows)
3186 .flat_map(|row| vector(row as f32))
3187 .collect::<Vec<_>>();
3188 let mut codes = vec![0_u8; rows * DIMS.div_ceil(2)];
3189 let mut factors = Vec::with_capacity(rows);
3190 for (row, destination) in rescore
3191 .chunks_exact(DIMS)
3192 .zip(codes.chunks_exact_mut(DIMS.div_ceil(2)))
3193 {
3194 factors.push(quantize_bit4(row, destination).expect("fixture row is finite"));
3195 }
3196 let neighbors = (0..rows)
3197 .map(|row| {
3198 (row + 1 < rows)
3199 .then_some((row + 1) as u32)
3200 .into_iter()
3201 .collect::<Vec<_>>()
3202 })
3203 .collect::<Vec<_>>();
3204 let nodes = (0..rows)
3205 .map(|row| GraphNodeBlockInput {
3206 codes: &codes[row * DIMS.div_ceil(2)..(row + 1) * DIMS.div_ceil(2)],
3207 factors: factors[row],
3208 flags: u8::from(row < 4),
3209 neighbors: &neighbors[row],
3210 })
3211 .collect::<Vec<_>>();
3212 let encoded = encode_node_blocks(GraphNodeBlockBuild {
3213 layout,
3214 nodes: &nodes,
3215 })
3216 .expect("fixture graph encodes");
3217 (encoded, rescore)
3218 }
3219
3220 fn best_first_graph_fixture() -> (crate::graph::block::EncodedNodeBlocks, Vec<f32>) {
3221 let values = [10.0_f32, 9.0, 8.0, 1.0, 7.0, 6.0, 5.0, 4.0, 0.0];
3222 let layout =
3223 GraphNodeLayout::new(DIMS as u32, DIMS as u32, 4).expect("best-first layout is valid");
3224 let rescore = values.into_iter().flat_map(vector).collect::<Vec<_>>();
3225 let mut codes = vec![0_u8; values.len() * DIMS.div_ceil(2)];
3226 let factors = rescore
3227 .chunks_exact(DIMS)
3228 .zip(codes.chunks_exact_mut(DIMS.div_ceil(2)))
3229 .map(|(row, destination)| {
3230 quantize_bit4(row, destination).expect("best-first row is finite")
3231 })
3232 .collect::<Vec<_>>();
3233 let neighbors = [
3234 vec![4, 5, 6, 7],
3235 Vec::new(),
3236 Vec::new(),
3237 vec![8],
3238 Vec::new(),
3239 Vec::new(),
3240 Vec::new(),
3241 Vec::new(),
3242 Vec::new(),
3243 ];
3244 let nodes = (0..values.len())
3245 .map(|row| GraphNodeBlockInput {
3246 codes: &codes[row * DIMS.div_ceil(2)..(row + 1) * DIMS.div_ceil(2)],
3247 factors: factors[row],
3248 flags: u8::from(row < 4),
3249 neighbors: &neighbors[row],
3250 })
3251 .collect::<Vec<_>>();
3252 let encoded = encode_node_blocks(GraphNodeBlockBuild {
3253 layout,
3254 nodes: &nodes,
3255 })
3256 .expect("best-first graph encodes");
3257 (encoded, rescore)
3258 }
3259
3260 fn factor_graph_fixture(
3261 factors: Bit4Factors,
3262 flags: u8,
3263 ) -> crate::graph::block::EncodedNodeBlocks {
3264 let layout = GraphNodeLayout::new(DIMS as u32, DIMS as u32, 0)
3265 .expect("factor graph layout is valid");
3266 let codes = vec![0_u8; DIMS.div_ceil(2)];
3267 let node = GraphNodeBlockInput {
3268 codes: &codes,
3269 factors,
3270 flags,
3271 neighbors: &[],
3272 };
3273 encode_node_blocks(GraphNodeBlockBuild {
3274 layout,
3275 nodes: &[node],
3276 })
3277 .expect("factor graph encodes")
3278 }
3279
3280 fn long_chain_graph_fixture(rows: usize) -> (crate::graph::block::EncodedNodeBlocks, Vec<f32>) {
3281 let layout =
3282 GraphNodeLayout::new(DIMS as u32, DIMS as u32, 44).expect("fixture layout is valid");
3283 let codes = vec![0_u8; rows * DIMS.div_ceil(2)];
3284 let factors = (0..rows)
3285 .map(|row| Bit4Factors::from_persisted(1.0, (rows - row) as f32, 0.0))
3286 .collect::<Vec<_>>();
3287 let neighbors = (0..rows)
3288 .map(|row| {
3289 (row + 1 < rows)
3290 .then_some((row + 1) as u32)
3291 .into_iter()
3292 .collect::<Vec<_>>()
3293 })
3294 .collect::<Vec<_>>();
3295 let nodes = (0..rows)
3296 .map(|row| GraphNodeBlockInput {
3297 codes: &codes[row * DIMS.div_ceil(2)..(row + 1) * DIMS.div_ceil(2)],
3298 factors: factors[row],
3299 flags: u8::from(row < 4),
3300 neighbors: &neighbors[row],
3301 })
3302 .collect::<Vec<_>>();
3303 let encoded = encode_node_blocks(GraphNodeBlockBuild {
3304 layout,
3305 nodes: &nodes,
3306 })
3307 .expect("fixture graph encodes");
3308 (encoded, vec![0.0_f32; rows * DIMS])
3309 }
3310
3311 fn vector(value: f32) -> Vec<f32> {
3312 let mut vector = vec![0.0_f32; DIMS];
3313 vector[0] = value;
3314 vector[1] = value * value * 0.1;
3315 vector[17] = value.sin();
3316 vector
3317 }
3318
3319 fn brute_force_topk(base: &[f32], query: &[f32], k: usize) -> Vec<u32> {
3320 let mut scored = base
3321 .chunks_exact(DIMS)
3322 .enumerate()
3323 .map(|(row_id, row)| {
3324 let distance = row
3325 .iter()
3326 .zip(query)
3327 .map(|(left, right)| {
3328 let delta = f64::from(*left) - f64::from(*right);
3329 delta * delta
3330 })
3331 .sum::<f64>();
3332 (distance, row_id as u32)
3333 })
3334 .collect::<Vec<_>>();
3335 scored.sort_unstable_by(|left, right| {
3336 left.0
3337 .total_cmp(&right.0)
3338 .then_with(|| left.1.cmp(&right.1))
3339 });
3340 scored
3341 .into_iter()
3342 .take(k)
3343 .map(|(_, row_id)| row_id)
3344 .collect()
3345 }
3346}