| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| use crate::diskann::DiskAnnRawVectorEncoding; |
| use crate::diskann_io::{ |
| decode_adjacency_list, CacheLockMetrics, OffsetLru, SectionRange, SharedWindowCacheLookup, |
| DISKANN_PAGE_SIZE, |
| }; |
| use crate::distance::{ |
| fvec_distance, fvec_l2sqr, pq_distance_four_codes, pq_distance_from_table, preprocess_vectors, |
| MetricType, |
| }; |
| use crate::index_io_util::decode_roaring_filter; |
| use crate::io::{ReadRequest, SeekRead}; |
| use crate::read_options::ReadPlan; |
| use crate::sparse_table::{estimated_memory_bytes as sparse_table_memory_bytes, SparseTable}; |
| use half::prelude::{HalfBitsSliceExt, HalfFloatSliceExt}; |
| use rayon::prelude::*; |
| use roaring::{RoaringBitmap, RoaringTreemap}; |
| use std::borrow::Cow; |
| use std::cmp::{Ordering, Reverse}; |
| use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet}; |
| use std::io; |
| use std::sync::Arc; |
| |
| const QUERY_WINDOW_BUFFER_LIMIT_BYTES: usize = 8 * 1024 * 1024; |
| const QUERY_ADJACENCY_WINDOW_LIMIT_BYTES: usize = 8 * 1024 * 1024; |
| const BATCH_WINDOW_BUFFER_LIMIT_BYTES: usize = 64 * 1024 * 1024; |
| const FILTERED_BATCH_RERANK_MAX_BYTES: usize = 64 * 1024 * 1024; |
| const FILTERED_BATCH_RERANK_MAX_RANGES: usize = 1024; |
| const BATCH_QUERY_CHUNK_SIZE: usize = 1024; |
| const FILTERED_PQ_MAX_QUERY_TILE_SIZE: usize = 4; |
| const FILTERED_PQ_TILE_TABLE_LIMIT_BYTES: usize = 2 * 1024 * 1024; |
| const FILTERED_SINGLE_PQ_NODE_CHUNK_SIZE: usize = 1024; |
| const PARALLEL_EXACT_RERANK_MIN_COMPONENTS: usize = 16 * 1024; |
| const PARALLEL_SESSION_MAX_QUERIES_PER_WORKER: usize = 4; |
| const SPARSE_VISITED_MIN_MEMORY_SAVINGS: usize = 2; |
| |
| #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] |
| pub struct DiskAnnSearchStats { |
| pub query_count: usize, |
| pub query_chunks: usize, |
| pub max_queries_per_chunk: usize, |
| pub filtered_exhaustive_queries: usize, |
| pub filtered_graph_queries: usize, |
| pub filtered_graph_fallbacks: usize, |
| pub pq_distance_evaluations: usize, |
| pub pq_code_loads: usize, |
| pub adjacency_cache_hits: usize, |
| pub adjacency_cache_misses: usize, |
| pub adjacency_cache_waits: usize, |
| pub adjacency_cache_evictions: usize, |
| pub adjacency_cache_lock_acquisitions: usize, |
| pub adjacency_cache_lock_wait_nanos: u64, |
| pub query_adjacency_cache_peak_bytes: usize, |
| pub query_adjacency_cache_evictions: usize, |
| pub rerank_candidate_references: usize, |
| pub rerank_unique_windows: usize, |
| pub rerank_chunks: usize, |
| pub raw_vector_cache_hits: usize, |
| pub raw_vector_cache_misses: usize, |
| pub raw_vector_cache_evictions: usize, |
| pub parallel_exact_rerank_chunks: usize, |
| pub parallel_exact_rerank_references: usize, |
| pub parallel_session_queries: usize, |
| } |
| |
| impl DiskAnnSearchStats { |
| fn record_adjacency_cache_lock(&mut self, metrics: CacheLockMetrics) { |
| self.adjacency_cache_lock_acquisitions = self |
| .adjacency_cache_lock_acquisitions |
| .saturating_add(metrics.acquisitions); |
| self.adjacency_cache_lock_wait_nanos = self |
| .adjacency_cache_lock_wait_nanos |
| .saturating_add(metrics.wait_nanos); |
| } |
| |
| fn merge_candidate_generation(&mut self, worker: Self) { |
| self.filtered_exhaustive_queries = self |
| .filtered_exhaustive_queries |
| .saturating_add(worker.filtered_exhaustive_queries); |
| self.filtered_graph_queries = self |
| .filtered_graph_queries |
| .saturating_add(worker.filtered_graph_queries); |
| self.filtered_graph_fallbacks = self |
| .filtered_graph_fallbacks |
| .saturating_add(worker.filtered_graph_fallbacks); |
| self.pq_distance_evaluations = self |
| .pq_distance_evaluations |
| .saturating_add(worker.pq_distance_evaluations); |
| self.pq_code_loads = self.pq_code_loads.saturating_add(worker.pq_code_loads); |
| self.adjacency_cache_hits = self |
| .adjacency_cache_hits |
| .saturating_add(worker.adjacency_cache_hits); |
| self.adjacency_cache_misses = self |
| .adjacency_cache_misses |
| .saturating_add(worker.adjacency_cache_misses); |
| self.adjacency_cache_waits = self |
| .adjacency_cache_waits |
| .saturating_add(worker.adjacency_cache_waits); |
| self.adjacency_cache_evictions = self |
| .adjacency_cache_evictions |
| .saturating_add(worker.adjacency_cache_evictions); |
| self.adjacency_cache_lock_acquisitions = self |
| .adjacency_cache_lock_acquisitions |
| .saturating_add(worker.adjacency_cache_lock_acquisitions); |
| self.adjacency_cache_lock_wait_nanos = self |
| .adjacency_cache_lock_wait_nanos |
| .saturating_add(worker.adjacency_cache_lock_wait_nanos); |
| self.query_adjacency_cache_peak_bytes = self |
| .query_adjacency_cache_peak_bytes |
| .max(worker.query_adjacency_cache_peak_bytes); |
| self.query_adjacency_cache_evictions = self |
| .query_adjacency_cache_evictions |
| .saturating_add(worker.query_adjacency_cache_evictions); |
| } |
| |
| fn merge_complete_query(&mut self, worker: Self) { |
| let worker_query_count = worker.query_count; |
| self.merge_candidate_generation(worker); |
| self.rerank_candidate_references = self |
| .rerank_candidate_references |
| .saturating_add(worker.rerank_candidate_references); |
| self.rerank_unique_windows = self |
| .rerank_unique_windows |
| .saturating_add(worker.rerank_unique_windows); |
| self.rerank_chunks = self.rerank_chunks.saturating_add(worker.rerank_chunks); |
| self.raw_vector_cache_hits = self |
| .raw_vector_cache_hits |
| .saturating_add(worker.raw_vector_cache_hits); |
| self.raw_vector_cache_misses = self |
| .raw_vector_cache_misses |
| .saturating_add(worker.raw_vector_cache_misses); |
| self.raw_vector_cache_evictions = self |
| .raw_vector_cache_evictions |
| .saturating_add(worker.raw_vector_cache_evictions); |
| self.parallel_exact_rerank_chunks = self |
| .parallel_exact_rerank_chunks |
| .saturating_add(worker.parallel_exact_rerank_chunks); |
| self.parallel_exact_rerank_references = self |
| .parallel_exact_rerank_references |
| .saturating_add(worker.parallel_exact_rerank_references); |
| self.parallel_session_queries = self |
| .parallel_session_queries |
| .saturating_add(worker_query_count.max(1)); |
| } |
| } |
| |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| pub(crate) struct ReadWindow { |
| pub offset: u64, |
| pub length: usize, |
| } |
| |
| impl ReadWindow { |
| pub const fn new(offset: u64, length: usize) -> Self { |
| Self { offset, length } |
| } |
| } |
| |
| pub(crate) struct ReadWindowPlanner { |
| plan: ReadPlan, |
| section: SectionRange, |
| } |
| |
| impl ReadWindowPlanner { |
| pub const fn new(plan: ReadPlan, section: SectionRange) -> Self { |
| Self { plan, section } |
| } |
| |
| #[cfg(test)] |
| pub const fn beam_width(&self) -> usize { |
| self.plan.graph_beam_width |
| } |
| |
| pub fn plan_logical_pages( |
| &self, |
| logical_pages: impl IntoIterator<Item = usize>, |
| ) -> Vec<ReadWindow> { |
| let mut windows = BTreeMap::new(); |
| for logical_page in logical_pages { |
| if let Some(window) = self.window_for_logical_page(logical_page) { |
| windows.insert(window.offset, window.length); |
| } |
| } |
| windows |
| .into_iter() |
| .map(|(offset, length)| ReadWindow::new(offset, length)) |
| .collect() |
| } |
| |
| fn window_for_logical_page(&self, logical_page: usize) -> Option<ReadWindow> { |
| let window_size = self.plan.window_bytes as u64; |
| let relative_page = (logical_page as u64).checked_mul(DISKANN_PAGE_SIZE as u64)?; |
| if relative_page >= self.section.length { |
| return None; |
| } |
| let relative_window = relative_page / window_size * window_size; |
| let length = window_size.min(self.section.length - relative_window) as usize; |
| Some(ReadWindow::new( |
| self.section.offset + relative_window, |
| length, |
| )) |
| } |
| } |
| |
| pub(crate) struct VectorWindowPlanner { |
| section: SectionRange, |
| record_size: usize, |
| records_per_window: usize, |
| } |
| |
| impl VectorWindowPlanner { |
| fn new(plan: ReadPlan, section: SectionRange, record_size: usize) -> io::Result<Self> { |
| if record_size == 0 { |
| return Err(invalid_data( |
| "DiskANN raw-vector record size must be greater than zero", |
| )); |
| } |
| Ok(Self { |
| section, |
| record_size, |
| records_per_window: (plan.window_bytes / record_size).max(1), |
| }) |
| } |
| |
| fn window_for_node(&self, node: usize) -> Option<ReadWindow> { |
| let record_offset = node.checked_mul(self.record_size)?; |
| if u64::try_from(record_offset).ok()? >= self.section.length { |
| return None; |
| } |
| let first_node = node / self.records_per_window * self.records_per_window; |
| let relative_offset = first_node.checked_mul(self.record_size)?; |
| let maximum_length = self.records_per_window.checked_mul(self.record_size)?; |
| let remaining = usize::try_from(self.section.length - relative_offset as u64).ok()?; |
| Some(ReadWindow::new( |
| self.section.offset.checked_add(relative_offset as u64)?, |
| maximum_length.min(remaining), |
| )) |
| } |
| |
| fn plan_nodes(&self, nodes: impl IntoIterator<Item = usize>) -> Vec<ReadWindow> { |
| let mut windows = BTreeMap::new(); |
| for node in nodes { |
| if let Some(window) = self.window_for_node(node) { |
| windows.insert(window.offset, window.length); |
| } |
| } |
| windows |
| .into_iter() |
| .map(|(offset, length)| ReadWindow::new(offset, length)) |
| .collect() |
| } |
| |
| fn record<'a>( |
| &self, |
| window: ReadWindow, |
| payload: &'a [u8], |
| node: usize, |
| ) -> io::Result<&'a [u8]> { |
| let absolute_offset = self |
| .section |
| .offset |
| .checked_add( |
| u64::try_from( |
| node.checked_mul(self.record_size) |
| .ok_or_else(|| invalid_data("DiskANN raw-vector offset overflows"))?, |
| ) |
| .map_err(|_| invalid_data("DiskANN raw-vector offset exceeds u64"))?, |
| ) |
| .ok_or_else(|| invalid_data("DiskANN raw-vector offset overflows"))?; |
| let relative_offset = absolute_offset |
| .checked_sub(window.offset) |
| .and_then(|offset| usize::try_from(offset).ok()) |
| .ok_or_else(|| invalid_data("DiskANN raw-vector window starts after its record"))?; |
| let record_end = relative_offset |
| .checked_add(self.record_size) |
| .ok_or_else(|| invalid_data("DiskANN raw-vector record range overflows"))?; |
| payload |
| .get(relative_offset..record_end) |
| .ok_or_else(|| invalid_data("DiskANN raw-vector record is truncated")) |
| } |
| } |
| |
| #[derive(Debug, Clone, Copy)] |
| struct SearchCandidate { |
| node: usize, |
| distance: f32, |
| } |
| |
| #[derive(Debug, Clone, Copy)] |
| struct ExactSearchResult { |
| row_id: i64, |
| distance: f32, |
| } |
| |
| #[derive(Clone, Copy)] |
| struct ExactRerankReference<'a> { |
| query_index: usize, |
| row_id: i64, |
| record: &'a [u8], |
| } |
| |
| enum WindowPayload { |
| Owned(Vec<u8>), |
| Shared(Arc<Vec<u8>>), |
| } |
| |
| impl WindowPayload { |
| fn as_slice(&self) -> &[u8] { |
| match self { |
| Self::Owned(payload) => payload, |
| Self::Shared(payload) => payload, |
| } |
| } |
| |
| fn capacity(&self) -> usize { |
| match self { |
| Self::Owned(payload) => payload.capacity(), |
| Self::Shared(payload) => payload.capacity(), |
| } |
| } |
| } |
| |
| impl From<Vec<u8>> for WindowPayload { |
| fn from(payload: Vec<u8>) -> Self { |
| Self::Owned(payload) |
| } |
| } |
| |
| #[derive(Default)] |
| struct AdjacencyWindowCache { |
| entries: HashMap<u64, WindowPayload>, |
| recency: OffsetLru, |
| retained_capacity: usize, |
| } |
| |
| impl AdjacencyWindowCache { |
| fn contains_key(&self, offset: &u64) -> bool { |
| self.entries.contains_key(offset) |
| } |
| |
| fn get(&self, offset: &u64) -> Option<&WindowPayload> { |
| self.entries.get(offset) |
| } |
| |
| fn insert(&mut self, offset: u64, payload: WindowPayload) { |
| let payload_capacity = payload.capacity(); |
| if let Some(previous) = self.entries.insert(offset, payload) { |
| self.retained_capacity = self.retained_capacity.saturating_sub(previous.capacity()); |
| self.recency.remove(offset); |
| } |
| self.retained_capacity = self.retained_capacity.saturating_add(payload_capacity); |
| self.recency.touch(offset); |
| } |
| |
| fn touch_windows(&mut self, windows: &[ReadWindow]) { |
| for window in windows { |
| if self.entries.contains_key(&window.offset) { |
| self.recency.touch(window.offset); |
| } |
| } |
| } |
| |
| fn trim(&mut self, window_buffers: &mut WindowBufferPool, capacity_limit: usize) -> usize { |
| let mut evictions = 0usize; |
| while self.retained_capacity > capacity_limit { |
| let Some(offset) = self.recency.pop_oldest() else { |
| break; |
| }; |
| if let Some(payload) = self.entries.remove(&offset) { |
| self.retained_capacity = self.retained_capacity.saturating_sub(payload.capacity()); |
| if let WindowPayload::Owned(payload) = payload { |
| window_buffers.recycle(payload); |
| } |
| evictions = evictions.saturating_add(1); |
| } |
| } |
| evictions |
| } |
| |
| fn recycle(&mut self, window_buffers: &mut WindowBufferPool) { |
| for (_, payload) in self.entries.drain() { |
| if let WindowPayload::Owned(payload) = payload { |
| window_buffers.recycle(payload); |
| } |
| } |
| self.recency.clear(); |
| self.retained_capacity = 0; |
| } |
| |
| #[cfg(test)] |
| fn is_empty(&self) -> bool { |
| self.entries.is_empty() |
| } |
| |
| fn retained_capacity(&self) -> usize { |
| self.retained_capacity |
| } |
| |
| #[cfg(test)] |
| fn reserve(&mut self, additional: usize) { |
| self.entries.reserve(additional); |
| } |
| |
| #[cfg(test)] |
| fn capacity(&self) -> usize { |
| self.entries.capacity() |
| } |
| } |
| |
| fn prepare_adjacency_window_cache( |
| required_windows: &[ReadWindow], |
| incoming_bytes: usize, |
| cache: &mut AdjacencyWindowCache, |
| window_buffers: &mut WindowBufferPool, |
| ) -> usize { |
| // A window that was available when the read plan was assembled may still be |
| // needed by this round even when its individual page does not need loading. |
| // Mark all round inputs as most-recent before making room for missing |
| // windows, otherwise trimming for a different page can evict one that |
| // decode is about to consume. |
| cache.touch_windows(required_windows); |
| cache.trim( |
| window_buffers, |
| QUERY_ADJACENCY_WINDOW_LIMIT_BYTES.saturating_sub(incoming_bytes), |
| ) |
| } |
| |
| fn share_window_payload(payload: Vec<u8>) -> Arc<Vec<u8>> { |
| Arc::new(payload) |
| } |
| |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| enum FilteredCandidateStrategy { |
| Exhaustive { |
| target_candidates: usize, |
| }, |
| Graph { |
| target_candidates: usize, |
| search_list_size: usize, |
| }, |
| } |
| |
| #[derive(Default)] |
| pub(crate) struct DiskAnnQueryScratch { |
| visited: Vec<bool>, |
| sparse_visited: SparseTable<()>, |
| uses_sparse_visited: bool, |
| touched_nodes: Vec<usize>, |
| distance_table: Vec<f32>, |
| candidates: Vec<SearchCandidate>, |
| rerank_candidates: Vec<SearchCandidate>, |
| rerank_windows: HashSet<u64>, |
| retained_candidates: BinaryHeap<SearchCandidate>, |
| frontier: BinaryHeap<Reverse<SearchCandidate>>, |
| selected_nodes: Vec<usize>, |
| loaded_adjacency_pages: HashSet<usize>, |
| adjacency_windows: AdjacencyWindowCache, |
| vector_windows: VectorWindowCache, |
| window_buffers: WindowBufferPool, |
| neighbor_buffer: Vec<u32>, |
| scored_neighbors: Vec<SearchCandidate>, |
| } |
| |
| #[derive(Default)] |
| struct VectorWindowCache { |
| entries: HashMap<u64, WindowPayload>, |
| recency: OffsetLru, |
| retained_capacity: usize, |
| } |
| |
| #[derive(Debug, Default)] |
| struct VectorWindowLoadStats { |
| hits: usize, |
| misses: usize, |
| evictions: usize, |
| } |
| |
| impl VectorWindowCache { |
| fn contains_key(&self, offset: &u64) -> bool { |
| self.entries.contains_key(offset) |
| } |
| |
| fn get(&self, offset: &u64) -> Option<&[u8]> { |
| self.entries.get(offset).map(WindowPayload::as_slice) |
| } |
| |
| fn insert(&mut self, offset: u64, payload: impl Into<WindowPayload>) { |
| let payload = payload.into(); |
| let payload_capacity = payload.capacity(); |
| if let Some(previous) = self.entries.insert(offset, payload) { |
| self.retained_capacity = self.retained_capacity.saturating_sub(previous.capacity()); |
| self.recency.remove(offset); |
| } |
| self.retained_capacity = self.retained_capacity.saturating_add(payload_capacity); |
| self.recency.touch(offset); |
| } |
| |
| #[cfg(test)] |
| fn remove(&mut self, offset: u64) -> Option<WindowPayload> { |
| let payload = self.entries.remove(&offset)?; |
| self.retained_capacity = self.retained_capacity.saturating_sub(payload.capacity()); |
| self.recency.remove(offset); |
| Some(payload) |
| } |
| |
| #[cfg(test)] |
| fn touch(&mut self, offset: u64) { |
| if self.entries.contains_key(&offset) { |
| self.recency.touch(offset); |
| } |
| } |
| |
| fn touch_windows(&mut self, windows: &[ReadWindow]) { |
| for window in windows { |
| debug_assert!(self.entries.contains_key(&window.offset)); |
| self.recency.touch(window.offset); |
| } |
| } |
| |
| fn trim(&mut self, window_buffers: &mut WindowBufferPool, capacity_limit: usize) -> usize { |
| let mut evictions = 0usize; |
| while self.retained_capacity > capacity_limit { |
| let Some(offset) = self.recency.pop_oldest() else { |
| break; |
| }; |
| if let Some(payload) = self.entries.remove(&offset) { |
| self.retained_capacity = self.retained_capacity.saturating_sub(payload.capacity()); |
| if let WindowPayload::Owned(payload) = payload { |
| window_buffers.recycle(payload); |
| } |
| evictions = evictions.saturating_add(1); |
| } |
| } |
| evictions |
| } |
| |
| fn recycle(&mut self, window_buffers: &mut WindowBufferPool) { |
| for (_, payload) in self.entries.drain() { |
| if let WindowPayload::Owned(payload) = payload { |
| window_buffers.recycle(payload); |
| } |
| } |
| self.recency.clear(); |
| self.retained_capacity = 0; |
| } |
| |
| fn len(&self) -> usize { |
| self.entries.len() |
| } |
| |
| #[cfg(test)] |
| fn is_empty(&self) -> bool { |
| self.entries.is_empty() |
| } |
| |
| #[cfg(test)] |
| fn retained_capacity(&self) -> usize { |
| self.retained_capacity |
| } |
| } |
| |
| struct WindowBufferPool { |
| buffers: Vec<Vec<u8>>, |
| retained_capacity: usize, |
| retained_capacity_limit: usize, |
| } |
| |
| impl Default for WindowBufferPool { |
| fn default() -> Self { |
| Self { |
| buffers: Vec::new(), |
| retained_capacity: 0, |
| retained_capacity_limit: QUERY_WINDOW_BUFFER_LIMIT_BYTES, |
| } |
| } |
| } |
| |
| impl WindowBufferPool { |
| #[cfg(test)] |
| fn with_retained_capacity_limit(retained_capacity_limit: usize) -> Self { |
| Self { |
| retained_capacity_limit, |
| ..Self::default() |
| } |
| } |
| |
| fn recycle(&mut self, mut buffer: Vec<u8>) { |
| buffer.clear(); |
| let capacity = buffer.capacity(); |
| let Some(retained_capacity) = self.retained_capacity.checked_add(capacity) else { |
| return; |
| }; |
| if retained_capacity > self.retained_capacity_limit { |
| return; |
| } |
| self.retained_capacity = retained_capacity; |
| self.buffers.push(buffer); |
| } |
| |
| fn take(&mut self, len: usize) -> io::Result<Vec<u8>> { |
| let best_fit = self |
| .buffers |
| .last() |
| .is_some_and(|buffer| buffer.capacity() == len) |
| .then(|| self.buffers.len() - 1) |
| .or_else(|| { |
| self.buffers |
| .iter() |
| .enumerate() |
| .filter(|(_, buffer)| buffer.capacity() >= len) |
| .min_by_key(|(_, buffer)| buffer.capacity()) |
| .map(|(index, _)| index) |
| }) |
| .or_else(|| { |
| self.buffers |
| .iter() |
| .enumerate() |
| .max_by_key(|(_, buffer)| buffer.capacity()) |
| .map(|(index, _)| index) |
| }); |
| let mut buffer = if let Some(index) = best_fit { |
| let buffer = self.buffers.swap_remove(index); |
| self.retained_capacity -= buffer.capacity(); |
| buffer |
| } else { |
| Vec::new() |
| }; |
| let additional_capacity = len.saturating_sub(buffer.capacity()); |
| if additional_capacity != 0 && buffer.try_reserve_exact(additional_capacity).is_err() { |
| self.recycle(buffer); |
| return Err(invalid_input( |
| "DiskANN query window buffer allocation failed", |
| )); |
| } |
| buffer.resize(len, 0); |
| Ok(buffer) |
| } |
| |
| fn set_retained_capacity_limit(&mut self, retained_capacity_limit: usize) { |
| self.retained_capacity_limit = retained_capacity_limit; |
| while self.retained_capacity > self.retained_capacity_limit { |
| let Some(buffer) = self.buffers.pop() else { |
| self.retained_capacity = 0; |
| break; |
| }; |
| self.retained_capacity -= buffer.capacity(); |
| } |
| } |
| } |
| |
| impl DiskAnnQueryScratch { |
| #[cfg(test)] |
| fn with_window_buffer_limit(retained_capacity_limit: usize) -> Self { |
| Self { |
| window_buffers: WindowBufferPool::with_retained_capacity_limit(retained_capacity_limit), |
| ..Self::default() |
| } |
| } |
| |
| fn set_window_buffer_limit(&mut self, retained_capacity_limit: usize) { |
| self.window_buffers |
| .set_retained_capacity_limit(retained_capacity_limit); |
| } |
| |
| #[cfg(test)] |
| fn begin_search(&mut self, vector_count: usize) { |
| self.begin_graph_search(vector_count, vector_count, 1) |
| .expect("test-sized DiskANN visited allocation"); |
| } |
| |
| fn begin_graph_search( |
| &mut self, |
| vector_count: usize, |
| search_list_size: usize, |
| max_degree: usize, |
| ) -> io::Result<()> { |
| self.begin_rerank(); |
| let expected_visited = search_list_size |
| .saturating_mul(max_degree) |
| .saturating_add(1) |
| .min(vector_count); |
| let dense_bytes = vector_count.div_ceil(8); |
| let sparse_bytes = |
| sparse_table_memory_bytes(expected_visited, size_of::<()>()).unwrap_or(usize::MAX); |
| // Dense bitmap probes are substantially cheaper than open-addressed |
| // hashing. Prefer them unless sparse storage saves at least 2x memory. |
| self.uses_sparse_visited = sparse_bytes |
| .checked_mul(SPARSE_VISITED_MIN_MEMORY_SAVINGS) |
| .is_some_and(|threshold| threshold < dense_bytes); |
| if self.uses_sparse_visited { |
| if expected_visited > self.sparse_visited.entry_capacity() { |
| self.sparse_visited = SparseTable::try_with_capacity(expected_visited) |
| .map_err(|_| invalid_input("DiskANN sparse visited allocation failed"))?; |
| } |
| } else { |
| self.visited.resize(vector_count, false); |
| } |
| Ok(()) |
| } |
| |
| fn begin_rerank(&mut self) { |
| if self.uses_sparse_visited { |
| self.sparse_visited.clear(); |
| self.touched_nodes.clear(); |
| } else { |
| for node in self.touched_nodes.drain(..) { |
| self.visited[node] = false; |
| } |
| } |
| self.candidates.clear(); |
| self.rerank_candidates.clear(); |
| self.rerank_windows.clear(); |
| self.retained_candidates.clear(); |
| self.frontier.clear(); |
| self.selected_nodes.clear(); |
| self.loaded_adjacency_pages.clear(); |
| self.recycle_adjacency_windows(); |
| self.neighbor_buffer.clear(); |
| self.scored_neighbors.clear(); |
| } |
| |
| fn recycle_adjacency_windows(&mut self) { |
| self.adjacency_windows.recycle(&mut self.window_buffers); |
| } |
| |
| fn recycle_vector_windows(&mut self) { |
| self.vector_windows.recycle(&mut self.window_buffers); |
| } |
| |
| fn recycle_window_caches(&mut self) { |
| self.recycle_adjacency_windows(); |
| self.recycle_vector_windows(); |
| } |
| |
| fn prepare_distance_table(&mut self, len: usize) -> &mut [f32] { |
| self.distance_table.resize(len, 0.0); |
| &mut self.distance_table |
| } |
| |
| fn select_round(&mut self, limit: usize) { |
| self.selected_nodes.clear(); |
| while self.selected_nodes.len() < limit { |
| let Some(Reverse(candidate)) = self.frontier.pop() else { |
| break; |
| }; |
| if self |
| .retained_candidates |
| .peek() |
| .is_some_and(|worst| candidate > *worst) |
| { |
| self.frontier.clear(); |
| break; |
| } |
| self.selected_nodes.push(candidate.node); |
| } |
| } |
| |
| fn insert_graph_candidate( |
| &mut self, |
| candidate: SearchCandidate, |
| limit: usize, |
| ) -> io::Result<()> { |
| if limit == 0 { |
| return Ok(()); |
| } |
| let replacing_worst = self.retained_candidates.len() == limit; |
| if replacing_worst { |
| let Some(worst) = self.retained_candidates.peek().copied() else { |
| return Ok(()); |
| }; |
| if candidate >= worst { |
| return Ok(()); |
| } |
| } else { |
| self.retained_candidates |
| .try_reserve(1) |
| .map_err(|_| invalid_input("DiskANN graph candidate allocation failed"))?; |
| } |
| self.frontier |
| .try_reserve(1) |
| .map_err(|_| invalid_input("DiskANN graph frontier allocation failed"))?; |
| if replacing_worst { |
| self.retained_candidates.pop(); |
| } |
| self.retained_candidates.push(candidate); |
| self.frontier.push(Reverse(candidate)); |
| if self.frontier.len() > limit.saturating_mul(2) { |
| let worst = *self |
| .retained_candidates |
| .peek() |
| .expect("non-empty retained DiskANN candidates"); |
| self.frontier |
| .retain(|Reverse(candidate)| *candidate <= worst); |
| } |
| Ok(()) |
| } |
| |
| fn finish_graph_candidates(&mut self) { |
| self.candidates.extend(self.retained_candidates.drain()); |
| sort_candidates(&mut self.candidates); |
| } |
| |
| #[cfg(test)] |
| fn is_visited(&self, node: usize) -> bool { |
| if self.uses_sparse_visited { |
| self.sparse_visited.get(node as u32).is_some() |
| } else { |
| self.visited[node] |
| } |
| } |
| |
| fn mark_visited(&mut self, node: usize) -> bool { |
| if self.uses_sparse_visited { |
| return self.sparse_visited.insert(node as u32, ()).is_none(); |
| } |
| if self.visited[node] { |
| return false; |
| } |
| self.visited[node] = true; |
| self.touched_nodes.push(node); |
| true |
| } |
| |
| #[cfg(test)] |
| fn visited_capacity(&self) -> usize { |
| self.visited.capacity() |
| } |
| |
| #[cfg(test)] |
| fn uses_sparse_visited(&self) -> bool { |
| self.uses_sparse_visited |
| } |
| |
| #[cfg(test)] |
| fn retained_window_capacity(&self) -> usize { |
| self.window_buffers.retained_capacity |
| } |
| } |
| |
| fn window_buffer_limit_per_worker(worker_count: usize) -> usize { |
| QUERY_WINDOW_BUFFER_LIMIT_BYTES.min(BATCH_WINDOW_BUFFER_LIMIT_BYTES / worker_count.max(1)) |
| } |
| |
| fn prepare_vector_window_cache( |
| windows: &[ReadWindow], |
| cache: &mut VectorWindowCache, |
| window_buffers: &mut WindowBufferPool, |
| capacity_limit: usize, |
| ) -> (bool, usize) { |
| let retain = windows |
| .iter() |
| .try_fold(0usize, |total, window| total.checked_add(window.length)) |
| .is_some_and(|total| total <= capacity_limit); |
| if !retain { |
| let evictions = cache.len(); |
| cache.recycle(window_buffers); |
| return (false, evictions); |
| } |
| let evictions = cache.trim(window_buffers, capacity_limit); |
| (true, evictions) |
| } |
| |
| type CandidatePartition = Vec<(usize, Vec<usize>)>; |
| type SessionQueryOutput = (usize, Vec<i64>, Vec<f32>, DiskAnnSearchStats); |
| |
| impl PartialEq for SearchCandidate { |
| fn eq(&self, other: &Self) -> bool { |
| self.node == other.node && self.distance.to_bits() == other.distance.to_bits() |
| } |
| } |
| |
| impl Eq for SearchCandidate {} |
| |
| impl PartialOrd for SearchCandidate { |
| fn partial_cmp(&self, other: &Self) -> Option<Ordering> { |
| Some(self.cmp(other)) |
| } |
| } |
| |
| impl Ord for SearchCandidate { |
| fn cmp(&self, other: &Self) -> Ordering { |
| self.distance |
| .total_cmp(&other.distance) |
| .then_with(|| self.node.cmp(&other.node)) |
| } |
| } |
| |
| impl PartialEq for ExactSearchResult { |
| fn eq(&self, other: &Self) -> bool { |
| self.row_id == other.row_id && self.distance.to_bits() == other.distance.to_bits() |
| } |
| } |
| |
| impl Eq for ExactSearchResult {} |
| |
| impl PartialOrd for ExactSearchResult { |
| fn partial_cmp(&self, other: &Self) -> Option<Ordering> { |
| Some(self.cmp(other)) |
| } |
| } |
| |
| impl Ord for ExactSearchResult { |
| fn cmp(&self, other: &Self) -> Ordering { |
| self.distance |
| .total_cmp(&other.distance) |
| .then_with(|| self.row_id.cmp(&other.row_id)) |
| } |
| } |
| |
| impl<R: SeekRead> crate::diskann_io::DiskAnnIndexReader<R> { |
| fn preprocess_queries<'a>(&self, queries: &'a [f32], query_count: usize) -> Cow<'a, [f32]> { |
| if self.header.metric_type() == MetricType::Cosine { |
| Cow::Owned(preprocess_vectors( |
| queries, |
| query_count, |
| self.header.dimension as usize, |
| MetricType::Cosine, |
| )) |
| } else { |
| Cow::Borrowed(queries) |
| } |
| } |
| |
| fn take_batch_workers( |
| &mut self, |
| worker_count: usize, |
| filtered: bool, |
| ) -> io::Result<Option<Vec<Self>>> { |
| if filtered { |
| self.ensure_resident()?; |
| } else { |
| self.optimize_for_search()?; |
| } |
| let mut workers = std::mem::take(&mut self.batch_workers); |
| workers.truncate(worker_count); |
| while workers.len() < worker_count { |
| let worker = if filtered { |
| self.try_clone_for_filtered_search() |
| } else { |
| self.try_clone_for_search() |
| }; |
| match worker { |
| Ok(Some(worker)) => workers.push(worker), |
| Ok(None) => { |
| self.batch_workers = workers; |
| return Ok(None); |
| } |
| Err(error) => { |
| self.batch_workers = workers; |
| return Err(error); |
| } |
| } |
| } |
| let window_buffer_limit = window_buffer_limit_per_worker(worker_count); |
| for worker in &mut workers { |
| worker.refresh_shared_state_from(self); |
| worker.limit_raw_vector_cache_bytes(window_buffer_limit); |
| worker |
| .query_scratch |
| .set_window_buffer_limit(window_buffer_limit); |
| } |
| Ok(Some(workers)) |
| } |
| |
| pub(crate) fn search_batch( |
| &mut self, |
| queries: &[f32], |
| top_k: usize, |
| l_search: usize, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| let dimension = self.header.dimension as usize; |
| let query_count = queries.len() / dimension; |
| self.last_search_stats = DiskAnnSearchStats { |
| query_count, |
| ..DiskAnnSearchStats::default() |
| }; |
| if top_k == 0 { |
| return Ok((Vec::new(), Vec::new())); |
| } |
| let processed_queries = self.preprocess_queries(queries, query_count); |
| let queries = processed_queries.as_ref(); |
| let worker_count = query_count.min(rayon::current_num_threads()); |
| if worker_count <= 1 { |
| self.batch_workers.clear(); |
| if self.header.is_interleaved() { |
| return self.search_batch_direct_serial(queries, top_k, l_search); |
| } |
| return self.search_batch_serial(queries, top_k, l_search); |
| } |
| |
| let Some(mut workers) = self.take_batch_workers(worker_count, false)? else { |
| if self.header.is_interleaved() { |
| return self.search_batch_direct_serial(queries, top_k, l_search); |
| } |
| return self.search_batch_serial(queries, top_k, l_search); |
| }; |
| if self.header.is_interleaved() |
| || query_count <= worker_count.saturating_mul(PARALLEL_SESSION_MAX_QUERIES_PER_WORKER) |
| { |
| let result = |
| self.search_batch_in_parallel_sessions(queries, top_k, l_search, &mut workers); |
| self.batch_workers = workers; |
| return result; |
| } |
| let result = (|| { |
| let mut ids = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| let mut distances = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| for query_chunk in queries.chunks(BATCH_QUERY_CHUNK_SIZE * dimension) { |
| let chunk_query_count = query_chunk.len() / dimension; |
| self.record_query_chunk(chunk_query_count); |
| let worker_outputs = workers |
| .par_iter_mut() |
| .enumerate() |
| .map(|(worker_index, worker)| { |
| worker.last_search_stats = DiskAnnSearchStats::default(); |
| let mut partition = Vec::new(); |
| for query_index in (worker_index..chunk_query_count).step_by(worker_count) { |
| let query = &query_chunk |
| [query_index * dimension..(query_index + 1) * dimension]; |
| let candidates = worker |
| .generate_unfiltered_candidate_nodes(query, top_k, l_search)?; |
| partition.push((query_index, candidates)); |
| } |
| Ok::<_, io::Error>((partition, worker.last_search_stats)) |
| }) |
| .collect::<io::Result<Vec<_>>>()?; |
| let mut partitions = Vec::with_capacity(worker_outputs.len()); |
| for (partition, worker_stats) in worker_outputs { |
| self.last_search_stats |
| .merge_candidate_generation(worker_stats); |
| partitions.push(partition); |
| } |
| let (chunk_ids, chunk_distances) = |
| self.rerank_candidate_batch_streaming(query_chunk, top_k, partitions)?; |
| ids.extend(chunk_ids); |
| distances.extend(chunk_distances); |
| } |
| Ok((ids, distances)) |
| })(); |
| self.batch_workers = workers; |
| result |
| } |
| |
| /// Warms resident metadata and the query-dependent adjacency/raw-vector |
| /// caches with representative queries without changing reported search |
| /// statistics. |
| pub fn warmup_queries(&mut self, queries: &[f32], l_search: usize) -> io::Result<()> { |
| let dimension = self.header.dimension as usize; |
| if !queries.len().is_multiple_of(dimension) { |
| return Err(invalid_input(format!( |
| "warmup query length {} is not divisible by dimension {}", |
| queries.len(), |
| dimension |
| ))); |
| } |
| if queries.iter().any(|value| !value.is_finite()) { |
| return Err(invalid_input("warmup query values must be finite")); |
| } |
| self.optimize_for_search()?; |
| if queries.is_empty() { |
| return Ok(()); |
| } |
| let saved_stats = self.last_search_stats; |
| // Replay on the parent Reader so both adjacency and raw-vector windows |
| // are useful to the subsequent single-query path. A batch warm-up may |
| // otherwise populate only retained worker-local raw-vector caches. |
| let result = queries |
| .chunks_exact(dimension) |
| .try_for_each(|query| self.search(query, 1, l_search).map(|_| ())); |
| self.last_search_stats = saved_stats; |
| result |
| } |
| |
| /// Calibrates the automatic search width from representative queries. |
| /// |
| /// This is a stability proxy, not a ground-truth recall guarantee: it |
| /// chooses the first width whose Top-K overlap with the next wider search |
| /// reaches 98% across the sample. |
| pub fn calibrate_l_search(&mut self, queries: &[f32], top_k: usize) -> io::Result<usize> { |
| let dimension = self.header.dimension as usize; |
| if queries.is_empty() || !queries.len().is_multiple_of(dimension) { |
| return Err(invalid_input( |
| "calibration queries must contain one or more complete vectors", |
| )); |
| } |
| if top_k == 0 { |
| return Err(invalid_input("calibration top_k must be greater than 0")); |
| } |
| if queries.iter().any(|value| !value.is_finite()) { |
| return Err(invalid_input("calibration query values must be finite")); |
| } |
| self.optimize_for_search()?; |
| let widths = [ |
| 100usize.max(top_k), |
| 200usize.max(top_k), |
| 400usize.max(top_k), |
| ]; |
| let mut results = Vec::with_capacity(widths.len()); |
| let saved_stats = self.last_search_stats; |
| for width in widths { |
| results.push(self.search_batch(queries, top_k, width)?); |
| } |
| self.last_search_stats = saved_stats; |
| let chosen = if topk_result_stability( |
| &results[0].0, |
| &results[0].1, |
| &results[1].0, |
| &results[1].1, |
| top_k, |
| ) >= 0.98 |
| { |
| widths[0] |
| } else if topk_result_stability( |
| &results[1].0, |
| &results[1].1, |
| &results[2].0, |
| &results[2].1, |
| top_k, |
| ) >= 0.98 |
| { |
| widths[1] |
| } else { |
| widths[2] |
| }; |
| self.calibrated_l_search = Some(chosen); |
| for worker in &mut self.batch_workers { |
| worker.calibrated_l_search = Some(chosen); |
| } |
| Ok(chosen) |
| } |
| |
| pub(crate) fn search_batch_with_roaring_filter( |
| &mut self, |
| queries: &[f32], |
| top_k: usize, |
| l_search: usize, |
| roaring_filter_bytes: &[u8], |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| let filter = decode_roaring_filter(roaring_filter_bytes)?; |
| let dimension = self.header.dimension as usize; |
| let query_count = queries.len() / dimension; |
| self.last_search_stats = DiskAnnSearchStats { |
| query_count, |
| ..DiskAnnSearchStats::default() |
| }; |
| if top_k == 0 { |
| return Ok((Vec::new(), Vec::new())); |
| } |
| let processed_queries = self.preprocess_queries(queries, query_count); |
| let queries = processed_queries.as_ref(); |
| if filter.is_empty() { |
| return Ok(( |
| vec![-1; query_count * top_k], |
| vec![f32::MAX; query_count * top_k], |
| )); |
| } |
| let matching_nodes = self.matching_nodes_for_filter(&filter)?; |
| if matching_nodes.is_empty() { |
| return Ok(( |
| vec![-1; query_count * top_k], |
| vec![f32::MAX; query_count * top_k], |
| )); |
| } |
| if self.header.is_interleaved() { |
| let worker_count = query_count.min(rayon::current_num_threads()); |
| if worker_count <= 1 { |
| self.batch_workers.clear(); |
| return self.search_batch_with_matching_nodes_direct_serial( |
| queries, |
| top_k, |
| l_search, |
| &matching_nodes, |
| ); |
| } |
| let Some(mut workers) = self.take_batch_workers(worker_count, true)? else { |
| return self.search_batch_with_matching_nodes_direct_serial( |
| queries, |
| top_k, |
| l_search, |
| &matching_nodes, |
| ); |
| }; |
| let result = self.search_batch_with_matching_nodes_in_parallel_sessions( |
| queries, |
| top_k, |
| l_search, |
| &matching_nodes, |
| &mut workers, |
| ); |
| self.batch_workers = workers; |
| return result; |
| } |
| let matching_count = usize::try_from(matching_nodes.len()).unwrap_or(usize::MAX); |
| if let FilteredCandidateStrategy::Exhaustive { target_candidates } = |
| select_filtered_candidate_strategy( |
| self.header.vector_count as usize, |
| matching_count, |
| top_k, |
| l_search, |
| self.header.max_degree as usize, |
| self.read_plan(), |
| self.adjacency_fully_preloaded(), |
| ) |
| { |
| return self.search_batch_filtered_exhaustive( |
| queries, |
| top_k, |
| &matching_nodes, |
| target_candidates, |
| ); |
| } |
| let worker_count = query_count.min(rayon::current_num_threads()); |
| if worker_count <= 1 { |
| self.batch_workers.clear(); |
| return self.search_batch_with_matching_nodes_serial( |
| queries, |
| top_k, |
| l_search, |
| &matching_nodes, |
| ); |
| } |
| |
| let Some(mut workers) = self.take_batch_workers(worker_count, true)? else { |
| return self.search_batch_with_matching_nodes_serial( |
| queries, |
| top_k, |
| l_search, |
| &matching_nodes, |
| ); |
| }; |
| if query_count <= worker_count.saturating_mul(PARALLEL_SESSION_MAX_QUERIES_PER_WORKER) { |
| let result = self.search_batch_with_matching_nodes_in_parallel_sessions( |
| queries, |
| top_k, |
| l_search, |
| &matching_nodes, |
| &mut workers, |
| ); |
| self.batch_workers = workers; |
| return result; |
| } |
| let result = (|| { |
| let mut ids = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| let mut distances = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| for query_chunk in queries.chunks(BATCH_QUERY_CHUNK_SIZE * dimension) { |
| let chunk_query_count = query_chunk.len() / dimension; |
| self.record_query_chunk(chunk_query_count); |
| let worker_outputs = workers |
| .par_iter_mut() |
| .enumerate() |
| .map(|(worker_index, worker)| { |
| worker.last_search_stats = DiskAnnSearchStats::default(); |
| let mut partition = Vec::new(); |
| for query_index in (worker_index..chunk_query_count).step_by(worker_count) { |
| let query = &query_chunk |
| [query_index * dimension..(query_index + 1) * dimension]; |
| let candidates = worker.generate_filtered_candidates( |
| query, |
| top_k, |
| l_search, |
| &matching_nodes, |
| )?; |
| partition.push(( |
| query_index, |
| candidates |
| .into_iter() |
| .map(|candidate| candidate.node) |
| .collect(), |
| )); |
| } |
| Ok::<_, io::Error>((partition, worker.last_search_stats)) |
| }) |
| .collect::<io::Result<Vec<_>>>()?; |
| let mut partitions = Vec::with_capacity(worker_outputs.len()); |
| for (partition, worker_stats) in worker_outputs { |
| self.last_search_stats |
| .merge_candidate_generation(worker_stats); |
| partitions.push(partition); |
| } |
| let (chunk_ids, chunk_distances) = |
| self.rerank_candidate_batch_streaming(query_chunk, top_k, partitions)?; |
| ids.extend(chunk_ids); |
| distances.extend(chunk_distances); |
| } |
| Ok((ids, distances)) |
| })(); |
| self.batch_workers = workers; |
| result |
| } |
| |
| fn search_batch_in_parallel_sessions( |
| &mut self, |
| queries: &[f32], |
| top_k: usize, |
| l_search: usize, |
| workers: &mut [Self], |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| let dimension = self.header.dimension as usize; |
| let query_count = queries.len() / dimension; |
| self.record_query_chunk(query_count); |
| let worker_count = workers.len(); |
| let worker_outputs = workers |
| .par_iter_mut() |
| .enumerate() |
| .map(|(worker_index, worker)| { |
| let mut outputs = Vec::new(); |
| for query_index in (worker_index..query_count).step_by(worker_count) { |
| let query = &queries[query_index * dimension..(query_index + 1) * dimension]; |
| let (ids, distances) = worker.search_preprocessed(query, top_k, l_search)?; |
| outputs.push((query_index, ids, distances, worker.last_search_stats)); |
| } |
| Ok::<_, io::Error>(outputs) |
| }) |
| .collect::<io::Result<Vec<_>>>()?; |
| self.collect_parallel_session_outputs(worker_outputs, query_count, top_k) |
| } |
| |
| fn search_batch_with_matching_nodes_in_parallel_sessions( |
| &mut self, |
| queries: &[f32], |
| top_k: usize, |
| l_search: usize, |
| matching_nodes: &RoaringBitmap, |
| workers: &mut [Self], |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| let dimension = self.header.dimension as usize; |
| let query_count = queries.len() / dimension; |
| self.record_query_chunk(query_count); |
| let worker_count = workers.len(); |
| let worker_outputs = workers |
| .par_iter_mut() |
| .enumerate() |
| .map(|(worker_index, worker)| { |
| let mut outputs = Vec::new(); |
| for query_index in (worker_index..query_count).step_by(worker_count) { |
| let query = &queries[query_index * dimension..(query_index + 1) * dimension]; |
| worker.last_search_stats = DiskAnnSearchStats { |
| query_count: 1, |
| ..DiskAnnSearchStats::default() |
| }; |
| let (ids, distances) = worker.search_with_matching_nodes( |
| query, |
| top_k, |
| l_search, |
| matching_nodes, |
| )?; |
| outputs.push((query_index, ids, distances, worker.last_search_stats)); |
| } |
| Ok::<_, io::Error>(outputs) |
| }) |
| .collect::<io::Result<Vec<_>>>()?; |
| self.collect_parallel_session_outputs(worker_outputs, query_count, top_k) |
| } |
| |
| fn collect_parallel_session_outputs( |
| &mut self, |
| worker_outputs: Vec<Vec<SessionQueryOutput>>, |
| query_count: usize, |
| top_k: usize, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| let mut ordered = (0..query_count) |
| .map(|_| None) |
| .collect::<Vec<Option<(Vec<i64>, Vec<f32>)>>>(); |
| for (query_index, ids, distances, stats) in worker_outputs.into_iter().flatten() { |
| let slot = ordered |
| .get_mut(query_index) |
| .ok_or_else(|| invalid_data("DiskANN parallel session query index is invalid"))?; |
| if slot.replace((ids, distances)).is_some() { |
| return Err(invalid_data( |
| "DiskANN parallel sessions returned a duplicate query", |
| )); |
| } |
| self.last_search_stats.merge_complete_query(stats); |
| } |
| if ordered.iter().any(Option::is_none) { |
| return Err(invalid_data( |
| "DiskANN parallel sessions did not return every query", |
| )); |
| } |
| let mut ids = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| let mut distances = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| for result in ordered { |
| let (query_ids, query_distances) = |
| result.expect("validated DiskANN parallel session output"); |
| ids.extend(query_ids); |
| distances.extend(query_distances); |
| } |
| Ok((ids, distances)) |
| } |
| |
| fn search_batch_filtered_exhaustive( |
| &mut self, |
| queries: &[f32], |
| top_k: usize, |
| matching_nodes: &RoaringBitmap, |
| candidate_limit: usize, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| let dimension = self.header.dimension as usize; |
| let query_count = queries.len() / dimension; |
| let matching_count = usize::try_from(matching_nodes.len()).unwrap_or(usize::MAX); |
| let pq_m = self.header.pq_m as usize; |
| let pq_ksub = 1usize << self.header.pq_bits; |
| let tile_size = filtered_pq_query_tile_size(pq_m, pq_ksub); |
| let mut ids = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| let mut distances = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| for query_chunk in queries.chunks(BATCH_QUERY_CHUNK_SIZE * dimension) { |
| let chunk_query_count = query_chunk.len() / dimension; |
| self.record_query_chunk(chunk_query_count); |
| let candidate_sets = self.exhaustive_filtered_candidate_nodes_batch( |
| query_chunk, |
| matching_nodes, |
| candidate_limit, |
| )?; |
| self.last_search_stats.filtered_exhaustive_queries = self |
| .last_search_stats |
| .filtered_exhaustive_queries |
| .saturating_add(chunk_query_count); |
| self.last_search_stats.pq_distance_evaluations = self |
| .last_search_stats |
| .pq_distance_evaluations |
| .saturating_add(chunk_query_count.saturating_mul(matching_count)); |
| self.last_search_stats.pq_code_loads = |
| self.last_search_stats.pq_code_loads.saturating_add( |
| chunk_query_count |
| .div_ceil(tile_size) |
| .saturating_mul(matching_count), |
| ); |
| let partition = candidate_sets.into_iter().enumerate().collect(); |
| let (chunk_ids, chunk_distances) = |
| self.rerank_candidate_batch_streaming(query_chunk, top_k, vec![partition])?; |
| ids.extend(chunk_ids); |
| distances.extend(chunk_distances); |
| } |
| Ok((ids, distances)) |
| } |
| |
| fn exhaustive_filtered_candidate_nodes_batch( |
| &self, |
| queries: &[f32], |
| matching_nodes: &RoaringBitmap, |
| candidate_limit: usize, |
| ) -> io::Result<Vec<Vec<usize>>> { |
| let dimension = self.header.dimension as usize; |
| let query_count = queries.len() / dimension; |
| let pq_m = self.header.pq_m as usize; |
| let pq = self.pq()?; |
| let pq_ksub = pq.ksub; |
| let pq_code_size = pq.code_size(); |
| let pq_codes = self.pq_codes()?; |
| let metric = self.header.metric_type(); |
| let tile_size = filtered_pq_query_tile_size(pq_m, pq_ksub); |
| let tile_count = query_count.div_ceil(tile_size); |
| let mut tiles = (0..tile_count) |
| .into_par_iter() |
| .map(|tile_index| { |
| let query_start = tile_index * tile_size; |
| let query_end = (query_start + tile_size).min(query_count); |
| let tile_query_count = query_end - query_start; |
| let mut distance_tables = vec![0.0f32; tile_query_count * pq_m * pq_ksub]; |
| for tile_query_index in 0..tile_query_count { |
| let query_index = query_start + tile_query_index; |
| let query = &queries[query_index * dimension..(query_index + 1) * dimension]; |
| let table_start = tile_query_index * pq_m * pq_ksub; |
| pq.compute_distance_table( |
| query, |
| metric, |
| &mut distance_tables[table_start..table_start + pq_m * pq_ksub], |
| ); |
| } |
| let mut heaps = (0..tile_query_count) |
| .map(|_| BinaryHeap::new()) |
| .collect::<Vec<BinaryHeap<SearchCandidate>>>(); |
| for heap in &mut heaps { |
| heap.try_reserve(candidate_limit.min(1024)).map_err(|_| { |
| invalid_input("DiskANN filtered candidate allocation failed") |
| })?; |
| } |
| for node in matching_nodes.iter() { |
| let node = node as usize; |
| let code_start = node |
| .checked_mul(pq_code_size) |
| .ok_or_else(|| invalid_data("DiskANN PQ code offset overflows"))?; |
| let codes = pq_codes |
| .get(code_start..code_start + pq_code_size) |
| .ok_or_else(|| invalid_data("DiskANN PQ codes are truncated"))?; |
| for tile_query_index in 0..tile_query_count { |
| let table_start = tile_query_index * pq_m * pq_ksub; |
| push_bounded_candidate( |
| &mut heaps[tile_query_index], |
| SearchCandidate { |
| node, |
| distance: pq.distance_from_table( |
| &distance_tables[table_start..table_start + pq_m * pq_ksub], |
| codes, |
| ), |
| }, |
| candidate_limit, |
| )?; |
| } |
| } |
| let candidates = heaps |
| .into_iter() |
| .map(|heap| { |
| let mut candidates = heap.into_vec(); |
| sort_candidates(&mut candidates); |
| candidates |
| .into_iter() |
| .map(|candidate| candidate.node) |
| .collect::<Vec<_>>() |
| }) |
| .collect::<Vec<_>>(); |
| Ok::<_, io::Error>((query_start, candidates)) |
| }) |
| .collect::<io::Result<Vec<_>>>()?; |
| tiles.sort_unstable_by_key(|(query_start, _)| *query_start); |
| Ok(tiles |
| .into_iter() |
| .flat_map(|(_, candidates)| candidates) |
| .collect()) |
| } |
| |
| fn search_batch_direct_serial( |
| &mut self, |
| queries: &[f32], |
| top_k: usize, |
| l_search: usize, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| let dimension = self.header.dimension as usize; |
| let query_count = queries.len() / dimension; |
| let mut aggregate = DiskAnnSearchStats { |
| query_count, |
| query_chunks: usize::from(query_count != 0), |
| max_queries_per_chunk: query_count, |
| ..DiskAnnSearchStats::default() |
| }; |
| let mut ids = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| let mut distances = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| for query in queries.chunks_exact(dimension) { |
| let (query_ids, query_distances) = self.search_preprocessed(query, top_k, l_search)?; |
| aggregate.merge_complete_query(self.last_search_stats); |
| ids.extend(query_ids); |
| distances.extend(query_distances); |
| } |
| aggregate.parallel_session_queries = 0; |
| self.last_search_stats = aggregate; |
| Ok((ids, distances)) |
| } |
| |
| fn search_batch_with_matching_nodes_direct_serial( |
| &mut self, |
| queries: &[f32], |
| top_k: usize, |
| l_search: usize, |
| matching_nodes: &RoaringBitmap, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| let dimension = self.header.dimension as usize; |
| let query_count = queries.len() / dimension; |
| let mut aggregate = DiskAnnSearchStats { |
| query_count, |
| query_chunks: usize::from(query_count != 0), |
| max_queries_per_chunk: query_count, |
| ..DiskAnnSearchStats::default() |
| }; |
| let mut ids = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| let mut distances = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| for query in queries.chunks_exact(dimension) { |
| self.last_search_stats = DiskAnnSearchStats { |
| query_count: 1, |
| ..DiskAnnSearchStats::default() |
| }; |
| let (query_ids, query_distances) = |
| self.search_with_matching_nodes(query, top_k, l_search, matching_nodes)?; |
| aggregate.merge_complete_query(self.last_search_stats); |
| ids.extend(query_ids); |
| distances.extend(query_distances); |
| } |
| aggregate.parallel_session_queries = 0; |
| self.last_search_stats = aggregate; |
| Ok((ids, distances)) |
| } |
| |
| fn search_batch_serial( |
| &mut self, |
| queries: &[f32], |
| top_k: usize, |
| l_search: usize, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| let dimension = self.header.dimension as usize; |
| let query_count = queries.len() / dimension; |
| let mut ids = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| let mut distances = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| for query_chunk in queries.chunks(BATCH_QUERY_CHUNK_SIZE * dimension) { |
| let chunk_query_count = query_chunk.len() / dimension; |
| self.record_query_chunk(chunk_query_count); |
| let mut partition = Vec::with_capacity(chunk_query_count); |
| for (query_index, query) in query_chunk.chunks_exact(dimension).enumerate() { |
| let candidates = |
| self.generate_unfiltered_candidate_nodes(query, top_k, l_search)?; |
| partition.push((query_index, candidates)); |
| } |
| let (chunk_ids, chunk_distances) = |
| self.rerank_candidate_batch_streaming(query_chunk, top_k, vec![partition])?; |
| ids.extend(chunk_ids); |
| distances.extend(chunk_distances); |
| } |
| Ok((ids, distances)) |
| } |
| |
| fn search_batch_with_matching_nodes_serial( |
| &mut self, |
| queries: &[f32], |
| top_k: usize, |
| l_search: usize, |
| matching_nodes: &RoaringBitmap, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| let dimension = self.header.dimension as usize; |
| let query_count = queries.len() / dimension; |
| let mut ids = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| let mut distances = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| for query_chunk in queries.chunks(BATCH_QUERY_CHUNK_SIZE * dimension) { |
| let chunk_query_count = query_chunk.len() / dimension; |
| self.record_query_chunk(chunk_query_count); |
| let mut partition = Vec::with_capacity(chunk_query_count); |
| for (query_index, query) in query_chunk.chunks_exact(dimension).enumerate() { |
| let candidates = |
| self.generate_filtered_candidates(query, top_k, l_search, matching_nodes)?; |
| partition.push(( |
| query_index, |
| candidates |
| .into_iter() |
| .map(|candidate| candidate.node) |
| .collect(), |
| )); |
| } |
| let (chunk_ids, chunk_distances) = |
| self.rerank_candidate_batch_streaming(query_chunk, top_k, vec![partition])?; |
| ids.extend(chunk_ids); |
| distances.extend(chunk_distances); |
| } |
| Ok((ids, distances)) |
| } |
| |
| fn record_query_chunk(&mut self, query_count: usize) { |
| self.last_search_stats.query_chunks = self.last_search_stats.query_chunks.saturating_add(1); |
| self.last_search_stats.max_queries_per_chunk = self |
| .last_search_stats |
| .max_queries_per_chunk |
| .max(query_count); |
| } |
| |
| fn generate_unfiltered_candidate_nodes( |
| &mut self, |
| query: &[f32], |
| top_k: usize, |
| l_search: usize, |
| ) -> io::Result<Vec<usize>> { |
| self.ensure_resident()?; |
| let search_list_size = resolve_diskann_l_search(top_k, l_search); |
| let mut scratch = std::mem::take(&mut self.query_scratch); |
| let result = (|| { |
| self.generate_graph_candidates( |
| query, |
| search_list_size, |
| self.read_plan().graph_beam_width, |
| &mut scratch, |
| )?; |
| let rerank_count = search_list_size |
| .min(top_k.saturating_mul(4).max(64)) |
| .min(scratch.candidates.len()); |
| if rerank_count == scratch.candidates.len() { |
| return Ok(scratch |
| .candidates |
| .iter() |
| .map(|candidate| candidate.node) |
| .collect()); |
| } |
| if self.header.is_interleaved() { |
| let planner = |
| ReadWindowPlanner::new(self.read_plan(), self.header.sections.adjacency); |
| expand_rerank_candidates_within_seed_windows( |
| &scratch.candidates, |
| rerank_count, |
| |node| { |
| let page = self.adjacency_locator(node)?.page_index as usize; |
| planner |
| .window_for_logical_page(page) |
| .map(|window| window.offset) |
| .ok_or_else(|| invalid_data("DiskANN adjacency page is out of range")) |
| }, |
| &mut scratch.rerank_windows, |
| &mut scratch.rerank_candidates, |
| )?; |
| return Ok(scratch |
| .rerank_candidates |
| .iter() |
| .map(|candidate| candidate.node) |
| .collect()); |
| } |
| let planner = VectorWindowPlanner::new( |
| self.read_plan(), |
| self.header.sections.vectors, |
| self.header.vector_record_size as usize, |
| )?; |
| expand_rerank_candidates_within_seed_windows( |
| &scratch.candidates, |
| rerank_count, |
| |node| { |
| planner |
| .window_for_node(node) |
| .map(|window| window.offset) |
| .ok_or_else(|| invalid_data("DiskANN raw-vector record is out of range")) |
| }, |
| &mut scratch.rerank_windows, |
| &mut scratch.rerank_candidates, |
| )?; |
| Ok(scratch |
| .rerank_candidates |
| .iter() |
| .map(|candidate| candidate.node) |
| .collect()) |
| })(); |
| scratch.recycle_adjacency_windows(); |
| self.query_scratch = scratch; |
| result |
| } |
| |
| pub fn search( |
| &mut self, |
| query: &[f32], |
| top_k: usize, |
| l_search: usize, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| self.last_search_stats = DiskAnnSearchStats { |
| query_count: 1, |
| ..DiskAnnSearchStats::default() |
| }; |
| let dimension = self.header.dimension as usize; |
| if query.len() != dimension { |
| return Err(invalid_input(format!( |
| "query dimension mismatch: expected {}, got {}", |
| dimension, |
| query.len() |
| ))); |
| } |
| if query.iter().any(|value| !value.is_finite()) { |
| return Err(invalid_input("query values must be finite")); |
| } |
| let processed_query = self.preprocess_queries(query, 1); |
| self.search_preprocessed(processed_query.as_ref(), top_k, l_search) |
| } |
| |
| fn search_preprocessed( |
| &mut self, |
| query: &[f32], |
| top_k: usize, |
| l_search: usize, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| self.last_search_stats = DiskAnnSearchStats { |
| query_count: 1, |
| ..DiskAnnSearchStats::default() |
| }; |
| if top_k == 0 { |
| return Ok((Vec::new(), Vec::new())); |
| } |
| self.ensure_resident()?; |
| let search_list_size = resolve_diskann_l_search(top_k, l_search); |
| let mut scratch = std::mem::take(&mut self.query_scratch); |
| let result = (|| { |
| self.generate_graph_candidates( |
| query, |
| search_list_size, |
| self.read_plan().graph_beam_width, |
| &mut scratch, |
| )?; |
| |
| let rerank_count = search_list_size |
| .min(top_k.saturating_mul(4).max(64)) |
| .min(scratch.candidates.len()); |
| if rerank_count == scratch.candidates.len() { |
| if self.header.is_interleaved() { |
| return self.rerank_interleaved( |
| query, |
| &scratch.candidates, |
| top_k, |
| &mut scratch.adjacency_windows, |
| &mut scratch.loaded_adjacency_pages, |
| &mut scratch.window_buffers, |
| ); |
| } |
| return self.rerank( |
| query, |
| &scratch.candidates, |
| top_k, |
| &mut scratch.vector_windows, |
| &mut scratch.window_buffers, |
| ); |
| } |
| if self.header.is_interleaved() { |
| let planner = |
| ReadWindowPlanner::new(self.read_plan(), self.header.sections.adjacency); |
| expand_rerank_candidates_within_seed_windows( |
| &scratch.candidates, |
| rerank_count, |
| |node| { |
| let page = self.adjacency_locator(node)?.page_index as usize; |
| planner |
| .window_for_logical_page(page) |
| .map(|window| window.offset) |
| .ok_or_else(|| invalid_data("DiskANN adjacency page is out of range")) |
| }, |
| &mut scratch.rerank_windows, |
| &mut scratch.rerank_candidates, |
| )?; |
| return self.rerank_interleaved( |
| query, |
| &scratch.rerank_candidates, |
| top_k, |
| &mut scratch.adjacency_windows, |
| &mut scratch.loaded_adjacency_pages, |
| &mut scratch.window_buffers, |
| ); |
| } |
| let planner = VectorWindowPlanner::new( |
| self.read_plan(), |
| self.header.sections.vectors, |
| self.header.vector_record_size as usize, |
| )?; |
| expand_rerank_candidates_within_seed_windows( |
| &scratch.candidates, |
| rerank_count, |
| |node| { |
| planner |
| .window_for_node(node) |
| .map(|window| window.offset) |
| .ok_or_else(|| invalid_data("DiskANN raw-vector record is out of range")) |
| }, |
| &mut scratch.rerank_windows, |
| &mut scratch.rerank_candidates, |
| )?; |
| self.rerank( |
| query, |
| &scratch.rerank_candidates, |
| top_k, |
| &mut scratch.vector_windows, |
| &mut scratch.window_buffers, |
| ) |
| })(); |
| scratch.recycle_adjacency_windows(); |
| self.query_scratch = scratch; |
| result |
| } |
| |
| fn generate_graph_candidates( |
| &mut self, |
| query: &[f32], |
| search_list_size: usize, |
| beam_width: usize, |
| scratch: &mut DiskAnnQueryScratch, |
| ) -> io::Result<()> { |
| let vector_count = self.header.vector_count as usize; |
| let search_list_size = search_list_size.min(vector_count); |
| scratch.begin_graph_search( |
| vector_count, |
| search_list_size, |
| self.header.max_degree as usize, |
| )?; |
| let pq = self.pq()?; |
| let distance_table_len = pq.m * pq.ksub; |
| pq.compute_distance_table( |
| query, |
| self.header.metric_type(), |
| scratch.prepare_distance_table(distance_table_len), |
| ); |
| |
| let entry_node = self.header.entry_node as usize; |
| scratch.mark_visited(entry_node); |
| scratch.insert_graph_candidate( |
| SearchCandidate { |
| node: entry_node, |
| distance: self.pq_distance(entry_node, &scratch.distance_table)?, |
| }, |
| search_list_size, |
| )?; |
| let mut expanded_count = 0usize; |
| while expanded_count < search_list_size { |
| scratch.select_round(beam_width.min(search_list_size - expanded_count)); |
| if scratch.selected_nodes.is_empty() { |
| break; |
| } |
| self.load_adjacency_pages( |
| &scratch.selected_nodes, |
| &mut scratch.adjacency_windows, |
| &mut scratch.loaded_adjacency_pages, |
| &mut scratch.window_buffers, |
| )?; |
| expanded_count += scratch.selected_nodes.len(); |
| for selected_node_index in 0..scratch.selected_nodes.len() { |
| let node = scratch.selected_nodes[selected_node_index]; |
| self.decode_adjacency_neighbors( |
| node, |
| &scratch.adjacency_windows, |
| &mut scratch.neighbor_buffer, |
| )?; |
| let mut retained_neighbors = 0; |
| for neighbor_index in 0..scratch.neighbor_buffer.len() { |
| let neighbor = scratch.neighbor_buffer[neighbor_index] as usize; |
| if !scratch.mark_visited(neighbor) { |
| continue; |
| } |
| scratch.neighbor_buffer[retained_neighbors] = neighbor as u32; |
| retained_neighbors += 1; |
| } |
| scratch.neighbor_buffer.truncate(retained_neighbors); |
| score_pq_neighbors( |
| &scratch.distance_table, |
| self.pq_codes()?, |
| self.header.pq_m as usize, |
| self.header.pq_bits as usize, |
| &scratch.neighbor_buffer, |
| &mut scratch.scored_neighbors, |
| )?; |
| for scored_index in 0..scratch.scored_neighbors.len() { |
| let candidate = scratch.scored_neighbors[scored_index]; |
| scratch.insert_graph_candidate(candidate, search_list_size)?; |
| } |
| } |
| let evictions = scratch.adjacency_windows.trim( |
| &mut scratch.window_buffers, |
| QUERY_ADJACENCY_WINDOW_LIMIT_BYTES, |
| ); |
| self.last_search_stats.query_adjacency_cache_evictions = self |
| .last_search_stats |
| .query_adjacency_cache_evictions |
| .saturating_add(evictions); |
| } |
| scratch.finish_graph_candidates(); |
| Ok(()) |
| } |
| |
| pub fn search_with_roaring_filter( |
| &mut self, |
| query: &[f32], |
| top_k: usize, |
| l_search: usize, |
| roaring_filter_bytes: &[u8], |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| let dimension = self.header.dimension as usize; |
| if query.len() != dimension { |
| return Err(invalid_input(format!( |
| "query dimension mismatch: expected {}, got {}", |
| dimension, |
| query.len() |
| ))); |
| } |
| if query.iter().any(|value| !value.is_finite()) { |
| return Err(invalid_input("query values must be finite")); |
| } |
| let processed_query = self.preprocess_queries(query, 1); |
| let query = processed_query.as_ref(); |
| let filter = decode_roaring_filter(roaring_filter_bytes)?; |
| self.search_with_decoded_roaring_filter(query, top_k, l_search, &filter) |
| } |
| |
| fn search_with_decoded_roaring_filter( |
| &mut self, |
| query: &[f32], |
| top_k: usize, |
| l_search: usize, |
| filter: &RoaringTreemap, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| self.last_search_stats = DiskAnnSearchStats { |
| query_count: 1, |
| ..DiskAnnSearchStats::default() |
| }; |
| if top_k == 0 { |
| return Ok((Vec::new(), Vec::new())); |
| } |
| if filter.is_empty() { |
| return Ok((vec![-1; top_k], vec![f32::MAX; top_k])); |
| } |
| let matching_nodes = self.matching_nodes_for_filter(filter)?; |
| if matching_nodes.is_empty() { |
| return Ok((vec![-1; top_k], vec![f32::MAX; top_k])); |
| } |
| self.search_with_matching_nodes(query, top_k, l_search, &matching_nodes) |
| } |
| |
| fn exhaustive_filtered_candidates( |
| &mut self, |
| query: &[f32], |
| matching_nodes: &RoaringBitmap, |
| candidate_limit: usize, |
| ) -> io::Result<Vec<SearchCandidate>> { |
| let matching_count = usize::try_from(matching_nodes.len()).unwrap_or(usize::MAX); |
| self.last_search_stats.filtered_exhaustive_queries = self |
| .last_search_stats |
| .filtered_exhaustive_queries |
| .saturating_add(1); |
| self.last_search_stats.pq_distance_evaluations = self |
| .last_search_stats |
| .pq_distance_evaluations |
| .saturating_add(matching_count); |
| self.last_search_stats.pq_code_loads = self |
| .last_search_stats |
| .pq_code_loads |
| .saturating_add(matching_count); |
| let mut scratch = std::mem::take(&mut self.query_scratch); |
| let result = (|| { |
| scratch.begin_rerank(); |
| let pq = self.pq()?; |
| let distance_table_len = pq.m * pq.ksub; |
| pq.compute_distance_table( |
| query, |
| self.header.metric_type(), |
| scratch.prepare_distance_table(distance_table_len), |
| ); |
| let pq_codes = self.pq_codes()?; |
| let pq_m = self.header.pq_m as usize; |
| let pq_bits = self.header.pq_bits as usize; |
| let mut candidates = BinaryHeap::new(); |
| candidates |
| .try_reserve(candidate_limit.min(1024)) |
| .map_err(|_| invalid_input("DiskANN filtered candidate allocation failed"))?; |
| for node in matching_nodes.iter() { |
| scratch.neighbor_buffer.push(node); |
| if scratch.neighbor_buffer.len() == FILTERED_SINGLE_PQ_NODE_CHUNK_SIZE { |
| score_filtered_candidate_chunk( |
| &scratch.distance_table, |
| pq_codes, |
| pq_m, |
| pq_bits, |
| &scratch.neighbor_buffer, |
| &mut scratch.scored_neighbors, |
| &mut candidates, |
| candidate_limit, |
| )?; |
| scratch.neighbor_buffer.clear(); |
| } |
| } |
| if !scratch.neighbor_buffer.is_empty() { |
| score_filtered_candidate_chunk( |
| &scratch.distance_table, |
| pq_codes, |
| pq_m, |
| pq_bits, |
| &scratch.neighbor_buffer, |
| &mut scratch.scored_neighbors, |
| &mut candidates, |
| candidate_limit, |
| )?; |
| scratch.neighbor_buffer.clear(); |
| } |
| let mut candidates = candidates.into_vec(); |
| sort_candidates(&mut candidates); |
| Ok(candidates) |
| })(); |
| self.query_scratch = scratch; |
| result |
| } |
| |
| fn matching_nodes_for_filter(&mut self, filter: &RoaringTreemap) -> io::Result<RoaringBitmap> { |
| self.ensure_resident()?; |
| if use_row_id_order(filter.len(), self.header.vector_count as usize) { |
| if let Some(row_id_order) = self.ensure_row_id_order()? { |
| let matching_ranges = |
| matching_ranges_from_row_id_order(&row_id_order, filter, |node| { |
| self.row_id(node) |
| })?; |
| let mut matching = RoaringBitmap::new(); |
| for range in matching_ranges { |
| for &node in &row_id_order[range] { |
| matching.insert(node); |
| } |
| } |
| return Ok(matching); |
| } |
| } |
| matching_nodes_from_sequential_row_ids(filter, |visitor| self.try_for_each_row_id(visitor)) |
| } |
| |
| fn search_with_matching_nodes( |
| &mut self, |
| query: &[f32], |
| top_k: usize, |
| l_search: usize, |
| matching_nodes: &RoaringBitmap, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| if top_k == 0 { |
| return Ok((Vec::new(), Vec::new())); |
| } |
| let candidates = |
| self.generate_filtered_candidates(query, top_k, l_search, matching_nodes)?; |
| self.rerank_with_query_scratch(query, &candidates, top_k) |
| } |
| |
| fn generate_filtered_candidates( |
| &mut self, |
| query: &[f32], |
| top_k: usize, |
| l_search: usize, |
| matching_nodes: &RoaringBitmap, |
| ) -> io::Result<Vec<SearchCandidate>> { |
| let matching_count = usize::try_from(matching_nodes.len()).unwrap_or(usize::MAX); |
| let strategy = select_filtered_candidate_strategy( |
| self.header.vector_count as usize, |
| matching_count, |
| top_k, |
| l_search, |
| self.header.max_degree as usize, |
| self.read_plan(), |
| self.adjacency_fully_preloaded(), |
| ); |
| match strategy { |
| FilteredCandidateStrategy::Exhaustive { target_candidates } => { |
| self.exhaustive_filtered_candidates(query, matching_nodes, target_candidates) |
| } |
| FilteredCandidateStrategy::Graph { |
| target_candidates, |
| search_list_size, |
| } => { |
| self.last_search_stats.filtered_graph_queries = self |
| .last_search_stats |
| .filtered_graph_queries |
| .saturating_add(1); |
| let mut scratch = std::mem::take(&mut self.query_scratch); |
| let graph_candidates = (|| { |
| self.generate_graph_candidates( |
| query, |
| search_list_size, |
| self.options() |
| .read_tier |
| .read_plan() |
| .filtered_graph_beam_width, |
| &mut scratch, |
| )?; |
| Ok::<_, io::Error>(post_filter_graph_candidates( |
| &scratch.candidates, |
| matching_nodes, |
| target_candidates, |
| )) |
| })(); |
| scratch.recycle_adjacency_windows(); |
| self.query_scratch = scratch; |
| if let Some(candidates) = graph_candidates? { |
| Ok(candidates) |
| } else { |
| self.last_search_stats.filtered_graph_fallbacks = self |
| .last_search_stats |
| .filtered_graph_fallbacks |
| .saturating_add(1); |
| self.exhaustive_filtered_candidates(query, matching_nodes, target_candidates) |
| } |
| } |
| } |
| } |
| |
| fn pq_distance(&self, node: usize, distance_table: &[f32]) -> io::Result<f32> { |
| let pq = self.pq()?; |
| let code_size = pq.code_size(); |
| let start = node |
| .checked_mul(code_size) |
| .ok_or_else(|| invalid_data("DiskANN PQ code offset overflows"))?; |
| let end = start + code_size; |
| let codes = self |
| .pq_codes()? |
| .get(start..end) |
| .ok_or_else(|| invalid_data("DiskANN PQ codes are truncated"))?; |
| Ok(pq.distance_from_table(distance_table, codes)) |
| } |
| |
| fn rerank( |
| &mut self, |
| query: &[f32], |
| candidates: &[SearchCandidate], |
| top_k: usize, |
| window_cache: &mut VectorWindowCache, |
| window_buffers: &mut WindowBufferPool, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| let record_size = self.header.vector_record_size as usize; |
| let encoding = self.header.raw_vector_encoding(); |
| let planner = |
| VectorWindowPlanner::new(self.read_plan(), self.header.sections.vectors, record_size)?; |
| let windows = planner.plan_nodes(candidates.iter().map(|candidate| candidate.node)); |
| self.last_search_stats.rerank_candidate_references = self |
| .last_search_stats |
| .rerank_candidate_references |
| .saturating_add(candidates.len()); |
| self.last_search_stats.rerank_unique_windows = self |
| .last_search_stats |
| .rerank_unique_windows |
| .saturating_add(windows.len()); |
| self.last_search_stats.rerank_chunks = self |
| .last_search_stats |
| .rerank_chunks |
| .saturating_add(usize::from(!windows.is_empty())); |
| let raw_vector_cache_bytes = self.options().raw_vector_cache_bytes; |
| let (retain_vector_windows, preparation_evictions) = prepare_vector_window_cache( |
| &windows, |
| window_cache, |
| window_buffers, |
| raw_vector_cache_bytes, |
| ); |
| let distance_kernel = selected_raw_vector_distance_kernel(query.len()); |
| let metric = self.header.metric_type(); |
| self.last_search_stats.raw_vector_cache_evictions = self |
| .last_search_stats |
| .raw_vector_cache_evictions |
| .saturating_add(preparation_evictions); |
| let cache_load = self.load_vector_windows(&windows, window_cache, window_buffers)?; |
| self.last_search_stats.raw_vector_cache_hits = self |
| .last_search_stats |
| .raw_vector_cache_hits |
| .saturating_add(cache_load.hits); |
| self.last_search_stats.raw_vector_cache_misses = self |
| .last_search_stats |
| .raw_vector_cache_misses |
| .saturating_add(cache_load.misses); |
| self.last_search_stats.raw_vector_cache_evictions = self |
| .last_search_stats |
| .raw_vector_cache_evictions |
| .saturating_add(cache_load.evictions); |
| let result = (|| { |
| let mut exact = BinaryHeap::new(); |
| exact |
| .try_reserve(top_k.min(candidates.len())) |
| .map_err(|_| invalid_input("DiskANN exact result allocation failed"))?; |
| for candidate in candidates { |
| let window = planner |
| .window_for_node(candidate.node) |
| .ok_or_else(|| invalid_data("DiskANN raw-vector record is out of range"))?; |
| let payload = window_cache |
| .get(&window.offset) |
| .ok_or_else(|| invalid_data("DiskANN vector window is not loaded"))?; |
| let record = planner.record(window, payload, candidate.node)?; |
| let distance = |
| raw_vector_distance(query, record, encoding, metric, distance_kernel)?; |
| let row_id = self.row_id(candidate.node)?; |
| push_bounded_exact_result( |
| &mut exact, |
| ExactSearchResult { row_id, distance }, |
| top_k, |
| )?; |
| } |
| let exact = exact.into_sorted_vec(); |
| let mut ids = exact.iter().map(|result| result.row_id).collect::<Vec<_>>(); |
| let mut distances = exact |
| .iter() |
| .map(|result| result.distance) |
| .collect::<Vec<_>>(); |
| ids.resize(top_k, -1); |
| distances.resize(top_k, f32::MAX); |
| Ok((ids, distances)) |
| })(); |
| if retain_vector_windows && result.is_ok() { |
| window_cache.touch_windows(&windows); |
| let evictions = window_cache.trim(window_buffers, raw_vector_cache_bytes); |
| self.last_search_stats.raw_vector_cache_evictions = self |
| .last_search_stats |
| .raw_vector_cache_evictions |
| .saturating_add(evictions); |
| } else { |
| window_cache.recycle(window_buffers); |
| } |
| result |
| } |
| |
| fn rerank_interleaved( |
| &mut self, |
| query: &[f32], |
| candidates: &[SearchCandidate], |
| top_k: usize, |
| adjacency_windows: &mut AdjacencyWindowCache, |
| loaded_pages: &mut HashSet<usize>, |
| window_buffers: &mut WindowBufferPool, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| let nodes = candidates |
| .iter() |
| .map(|candidate| candidate.node) |
| .collect::<Vec<_>>(); |
| self.load_adjacency_pages(&nodes, adjacency_windows, loaded_pages, window_buffers)?; |
| let planner = ReadWindowPlanner::new(self.read_plan(), self.header.sections.adjacency); |
| let windows = nodes |
| .iter() |
| .map(|&node| { |
| let page = self.adjacency_locator(node)?.page_index as usize; |
| planner |
| .window_for_logical_page(page) |
| .map(|window| window.offset) |
| .ok_or_else(|| invalid_data("DiskANN adjacency page is out of range")) |
| }) |
| .collect::<io::Result<BTreeSet<_>>>()?; |
| self.last_search_stats.rerank_candidate_references = self |
| .last_search_stats |
| .rerank_candidate_references |
| .saturating_add(candidates.len()); |
| self.last_search_stats.rerank_unique_windows = self |
| .last_search_stats |
| .rerank_unique_windows |
| .saturating_add(windows.len()); |
| self.last_search_stats.rerank_chunks = self |
| .last_search_stats |
| .rerank_chunks |
| .saturating_add(usize::from(!windows.is_empty())); |
| let record_size = self.header.vector_record_size as usize; |
| let encoding = self.header.raw_vector_encoding(); |
| let distance_kernel = selected_raw_vector_distance_kernel(query.len()); |
| let metric = self.header.metric_type(); |
| let mut exact = BinaryHeap::new(); |
| exact |
| .try_reserve(top_k.min(candidates.len())) |
| .map_err(|_| invalid_input("DiskANN exact result allocation failed"))?; |
| for candidate in candidates { |
| let locator = self.adjacency_locator(candidate.node)?; |
| let page_index = locator.page_index as usize; |
| let window = planner |
| .window_for_logical_page(page_index) |
| .ok_or_else(|| invalid_data("DiskANN adjacency page is out of range"))?; |
| let payload = if let Some(hot) = self.hot_adjacency_window(window.offset, window.length) |
| { |
| hot |
| } else { |
| adjacency_windows |
| .get(&window.offset) |
| .map(WindowPayload::as_slice) |
| .ok_or_else(|| invalid_data("DiskANN adjacency rerank window is not loaded"))? |
| }; |
| let page_offset = self.header.sections.adjacency.offset |
| + page_index as u64 * DISKANN_PAGE_SIZE as u64 |
| - window.offset; |
| let record_offset = (page_offset as usize) |
| .checked_add(locator.byte_offset as usize) |
| .and_then(|offset| offset.checked_sub(record_size)) |
| .ok_or_else(|| invalid_data("DiskANN interleaved vector offset underflows"))?; |
| let record = payload |
| .get(record_offset..record_offset + record_size) |
| .ok_or_else(|| invalid_data("DiskANN interleaved raw vector is truncated"))?; |
| let distance = raw_vector_distance(query, record, encoding, metric, distance_kernel)?; |
| push_bounded_exact_result( |
| &mut exact, |
| ExactSearchResult { |
| row_id: self.row_id(candidate.node)?, |
| distance, |
| }, |
| top_k, |
| )?; |
| } |
| let exact = exact.into_sorted_vec(); |
| let mut ids = exact.iter().map(|result| result.row_id).collect::<Vec<_>>(); |
| let mut distances = exact |
| .iter() |
| .map(|result| result.distance) |
| .collect::<Vec<_>>(); |
| ids.resize(top_k, -1); |
| distances.resize(top_k, f32::MAX); |
| Ok((ids, distances)) |
| } |
| |
| fn rerank_with_query_scratch( |
| &mut self, |
| query: &[f32], |
| candidates: &[SearchCandidate], |
| top_k: usize, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| let mut scratch = std::mem::take(&mut self.query_scratch); |
| let result = { |
| scratch.begin_rerank(); |
| if self.header.is_interleaved() { |
| self.rerank_interleaved( |
| query, |
| candidates, |
| top_k, |
| &mut scratch.adjacency_windows, |
| &mut scratch.loaded_adjacency_pages, |
| &mut scratch.window_buffers, |
| ) |
| } else { |
| self.rerank( |
| query, |
| candidates, |
| top_k, |
| &mut scratch.vector_windows, |
| &mut scratch.window_buffers, |
| ) |
| } |
| }; |
| scratch.recycle_adjacency_windows(); |
| self.query_scratch = scratch; |
| result |
| } |
| |
| fn rerank_candidate_batch_streaming( |
| &mut self, |
| queries: &[f32], |
| top_k: usize, |
| partitions: Vec<CandidatePartition>, |
| ) -> io::Result<(Vec<i64>, Vec<f32>)> { |
| if self.header.is_interleaved() { |
| return Err(invalid_data( |
| "DiskANN interleaved rerank must run inside a search session", |
| )); |
| } |
| let dimension = self.header.dimension as usize; |
| let query_count = queries.len() / dimension; |
| let distance_kernel = selected_raw_vector_distance_kernel(dimension); |
| let metric = self.header.metric_type(); |
| let mut candidate_sets = (0..query_count).map(|_| None).collect::<Vec<_>>(); |
| for (query_index, candidates) in partitions.into_iter().flatten() { |
| let slot = candidate_sets |
| .get_mut(query_index) |
| .ok_or_else(|| invalid_data("DiskANN filtered batch query index is invalid"))?; |
| if slot.replace(candidates).is_some() { |
| return Err(invalid_data( |
| "DiskANN filtered batch contains duplicate query results", |
| )); |
| } |
| } |
| if candidate_sets.iter().any(Option::is_none) { |
| return Err(invalid_data( |
| "DiskANN filtered batch is missing query candidates", |
| )); |
| } |
| |
| let record_size = self.header.vector_record_size as usize; |
| let encoding = self.header.raw_vector_encoding(); |
| let planner = |
| VectorWindowPlanner::new(self.read_plan(), self.header.sections.vectors, record_size)?; |
| let mut grouped = HashMap::<u64, (ReadWindow, Vec<(usize, usize)>)>::new(); |
| for (query_index, candidates) in candidate_sets.into_iter().enumerate() { |
| for node in candidates.expect("validated DiskANN batch candidates") { |
| let window = planner |
| .window_for_node(node) |
| .ok_or_else(|| invalid_data("DiskANN raw-vector record is out of range"))?; |
| grouped |
| .entry(window.offset) |
| .or_insert_with(|| (window, Vec::new())) |
| .1 |
| .push((query_index, node)); |
| } |
| } |
| let mut window_groups = grouped.into_values().collect::<Vec<_>>(); |
| window_groups.sort_unstable_by_key(|(window, _)| window.offset); |
| let windows = window_groups |
| .iter() |
| .map(|(window, _)| *window) |
| .collect::<Vec<_>>(); |
| let chunks = plan_streaming_window_chunks(&windows); |
| self.last_search_stats.rerank_candidate_references = self |
| .last_search_stats |
| .rerank_candidate_references |
| .saturating_add( |
| window_groups |
| .iter() |
| .map(|(_, references)| references.len()) |
| .sum::<usize>(), |
| ); |
| self.last_search_stats.rerank_unique_windows = self |
| .last_search_stats |
| .rerank_unique_windows |
| .saturating_add(windows.len()); |
| self.last_search_stats.rerank_chunks = self |
| .last_search_stats |
| .rerank_chunks |
| .saturating_add(chunks.len()); |
| let mut exact_heaps = (0..query_count) |
| .map(|_| BinaryHeap::new()) |
| .collect::<Vec<BinaryHeap<ExactSearchResult>>>(); |
| let raw_vector_cache_bytes = self.options().raw_vector_cache_bytes; |
| let mut scratch = std::mem::take(&mut self.query_scratch); |
| scratch.begin_rerank(); |
| let result = (|| { |
| for chunk in chunks { |
| let chunk_windows = &windows[chunk.clone()]; |
| let cache_load = self.load_vector_windows( |
| chunk_windows, |
| &mut scratch.vector_windows, |
| &mut scratch.window_buffers, |
| )?; |
| self.last_search_stats.raw_vector_cache_hits = self |
| .last_search_stats |
| .raw_vector_cache_hits |
| .saturating_add(cache_load.hits); |
| self.last_search_stats.raw_vector_cache_misses = self |
| .last_search_stats |
| .raw_vector_cache_misses |
| .saturating_add(cache_load.misses); |
| self.last_search_stats.raw_vector_cache_evictions = self |
| .last_search_stats |
| .raw_vector_cache_evictions |
| .saturating_add(cache_load.evictions); |
| let chunk_reference_count = window_groups[chunk.clone()] |
| .iter() |
| .map(|(_, references)| references.len()) |
| .sum::<usize>(); |
| let parallel = rayon::current_num_threads() > 1 |
| && chunk_reference_count.saturating_mul(dimension) |
| >= PARALLEL_EXACT_RERANK_MIN_COMPONENTS; |
| if parallel { |
| self.last_search_stats.parallel_exact_rerank_chunks = self |
| .last_search_stats |
| .parallel_exact_rerank_chunks |
| .saturating_add(1); |
| self.last_search_stats.parallel_exact_rerank_references = self |
| .last_search_stats |
| .parallel_exact_rerank_references |
| .saturating_add(chunk_reference_count); |
| let mut rerank_references = Vec::with_capacity(chunk_reference_count); |
| for (window, references) in &window_groups[chunk.clone()] { |
| let payload = scratch |
| .vector_windows |
| .get(&window.offset) |
| .ok_or_else(|| invalid_data("DiskANN vector window is not loaded"))?; |
| for &(query_index, node) in references { |
| rerank_references.push(ExactRerankReference { |
| query_index, |
| row_id: self.row_id(node)?, |
| record: planner.record(*window, payload, node)?, |
| }); |
| } |
| } |
| let exact_results = rerank_references |
| .par_iter() |
| .map(|reference| { |
| let query = &queries[reference.query_index * dimension |
| ..(reference.query_index + 1) * dimension]; |
| Ok::<_, io::Error>(( |
| reference.query_index, |
| ExactSearchResult { |
| row_id: reference.row_id, |
| distance: raw_vector_distance( |
| query, |
| reference.record, |
| encoding, |
| metric, |
| distance_kernel, |
| )?, |
| }, |
| )) |
| }) |
| .collect::<io::Result<Vec<_>>>()?; |
| for (query_index, exact) in exact_results { |
| push_bounded_exact_result(&mut exact_heaps[query_index], exact, top_k)?; |
| } |
| } else { |
| for (window, references) in &window_groups[chunk.clone()] { |
| let payload = scratch |
| .vector_windows |
| .get(&window.offset) |
| .ok_or_else(|| invalid_data("DiskANN vector window is not loaded"))?; |
| for &(query_index, node) in references { |
| let query = |
| &queries[query_index * dimension..(query_index + 1) * dimension]; |
| push_bounded_exact_result( |
| &mut exact_heaps[query_index], |
| ExactSearchResult { |
| row_id: self.row_id(node)?, |
| distance: raw_vector_distance( |
| query, |
| planner.record(*window, payload, node)?, |
| encoding, |
| metric, |
| distance_kernel, |
| )?, |
| }, |
| top_k, |
| )?; |
| } |
| } |
| } |
| scratch.vector_windows.touch_windows(chunk_windows); |
| let evictions = scratch |
| .vector_windows |
| .trim(&mut scratch.window_buffers, raw_vector_cache_bytes); |
| self.last_search_stats.raw_vector_cache_evictions = self |
| .last_search_stats |
| .raw_vector_cache_evictions |
| .saturating_add(evictions); |
| } |
| |
| let mut ids = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| let mut distances = Vec::with_capacity(query_count.saturating_mul(top_k)); |
| for heap in exact_heaps { |
| let exact = heap.into_sorted_vec(); |
| ids.extend(exact.iter().map(|result| result.row_id)); |
| distances.extend(exact.iter().map(|result| result.distance)); |
| ids.resize(ids.len() + top_k - exact.len(), -1); |
| distances.resize(distances.len() + top_k - exact.len(), f32::MAX); |
| } |
| Ok((ids, distances)) |
| })(); |
| if result.is_err() { |
| scratch.recycle_window_caches(); |
| } else { |
| scratch.recycle_adjacency_windows(); |
| } |
| self.query_scratch = scratch; |
| result |
| } |
| |
| fn load_adjacency_pages( |
| &mut self, |
| nodes: &[usize], |
| window_cache: &mut AdjacencyWindowCache, |
| loaded_pages: &mut HashSet<usize>, |
| window_buffers: &mut WindowBufferPool, |
| ) -> io::Result<()> { |
| let planner = ReadWindowPlanner::new(self.read_plan(), self.header.sections.adjacency); |
| let mut pages = BTreeSet::new(); |
| let mut required_windows = Vec::with_capacity(nodes.len()); |
| for &node in nodes { |
| let page = self.adjacency_locator(node)?.page_index as usize; |
| let window = planner |
| .window_for_logical_page(page) |
| .ok_or_else(|| invalid_data("DiskANN adjacency page is out of range"))?; |
| let window_is_hot = self |
| .hot_adjacency_window(window.offset, window.length) |
| .is_some(); |
| if !window_is_hot { |
| required_windows.push(window); |
| } |
| let window_is_available = window_is_hot || window_cache.contains_key(&window.offset); |
| if !loaded_pages.contains(&page) || !window_is_available { |
| pages.insert(page); |
| } |
| } |
| if pages.is_empty() { |
| return Ok(()); |
| } |
| let windows = planner.plan_logical_pages(pages.iter().copied()); |
| let cold_windows = windows |
| .iter() |
| .copied() |
| .filter(|window| { |
| self.hot_adjacency_window(window.offset, window.length) |
| .is_none() |
| }) |
| .collect::<Vec<_>>(); |
| let incoming_bytes = cold_windows |
| .iter() |
| .filter(|window| !window_cache.contains_key(&window.offset)) |
| .fold(0usize, |total, window| total.saturating_add(window.length)); |
| let preparation_evictions = prepare_adjacency_window_cache( |
| &required_windows, |
| incoming_bytes, |
| window_cache, |
| window_buffers, |
| ); |
| self.last_search_stats.query_adjacency_cache_evictions = self |
| .last_search_stats |
| .query_adjacency_cache_evictions |
| .saturating_add(preparation_evictions); |
| self.load_adjacency_windows(&cold_windows, window_cache, window_buffers)?; |
| self.last_search_stats.query_adjacency_cache_peak_bytes = self |
| .last_search_stats |
| .query_adjacency_cache_peak_bytes |
| .max(window_cache.retained_capacity()); |
| for page_index in pages { |
| let window = planner |
| .window_for_logical_page(page_index) |
| .ok_or_else(|| invalid_data("DiskANN adjacency page is out of range"))?; |
| let payload = if let Some(hot) = self.hot_adjacency_window(window.offset, window.length) |
| { |
| hot |
| } else { |
| window_cache |
| .get(&window.offset) |
| .map(WindowPayload::as_slice) |
| .ok_or_else(|| { |
| invalid_data("DiskANN adjacency validation window is not loaded") |
| })? |
| }; |
| let page_offset = self.header.sections.adjacency.offset |
| + page_index as u64 * DISKANN_PAGE_SIZE as u64 |
| - window.offset; |
| let page_start = page_offset as usize; |
| let page_end = page_start + DISKANN_PAGE_SIZE as usize; |
| self.validate_adjacency_page( |
| page_index, |
| payload |
| .get(page_start..page_end) |
| .ok_or_else(|| invalid_data("DiskANN adjacency page is truncated"))?, |
| )?; |
| loaded_pages.insert(page_index); |
| } |
| Ok(()) |
| } |
| |
| fn decode_adjacency_neighbors( |
| &self, |
| node: usize, |
| window_cache: &AdjacencyWindowCache, |
| neighbors: &mut Vec<u32>, |
| ) -> io::Result<()> { |
| let locator = self.adjacency_locator(node)?; |
| let page_index = locator.page_index as usize; |
| let planner = ReadWindowPlanner::new(self.read_plan(), self.header.sections.adjacency); |
| let window = planner |
| .window_for_logical_page(page_index) |
| .ok_or_else(|| invalid_data("DiskANN adjacency page is out of range"))?; |
| let payload = if let Some(hot) = self.hot_adjacency_window(window.offset, window.length) { |
| hot |
| } else { |
| window_cache |
| .get(&window.offset) |
| .map(WindowPayload::as_slice) |
| .ok_or_else(|| invalid_data("DiskANN adjacency decode window is not loaded"))? |
| }; |
| let page_offset = self.header.sections.adjacency.offset |
| + page_index as u64 * DISKANN_PAGE_SIZE as u64 |
| - window.offset; |
| let start = page_offset as usize + locator.byte_offset as usize; |
| let bytes = payload |
| .get(start..) |
| .ok_or_else(|| invalid_data("DiskANN adjacency list is truncated"))?; |
| decode_adjacency_list(bytes, locator.degree(), locator.encoding(), neighbors)?; |
| Ok(()) |
| } |
| |
| fn load_adjacency_windows( |
| &mut self, |
| windows: &[ReadWindow], |
| local_cache: &mut AdjacencyWindowCache, |
| window_buffers: &mut WindowBufferPool, |
| ) -> io::Result<()> { |
| let mut pending = windows |
| .iter() |
| .copied() |
| .filter(|window| !local_cache.contains_key(&window.offset)) |
| .collect::<Vec<_>>(); |
| if pending.is_empty() { |
| return Ok(()); |
| } |
| if self.options().adjacency_cache_bytes == 0 { |
| self.last_search_stats.adjacency_cache_misses = self |
| .last_search_stats |
| .adjacency_cache_misses |
| .saturating_add(pending.len()); |
| let mut payloads = Vec::with_capacity(pending.len()); |
| for window in &pending { |
| payloads.push(window_buffers.take(window.length)?); |
| } |
| let read_result = { |
| let mut requests = pending |
| .iter() |
| .zip(payloads.iter_mut()) |
| .map(|(window, payload)| ReadRequest::new(window.offset, payload)) |
| .collect::<Vec<_>>(); |
| self.pread_ranges(&mut requests) |
| }; |
| if let Err(error) = read_result { |
| for payload in payloads { |
| window_buffers.recycle(payload); |
| } |
| return Err(error); |
| } |
| for (window, payload) in pending.into_iter().zip(payloads) { |
| local_cache.insert(window.offset, WindowPayload::Owned(payload)); |
| } |
| return Ok(()); |
| } |
| |
| while !pending.is_empty() { |
| let mut reserved = Vec::new(); |
| let mut waiting = Vec::new(); |
| for window in &pending { |
| let (lookup, lock_metrics) = self |
| .adjacency_cache()? |
| .lookup_or_reserve(window.offset, window.length)?; |
| self.last_search_stats |
| .record_adjacency_cache_lock(lock_metrics); |
| match lookup { |
| SharedWindowCacheLookup::Hit(payload) => { |
| local_cache.insert(window.offset, WindowPayload::Shared(payload)); |
| self.last_search_stats.adjacency_cache_hits = self |
| .last_search_stats |
| .adjacency_cache_hits |
| .saturating_add(1); |
| } |
| SharedWindowCacheLookup::Reserved => { |
| reserved.push(*window); |
| self.last_search_stats.adjacency_cache_misses = self |
| .last_search_stats |
| .adjacency_cache_misses |
| .saturating_add(1); |
| } |
| SharedWindowCacheLookup::Loading => { |
| waiting.push(*window); |
| self.last_search_stats.adjacency_cache_waits = self |
| .last_search_stats |
| .adjacency_cache_waits |
| .saturating_add(1); |
| } |
| } |
| } |
| |
| if !reserved.is_empty() { |
| let mut payloads = Vec::with_capacity(reserved.len()); |
| for window in &reserved { |
| match window_buffers.take(window.length) { |
| Ok(payload) => payloads.push(payload), |
| Err(error) => { |
| let lock_metrics = self.adjacency_cache()?.cancel( |
| &reserved |
| .iter() |
| .map(|window| window.offset) |
| .collect::<Vec<_>>(), |
| )?; |
| self.last_search_stats |
| .record_adjacency_cache_lock(lock_metrics); |
| for payload in payloads { |
| window_buffers.recycle(payload); |
| } |
| return Err(error); |
| } |
| } |
| } |
| let read_result = { |
| let mut requests = reserved |
| .iter() |
| .zip(payloads.iter_mut()) |
| .map(|(window, payload)| ReadRequest::new(window.offset, payload)) |
| .collect::<Vec<_>>(); |
| self.pread_ranges(&mut requests) |
| }; |
| if let Err(error) = read_result { |
| let lock_metrics = self.adjacency_cache()?.cancel( |
| &reserved |
| .iter() |
| .map(|window| window.offset) |
| .collect::<Vec<_>>(), |
| )?; |
| self.last_search_stats |
| .record_adjacency_cache_lock(lock_metrics); |
| for payload in payloads { |
| window_buffers.recycle(payload); |
| } |
| return Err(error); |
| } |
| for (window, payload) in reserved.into_iter().zip(payloads) { |
| let payload = share_window_payload(payload); |
| local_cache.insert(window.offset, WindowPayload::Shared(Arc::clone(&payload))); |
| let (evictions, lock_metrics) = |
| self.adjacency_cache()?.publish(window.offset, payload)?; |
| self.last_search_stats |
| .record_adjacency_cache_lock(lock_metrics); |
| self.last_search_stats.adjacency_cache_evictions = self |
| .last_search_stats |
| .adjacency_cache_evictions |
| .saturating_add(evictions); |
| } |
| } |
| |
| for window in waiting { |
| let (payload, lock_metrics) = self |
| .adjacency_cache()? |
| .wait_for(window.offset, window.length)?; |
| self.last_search_stats |
| .record_adjacency_cache_lock(lock_metrics); |
| if let Some(payload) = payload { |
| local_cache.insert(window.offset, WindowPayload::Shared(payload)); |
| self.last_search_stats.adjacency_cache_hits = self |
| .last_search_stats |
| .adjacency_cache_hits |
| .saturating_add(1); |
| } |
| } |
| pending.retain(|window| !local_cache.contains_key(&window.offset)); |
| } |
| Ok(()) |
| } |
| |
| fn load_vector_windows( |
| &mut self, |
| windows: &[ReadWindow], |
| cache: &mut VectorWindowCache, |
| window_buffers: &mut WindowBufferPool, |
| ) -> io::Result<VectorWindowLoadStats> { |
| let mut pending = windows |
| .iter() |
| .copied() |
| .filter(|window| !cache.contains_key(&window.offset)) |
| .collect::<Vec<_>>(); |
| let mut stats = VectorWindowLoadStats { |
| hits: windows.len().saturating_sub(pending.len()), |
| ..VectorWindowLoadStats::default() |
| }; |
| if pending.is_empty() { |
| return Ok(stats); |
| } |
| |
| if self.options().raw_vector_cache_bytes == 0 { |
| stats.misses = pending.len(); |
| let mut payloads = Vec::with_capacity(pending.len()); |
| for window in &pending { |
| payloads.push(window_buffers.take(window.length)?); |
| } |
| let read_result = { |
| let mut requests = pending |
| .iter() |
| .zip(payloads.iter_mut()) |
| .map(|(window, payload)| ReadRequest::new(window.offset, payload)) |
| .collect::<Vec<_>>(); |
| self.pread_ranges(&mut requests) |
| }; |
| if let Err(error) = read_result { |
| for payload in payloads { |
| window_buffers.recycle(payload); |
| } |
| return Err(error); |
| } |
| for (window, payload) in pending.into_iter().zip(payloads) { |
| cache.insert(window.offset, payload); |
| } |
| return Ok(stats); |
| } |
| |
| while !pending.is_empty() { |
| let mut reserved = Vec::new(); |
| let mut waiting = Vec::new(); |
| for window in &pending { |
| match self |
| .raw_vector_cache()? |
| .lookup_or_reserve(window.offset, window.length)? |
| .0 |
| { |
| SharedWindowCacheLookup::Hit(payload) => { |
| cache.insert(window.offset, WindowPayload::Shared(payload)); |
| stats.hits = stats.hits.saturating_add(1); |
| } |
| SharedWindowCacheLookup::Reserved => { |
| reserved.push(*window); |
| stats.misses = stats.misses.saturating_add(1); |
| } |
| SharedWindowCacheLookup::Loading => waiting.push(*window), |
| } |
| } |
| |
| if !reserved.is_empty() { |
| let mut payloads = Vec::with_capacity(reserved.len()); |
| for window in &reserved { |
| match window_buffers.take(window.length) { |
| Ok(payload) => payloads.push(payload), |
| Err(error) => { |
| self.raw_vector_cache()?.cancel( |
| &reserved |
| .iter() |
| .map(|window| window.offset) |
| .collect::<Vec<_>>(), |
| )?; |
| for payload in payloads { |
| window_buffers.recycle(payload); |
| } |
| return Err(error); |
| } |
| } |
| } |
| let read_result = { |
| let mut requests = reserved |
| .iter() |
| .zip(payloads.iter_mut()) |
| .map(|(window, payload)| ReadRequest::new(window.offset, payload)) |
| .collect::<Vec<_>>(); |
| self.pread_ranges(&mut requests) |
| }; |
| if let Err(error) = read_result { |
| self.raw_vector_cache()?.cancel( |
| &reserved |
| .iter() |
| .map(|window| window.offset) |
| .collect::<Vec<_>>(), |
| )?; |
| for payload in payloads { |
| window_buffers.recycle(payload); |
| } |
| return Err(error); |
| } |
| for (window, payload) in reserved.into_iter().zip(payloads) { |
| let payload = share_window_payload(payload); |
| cache.insert(window.offset, WindowPayload::Shared(Arc::clone(&payload))); |
| stats.evictions = stats.evictions.saturating_add( |
| self.raw_vector_cache()?.publish(window.offset, payload)?.0, |
| ); |
| } |
| } |
| |
| for window in waiting { |
| if let Some(payload) = self |
| .raw_vector_cache()? |
| .wait_for(window.offset, window.length)? |
| .0 |
| { |
| cache.insert(window.offset, WindowPayload::Shared(payload)); |
| stats.hits = stats.hits.saturating_add(1); |
| } |
| } |
| pending.retain(|window| !cache.contains_key(&window.offset)); |
| } |
| Ok(stats) |
| } |
| } |
| |
| fn sort_candidates(candidates: &mut [SearchCandidate]) { |
| candidates.sort_by(|left, right| { |
| left.distance |
| .total_cmp(&right.distance) |
| .then_with(|| left.node.cmp(&right.node)) |
| }); |
| } |
| |
| fn desired_filtered_candidate_count(matching_count: usize, top_k: usize) -> usize { |
| matching_count.min(top_k.saturating_mul(4).max(64)) |
| } |
| |
| fn resolve_diskann_l_search(top_k: usize, l_search: usize) -> usize { |
| let configured = if l_search == 0 { |
| top_k.saturating_mul(2).max(100) |
| } else { |
| l_search |
| }; |
| top_k.max(configured) |
| } |
| |
| fn topk_result_stability( |
| left_ids: &[i64], |
| left_distances: &[f32], |
| right_ids: &[i64], |
| right_distances: &[f32], |
| top_k: usize, |
| ) -> f32 { |
| if top_k == 0 |
| || left_ids.len() != right_ids.len() |
| || left_ids.len() != left_distances.len() |
| || right_ids.len() != right_distances.len() |
| || !left_ids.len().is_multiple_of(top_k) |
| { |
| return 0.0; |
| } |
| let mut overlap = 0usize; |
| let mut denominator = 0usize; |
| for (((left_query_ids, left_query_distances), right_query_ids), right_query_distances) in |
| left_ids |
| .chunks_exact(top_k) |
| .zip(left_distances.chunks_exact(top_k)) |
| .zip(right_ids.chunks_exact(top_k)) |
| .zip(right_distances.chunks_exact(top_k)) |
| { |
| let mut right_counts = HashMap::<i64, usize>::with_capacity(top_k); |
| for (&row_id, &distance) in right_query_ids.iter().zip(right_query_distances) { |
| if distance != f32::MAX { |
| *right_counts.entry(row_id).or_default() += 1; |
| } |
| } |
| for (&row_id, &distance) in left_query_ids.iter().zip(left_query_distances) { |
| if distance != f32::MAX { |
| denominator += 1; |
| if right_counts.get_mut(&row_id).is_some_and(|count| { |
| if *count == 0 { |
| false |
| } else { |
| *count -= 1; |
| true |
| } |
| }) { |
| overlap += 1; |
| } |
| } |
| } |
| } |
| if denominator == 0 { |
| 1.0 |
| } else { |
| overlap as f32 / denominator as f32 |
| } |
| } |
| |
| fn filtered_pq_query_tile_size(pq_m: usize, pq_ksub: usize) -> usize { |
| let table_bytes_per_query = pq_m |
| .saturating_mul(pq_ksub) |
| .saturating_mul(size_of::<f32>()) |
| .max(1); |
| (FILTERED_PQ_TILE_TABLE_LIMIT_BYTES / table_bytes_per_query) |
| .clamp(1, FILTERED_PQ_MAX_QUERY_TILE_SIZE) |
| } |
| |
| fn expand_rerank_candidates_within_seed_windows( |
| candidates: &[SearchCandidate], |
| seed_count: usize, |
| mut window_for_node: impl FnMut(usize) -> io::Result<u64>, |
| selected_windows: &mut HashSet<u64>, |
| selected: &mut Vec<SearchCandidate>, |
| ) -> io::Result<()> { |
| selected.clear(); |
| selected_windows.clear(); |
| if candidates.is_empty() || seed_count == 0 { |
| return Ok(()); |
| } |
| if seed_count >= candidates.len() { |
| selected |
| .try_reserve(candidates.len()) |
| .map_err(|_| invalid_input("DiskANN rerank candidate allocation failed"))?; |
| selected.extend_from_slice(candidates); |
| return Ok(()); |
| } |
| selected_windows |
| .try_reserve(seed_count) |
| .map_err(|_| invalid_input("DiskANN rerank window allocation failed"))?; |
| for candidate in candidates.iter().take(seed_count) { |
| selected_windows.insert(window_for_node(candidate.node)?); |
| } |
| selected |
| .try_reserve(candidates.len().min(seed_count.saturating_mul(2))) |
| .map_err(|_| invalid_input("DiskANN rerank candidate allocation failed"))?; |
| for candidate in candidates { |
| if selected_windows.contains(&window_for_node(candidate.node)?) { |
| selected.push(*candidate); |
| } |
| } |
| Ok(()) |
| } |
| |
| fn adaptive_filtered_search_list_size( |
| vector_count: usize, |
| matching_count: usize, |
| target_candidates: usize, |
| configured_l_search: usize, |
| ) -> usize { |
| if vector_count == 0 || matching_count == 0 || target_candidates == 0 { |
| return 0; |
| } |
| let scaled_target = (target_candidates as u128) |
| .saturating_mul(vector_count as u128) |
| .div_ceil(matching_count as u128) |
| .saturating_mul(2) |
| .min(vector_count as u128) as usize; |
| configured_l_search.max(scaled_target).min(vector_count) |
| } |
| |
| #[allow(clippy::too_many_arguments)] |
| fn select_filtered_candidate_strategy( |
| vector_count: usize, |
| matching_count: usize, |
| top_k: usize, |
| l_search: usize, |
| max_degree: usize, |
| read_plan: ReadPlan, |
| adjacency_fully_preloaded: bool, |
| ) -> FilteredCandidateStrategy { |
| let target_candidates = desired_filtered_candidate_count(matching_count, top_k); |
| let exhaustive = FilteredCandidateStrategy::Exhaustive { target_candidates }; |
| let configured_l_search = resolve_diskann_l_search(top_k, l_search); |
| if vector_count == 0 |
| || matching_count != vector_count |
| || configured_l_search < 200 |
| || (read_plan.window_bytes > 16 * 1024 && !adjacency_fully_preloaded) |
| { |
| return exhaustive; |
| } |
| let search_list_size = adaptive_filtered_search_list_size( |
| vector_count, |
| matching_count, |
| target_candidates, |
| configured_l_search, |
| ); |
| let graph_work = search_list_size |
| .saturating_mul(max_degree.saturating_add(1)) |
| .min(vector_count); |
| if graph_work > matching_count / 2 { |
| return exhaustive; |
| } |
| FilteredCandidateStrategy::Graph { |
| target_candidates, |
| search_list_size, |
| } |
| } |
| |
| fn post_filter_graph_candidates( |
| graph_candidates: &[SearchCandidate], |
| matching_nodes: &RoaringBitmap, |
| target_candidates: usize, |
| ) -> Option<Vec<SearchCandidate>> { |
| let mut filtered = Vec::with_capacity(target_candidates); |
| for candidate in graph_candidates { |
| let Ok(node) = u32::try_from(candidate.node) else { |
| continue; |
| }; |
| if matching_nodes.contains(node) { |
| filtered.push(*candidate); |
| if filtered.len() == target_candidates { |
| break; |
| } |
| } |
| } |
| (filtered.len() == target_candidates).then_some(filtered) |
| } |
| |
| fn plan_streaming_window_chunks(windows: &[ReadWindow]) -> Vec<std::ops::Range<usize>> { |
| let mut chunks = Vec::new(); |
| let mut start = 0usize; |
| while start < windows.len() { |
| let mut end = start; |
| let mut payload_bytes = 0usize; |
| while end < windows.len() && end - start < FILTERED_BATCH_RERANK_MAX_RANGES { |
| let next_bytes = payload_bytes.saturating_add(windows[end].length); |
| if end != start && next_bytes > FILTERED_BATCH_RERANK_MAX_BYTES { |
| break; |
| } |
| payload_bytes = next_bytes; |
| end += 1; |
| } |
| chunks.push(start..end); |
| start = end; |
| } |
| chunks |
| } |
| |
| fn use_row_id_order(filter_cardinality: u64, vector_count: usize) -> bool { |
| filter_cardinality != 0 && filter_cardinality <= (vector_count / 16) as u64 |
| } |
| |
| fn matching_nodes_from_sequential_row_ids( |
| filter: &RoaringTreemap, |
| visit_row_ids: impl FnOnce(&mut dyn FnMut(usize, i64) -> io::Result<()>) -> io::Result<()>, |
| ) -> io::Result<RoaringBitmap> { |
| let mut matching = RoaringBitmap::new(); |
| let mut visitor = |node: usize, row_id: i64| { |
| if row_id >= 0 && filter.contains(row_id as u64) { |
| let node = u32::try_from(node) |
| .map_err(|_| invalid_data("DiskANN internal node ID exceeds u32"))?; |
| matching.insert(node); |
| } |
| Ok(()) |
| }; |
| visit_row_ids(&mut visitor)?; |
| Ok(matching) |
| } |
| |
| fn matching_ranges_from_row_id_order( |
| order: &[u32], |
| filter: &RoaringTreemap, |
| mut row_id_at: impl FnMut(usize) -> io::Result<i64>, |
| ) -> io::Result<Vec<std::ops::Range<usize>>> { |
| let mut matches = Vec::new(); |
| for row_id in filter.iter() { |
| let Ok(row_id) = i64::try_from(row_id) else { |
| continue; |
| }; |
| let start = |
| row_id_order_partition_point(order, |node| Ok(row_id_at(node as usize)? < row_id))?; |
| let end = |
| row_id_order_partition_point(order, |node| Ok(row_id_at(node as usize)? <= row_id))?; |
| if start != end { |
| matches.push(start..end); |
| } |
| } |
| Ok(matches) |
| } |
| |
| fn row_id_order_partition_point( |
| order: &[u32], |
| mut predicate: impl FnMut(u32) -> io::Result<bool>, |
| ) -> io::Result<usize> { |
| let mut left = 0usize; |
| let mut right = order.len(); |
| while left < right { |
| let middle = left + (right - left) / 2; |
| if predicate(order[middle])? { |
| left = middle + 1; |
| } else { |
| right = middle; |
| } |
| } |
| Ok(left) |
| } |
| |
| fn push_bounded_candidate( |
| candidates: &mut BinaryHeap<SearchCandidate>, |
| candidate: SearchCandidate, |
| limit: usize, |
| ) -> io::Result<()> { |
| if limit == 0 { |
| return Ok(()); |
| } |
| if candidates.len() < limit { |
| candidates |
| .try_reserve(1) |
| .map_err(|_| invalid_input("DiskANN filtered candidate allocation failed"))?; |
| candidates.push(candidate); |
| return Ok(()); |
| } |
| if candidates |
| .peek() |
| .is_some_and(|worst| candidate.cmp(worst) == Ordering::Less) |
| { |
| candidates.pop(); |
| candidates.push(candidate); |
| } |
| Ok(()) |
| } |
| |
| #[allow(clippy::too_many_arguments)] |
| fn score_filtered_candidate_chunk( |
| distance_table: &[f32], |
| pq_codes: &[u8], |
| pq_m: usize, |
| pq_bits: usize, |
| nodes: &[u32], |
| scored: &mut Vec<SearchCandidate>, |
| candidates: &mut BinaryHeap<SearchCandidate>, |
| candidate_limit: usize, |
| ) -> io::Result<()> { |
| score_pq_neighbors(distance_table, pq_codes, pq_m, pq_bits, nodes, scored)?; |
| for candidate in scored.iter().copied() { |
| push_bounded_candidate(candidates, candidate, candidate_limit)?; |
| } |
| Ok(()) |
| } |
| |
| fn push_bounded_exact_result( |
| results: &mut BinaryHeap<ExactSearchResult>, |
| result: ExactSearchResult, |
| limit: usize, |
| ) -> io::Result<()> { |
| if limit == 0 { |
| return Ok(()); |
| } |
| if results.len() < limit { |
| results |
| .try_reserve(1) |
| .map_err(|_| invalid_input("DiskANN exact result allocation failed"))?; |
| results.push(result); |
| } else if results.peek().is_some_and(|worst| result < *worst) { |
| results.pop(); |
| results.push(result); |
| } |
| Ok(()) |
| } |
| |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| enum RawVectorDistanceKernel { |
| Scalar, |
| #[cfg(all(target_endian = "little", target_arch = "x86_64"))] |
| Avx2, |
| #[cfg(all(target_endian = "little", target_arch = "aarch64"))] |
| Neon, |
| } |
| |
| fn selected_raw_vector_distance_kernel(dimension: usize) -> RawVectorDistanceKernel { |
| #[cfg(all(target_endian = "little", target_arch = "x86_64"))] |
| if dimension >= 8 && is_x86_feature_detected!("avx2") { |
| return RawVectorDistanceKernel::Avx2; |
| } |
| #[cfg(all(target_endian = "little", target_arch = "aarch64"))] |
| if dimension >= 4 { |
| return RawVectorDistanceKernel::Neon; |
| } |
| RawVectorDistanceKernel::Scalar |
| } |
| |
| #[cfg(test)] |
| fn l2_distance_from_le_bytes(query: &[f32], bytes: &[u8]) -> io::Result<f32> { |
| l2_distance_from_le_bytes_with_kernel( |
| query, |
| bytes, |
| selected_raw_vector_distance_kernel(query.len()), |
| ) |
| } |
| |
| fn raw_vector_distance( |
| query: &[f32], |
| bytes: &[u8], |
| encoding: DiskAnnRawVectorEncoding, |
| metric: MetricType, |
| f32_kernel: RawVectorDistanceKernel, |
| ) -> io::Result<f32> { |
| match encoding { |
| DiskAnnRawVectorEncoding::F32 if metric == MetricType::L2 => { |
| l2_distance_from_le_bytes_with_kernel(query, bytes, f32_kernel) |
| } |
| DiskAnnRawVectorEncoding::F32 => metric_distance_from_f32_le_bytes(query, bytes, metric), |
| DiskAnnRawVectorEncoding::F16 if metric == MetricType::L2 => { |
| l2_distance_from_f16_le_bytes(query, bytes) |
| } |
| DiskAnnRawVectorEncoding::F16 => metric_distance_from_f16_le_bytes(query, bytes, metric), |
| } |
| } |
| |
| fn metric_distance_from_f32_le_bytes( |
| query: &[f32], |
| bytes: &[u8], |
| metric: MetricType, |
| ) -> io::Result<f32> { |
| let expected_len = query |
| .len() |
| .checked_mul(size_of::<f32>()) |
| .ok_or_else(|| invalid_data("DiskANN raw vector size overflows"))?; |
| if bytes.len() != expected_len { |
| return Err(invalid_data("DiskANN raw vector record has invalid length")); |
| } |
| let mut dot = 0.0f32; |
| let mut query_norm = 0.0f32; |
| let mut vector_norm = 0.0f32; |
| for (&query_value, component) in query.iter().zip(bytes.chunks_exact(size_of::<f32>())) { |
| let value = f32::from_le_bytes( |
| component |
| .try_into() |
| .expect("validated four-byte raw vector component"), |
| ); |
| if !value.is_finite() { |
| return Err(invalid_data("DiskANN raw vectors must be finite")); |
| } |
| dot += query_value * value; |
| if metric == MetricType::Cosine { |
| query_norm += query_value * query_value; |
| vector_norm += value * value; |
| } |
| } |
| Ok(match metric { |
| MetricType::InnerProduct => -dot, |
| MetricType::Cosine if query_norm > 0.0 && vector_norm > 0.0 => { |
| 1.0 - dot / (query_norm * vector_norm).sqrt() |
| } |
| MetricType::Cosine => 1.0, |
| MetricType::L2 => unreachable!("L2 uses the selected raw-vector kernel"), |
| }) |
| } |
| |
| fn metric_distance_from_f16_le_bytes( |
| query: &[f32], |
| bytes: &[u8], |
| metric: MetricType, |
| ) -> io::Result<f32> { |
| let expected_len = query |
| .len() |
| .checked_mul(size_of::<u16>()) |
| .ok_or_else(|| invalid_data("DiskANN f16 raw-vector size overflows"))?; |
| if bytes.len() != expected_len { |
| return Err(invalid_data( |
| "DiskANN f16 raw-vector record has invalid length", |
| )); |
| } |
| if query.len() > 1024 { |
| return Err(invalid_data( |
| "DiskANN f16 raw-vector dimension exceeds the v1 limit", |
| )); |
| } |
| let mut decoded = [0.0f32; 1024]; |
| for (slot, component) in decoded[..query.len()] |
| .iter_mut() |
| .zip(bytes.chunks_exact(size_of::<u16>())) |
| { |
| let value = half::f16::from_bits(u16::from_le_bytes( |
| component |
| .try_into() |
| .expect("validated two-byte raw-vector component"), |
| )); |
| if !value.is_finite() { |
| return Err(invalid_data("DiskANN raw vectors must be finite")); |
| } |
| *slot = value.to_f32(); |
| } |
| Ok(fvec_distance(query, &decoded[..query.len()], metric)) |
| } |
| |
| fn l2_distance_from_f16_le_bytes(query: &[f32], bytes: &[u8]) -> io::Result<f32> { |
| let expected_len = query |
| .len() |
| .checked_mul(size_of::<u16>()) |
| .ok_or_else(|| invalid_data("DiskANN f16 raw-vector size overflows"))?; |
| if bytes.len() != expected_len { |
| return Err(invalid_data( |
| "DiskANN f16 raw-vector record has invalid length", |
| )); |
| } |
| if query.len() > 1024 { |
| return Err(invalid_data( |
| "DiskANN f16 raw-vector dimension exceeds the v1 limit", |
| )); |
| } |
| #[cfg(all(target_endian = "little", target_arch = "aarch64"))] |
| if query.len() >= 4 { |
| // SAFETY: AArch64 guarantees NEON. The kernel uses unaligned loads, |
| // validates every binary16 exponent, and stays inside the checked |
| // query/record length. |
| return unsafe { l2_distance_from_f16_le_bytes_neon(query, bytes) }; |
| } |
| let mut bits = [0u16; 1024]; |
| for (slot, component) in bits[..query.len()] |
| .iter_mut() |
| .zip(bytes.chunks_exact(size_of::<u16>())) |
| { |
| *slot = u16::from_le_bytes( |
| component |
| .try_into() |
| .expect("validated two-byte raw-vector component"), |
| ); |
| } |
| let values = bits[..query.len()].reinterpret_cast::<half::f16>(); |
| if values.iter().any(|value| !value.is_finite()) { |
| return Err(invalid_data("DiskANN raw vectors must be finite")); |
| } |
| let mut decoded = [0.0f32; 1024]; |
| values.convert_to_f32_slice(&mut decoded[..query.len()]); |
| Ok(fvec_l2sqr(query, &decoded[..query.len()])) |
| } |
| |
| #[cfg(all(target_endian = "little", target_arch = "aarch64"))] |
| #[target_feature(enable = "neon")] |
| unsafe fn l2_distance_from_f16_le_bytes_neon(query: &[f32], bytes: &[u8]) -> io::Result<f32> { |
| use std::arch::aarch64::*; |
| |
| let exponent_mask = vdup_n_u16(0x7c00); |
| let mut invalid = vdup_n_u16(0); |
| let mut sum = vdupq_n_f32(0.0); |
| let mut index = 0usize; |
| while index + 4 <= query.len() { |
| let bits = unsafe { vld1_u16(bytes.as_ptr().add(index * size_of::<u16>()).cast::<u16>()) }; |
| invalid = vorr_u16( |
| invalid, |
| vceq_u16(vand_u16(bits, exponent_mask), exponent_mask), |
| ); |
| let values = vcvt_f32_f16(vreinterpret_f16_u16(bits)); |
| let query_values = unsafe { vld1q_f32(query.as_ptr().add(index)) }; |
| let delta = vsubq_f32(query_values, values); |
| sum = vmlaq_f32(sum, delta, delta); |
| index += 4; |
| } |
| if vmaxv_u16(invalid) != 0 { |
| return Err(invalid_data("DiskANN raw vectors must be finite")); |
| } |
| |
| let mut distance = vaddvq_f32(sum); |
| while index < query.len() { |
| let start = index * size_of::<u16>(); |
| let value = half::f16::from_bits(u16::from_le_bytes( |
| bytes[start..start + size_of::<u16>()] |
| .try_into() |
| .expect("validated two-byte raw-vector component"), |
| )); |
| if !value.is_finite() { |
| return Err(invalid_data("DiskANN raw vectors must be finite")); |
| } |
| let delta = query[index] - value.to_f32(); |
| distance += delta * delta; |
| index += 1; |
| } |
| Ok(distance) |
| } |
| |
| fn l2_distance_from_le_bytes_with_kernel( |
| query: &[f32], |
| bytes: &[u8], |
| kernel: RawVectorDistanceKernel, |
| ) -> io::Result<f32> { |
| let expected_len = query |
| .len() |
| .checked_mul(size_of::<f32>()) |
| .ok_or_else(|| invalid_data("DiskANN raw vector size overflows"))?; |
| if bytes.len() != expected_len { |
| return Err(invalid_data("DiskANN raw vector record has invalid length")); |
| } |
| match kernel { |
| RawVectorDistanceKernel::Scalar => l2_distance_from_le_bytes_scalar(query, bytes), |
| #[cfg(all(target_endian = "little", target_arch = "x86_64"))] |
| RawVectorDistanceKernel::Avx2 => unsafe { l2_distance_from_le_bytes_avx2(query, bytes) }, |
| #[cfg(all(target_endian = "little", target_arch = "aarch64"))] |
| RawVectorDistanceKernel::Neon => unsafe { l2_distance_from_le_bytes_neon(query, bytes) }, |
| } |
| } |
| |
| fn l2_distance_from_le_bytes_scalar(query: &[f32], bytes: &[u8]) -> io::Result<f32> { |
| let mut distance = 0.0f32; |
| for (&query_value, component) in query.iter().zip(bytes.chunks_exact(4)) { |
| let value = f32::from_le_bytes( |
| component |
| .try_into() |
| .expect("validated four-byte raw vector component"), |
| ); |
| if !value.is_finite() { |
| return Err(invalid_data("DiskANN raw vectors must be finite")); |
| } |
| let delta = query_value - value; |
| distance += delta * delta; |
| } |
| Ok(distance) |
| } |
| |
| #[cfg(all(target_endian = "little", target_arch = "x86_64"))] |
| #[target_feature(enable = "avx2")] |
| unsafe fn l2_distance_from_le_bytes_avx2(query: &[f32], bytes: &[u8]) -> io::Result<f32> { |
| use std::arch::x86_64::*; |
| |
| let exponent_mask = _mm256_set1_epi32(0x7f80_0000u32 as i32); |
| let mut invalid = _mm256_setzero_si256(); |
| let mut sum = _mm256_setzero_ps(); |
| let mut index = 0usize; |
| while index + 8 <= query.len() { |
| let bits = unsafe { |
| _mm256_loadu_si256( |
| bytes |
| .as_ptr() |
| .add(index * size_of::<f32>()) |
| .cast::<__m256i>(), |
| ) |
| }; |
| let values = _mm256_castsi256_ps(bits); |
| let query_values = unsafe { _mm256_loadu_ps(query.as_ptr().add(index)) }; |
| invalid = _mm256_or_si256( |
| invalid, |
| _mm256_cmpeq_epi32(_mm256_and_si256(bits, exponent_mask), exponent_mask), |
| ); |
| let delta = _mm256_sub_ps(query_values, values); |
| sum = _mm256_add_ps(sum, _mm256_mul_ps(delta, delta)); |
| index += 8; |
| } |
| if _mm256_movemask_ps(_mm256_castsi256_ps(invalid)) != 0 { |
| return Err(invalid_data("DiskANN raw vectors must be finite")); |
| } |
| |
| let hi = _mm256_extractf128_ps::<1>(sum); |
| let lo = _mm256_castps256_ps128(sum); |
| let sum128 = _mm_add_ps(lo, hi); |
| let sum64 = _mm_add_ps(sum128, _mm_movehl_ps(sum128, sum128)); |
| let sum32 = _mm_add_ss(sum64, _mm_shuffle_ps::<1>(sum64, sum64)); |
| let mut distance = _mm_cvtss_f32(sum32); |
| while index < query.len() { |
| let start = index * size_of::<f32>(); |
| let value = f32::from_le_bytes( |
| bytes[start..start + size_of::<f32>()] |
| .try_into() |
| .expect("validated four-byte raw vector component"), |
| ); |
| if !value.is_finite() { |
| return Err(invalid_data("DiskANN raw vectors must be finite")); |
| } |
| let delta = query[index] - value; |
| distance += delta * delta; |
| index += 1; |
| } |
| Ok(distance) |
| } |
| |
| #[cfg(all(target_endian = "little", target_arch = "aarch64"))] |
| #[target_feature(enable = "neon")] |
| unsafe fn l2_distance_from_le_bytes_neon(query: &[f32], bytes: &[u8]) -> io::Result<f32> { |
| use std::arch::aarch64::*; |
| |
| let exponent_mask = vdupq_n_u32(0x7f80_0000); |
| let mut invalid = vdupq_n_u32(0); |
| let mut sum = vdupq_n_f32(0.0); |
| let mut index = 0usize; |
| while index + 4 <= query.len() { |
| let bits = unsafe { vld1q_u32(bytes.as_ptr().add(index * size_of::<f32>()).cast::<u32>()) }; |
| let values = vreinterpretq_f32_u32(bits); |
| let query_values = unsafe { vld1q_f32(query.as_ptr().add(index)) }; |
| invalid = vorrq_u32( |
| invalid, |
| vceqq_u32(vandq_u32(bits, exponent_mask), exponent_mask), |
| ); |
| let delta = vsubq_f32(query_values, values); |
| sum = vmlaq_f32(sum, delta, delta); |
| index += 4; |
| } |
| if vmaxvq_u32(invalid) != 0 { |
| return Err(invalid_data("DiskANN raw vectors must be finite")); |
| } |
| |
| let mut distance = vaddvq_f32(sum); |
| while index < query.len() { |
| let start = index * size_of::<f32>(); |
| let value = f32::from_le_bytes( |
| bytes[start..start + size_of::<f32>()] |
| .try_into() |
| .expect("validated four-byte raw vector component"), |
| ); |
| if !value.is_finite() { |
| return Err(invalid_data("DiskANN raw vectors must be finite")); |
| } |
| let delta = query[index] - value; |
| distance += delta * delta; |
| index += 1; |
| } |
| Ok(distance) |
| } |
| |
| fn invalid_data(message: impl Into<String>) -> io::Error { |
| io::Error::new(io::ErrorKind::InvalidData, message.into()) |
| } |
| |
| fn invalid_input(message: impl Into<String>) -> io::Error { |
| io::Error::new(io::ErrorKind::InvalidInput, message.into()) |
| } |
| |
| fn score_pq_neighbors( |
| distance_table: &[f32], |
| pq_codes: &[u8], |
| pq_m: usize, |
| pq_bits: usize, |
| nodes: &[u32], |
| scored: &mut Vec<SearchCandidate>, |
| ) -> io::Result<usize> { |
| if pq_m == 0 { |
| return Err(invalid_data("DiskANN PQ subspace count must be positive")); |
| } |
| let (code_size, ksub) = match pq_bits { |
| 4 => (pq_m.div_ceil(2), 16), |
| 8 => (pq_m, 256), |
| _ => return Err(invalid_data("DiskANN PQ bits must be 4 or 8")), |
| }; |
| let table_len = pq_m |
| .checked_mul(ksub) |
| .ok_or_else(|| invalid_data("DiskANN PQ distance-table length overflows"))?; |
| if distance_table.len() < table_len { |
| return Err(invalid_data("DiskANN PQ distance table is truncated")); |
| } |
| |
| scored.clear(); |
| if scored.capacity() < nodes.len() { |
| scored.reserve(nodes.len()); |
| } |
| let mut node_index = 0; |
| let mut four_code_batches = 0; |
| let prefetch_lookahead = pq_prefetch_lookahead(code_size); |
| while node_index + 4 <= nodes.len() { |
| for &future_node in nodes |
| .iter() |
| .skip(node_index.saturating_add(prefetch_lookahead)) |
| .take(4) |
| { |
| prefetch_pq_code(pq_codes, future_node as usize, code_size); |
| } |
| let batch_nodes = [ |
| nodes[node_index] as usize, |
| nodes[node_index + 1] as usize, |
| nodes[node_index + 2] as usize, |
| nodes[node_index + 3] as usize, |
| ]; |
| let mut offsets = [0; 4]; |
| for index in 0..4 { |
| offsets[index] = batch_nodes[index] |
| .checked_mul(code_size) |
| .ok_or_else(|| invalid_data("DiskANN PQ code offset overflows"))?; |
| let end = offsets[index] |
| .checked_add(code_size) |
| .ok_or_else(|| invalid_data("DiskANN PQ code range overflows"))?; |
| if end > pq_codes.len() { |
| return Err(invalid_data("DiskANN PQ codes are truncated")); |
| } |
| } |
| let distances = if pq_bits == 4 { |
| pq_distance_four_packed_4bit(distance_table, pq_codes, pq_m, offsets) |
| } else { |
| pq_distance_four_codes(distance_table, pq_codes, pq_m, ksub, offsets) |
| }; |
| for index in 0..4 { |
| scored.push(SearchCandidate { |
| node: batch_nodes[index], |
| distance: distances[index], |
| }); |
| } |
| node_index += 4; |
| four_code_batches += 1; |
| } |
| |
| for node in &nodes[node_index..] { |
| let node = *node as usize; |
| let start = node |
| .checked_mul(code_size) |
| .ok_or_else(|| invalid_data("DiskANN PQ code offset overflows"))?; |
| let end = start |
| .checked_add(code_size) |
| .ok_or_else(|| invalid_data("DiskANN PQ code range overflows"))?; |
| let codes = pq_codes |
| .get(start..end) |
| .ok_or_else(|| invalid_data("DiskANN PQ codes are truncated"))?; |
| scored.push(SearchCandidate { |
| node, |
| distance: if pq_bits == 4 { |
| pq_distance_packed_4bit(distance_table, codes, pq_m) |
| } else { |
| pq_distance_from_table(distance_table, codes, pq_m, ksub) |
| }, |
| }); |
| } |
| Ok(four_code_batches) |
| } |
| |
| #[inline] |
| fn pq_prefetch_lookahead(code_size: usize) -> usize { |
| // Aim for roughly 256 bytes of independent PQ-code work between the hint |
| // and its use, while bounding both tiny-code overhead and large-code delay. |
| (256 / code_size.max(1)).clamp(4, 16) |
| } |
| |
| #[inline] |
| fn prefetch_pq_code(pq_codes: &[u8], node: usize, code_size: usize) { |
| let Some(offset) = node.checked_mul(code_size) else { |
| return; |
| }; |
| let Some(byte) = pq_codes.get(offset) else { |
| return; |
| }; |
| let pointer = byte as *const u8; |
| #[cfg(target_arch = "x86_64")] |
| // SAFETY: `pointer` comes from an in-bounds slice element and the intrinsic |
| // only issues a non-faulting read hint. |
| unsafe { |
| std::arch::x86_64::_mm_prefetch(pointer.cast::<i8>(), std::arch::x86_64::_MM_HINT_T0); |
| } |
| #[cfg(target_arch = "aarch64")] |
| // SAFETY: `pointer` comes from an in-bounds slice element. `prfm` is a |
| // non-faulting cache hint and neither dereferences nor mutates Rust memory. |
| unsafe { |
| std::arch::asm!( |
| "prfm pldl1keep, [{address}]", |
| address = in(reg) pointer, |
| options(readonly, nostack, preserves_flags) |
| ); |
| } |
| #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] |
| let _ = pointer; |
| } |
| |
| #[inline] |
| fn pq_distance_packed_4bit(distance_table: &[f32], codes: &[u8], pq_m: usize) -> f32 { |
| let mut distance = 0.0f32; |
| for sub in 0..pq_m { |
| let byte = codes[sub / 2]; |
| let code = if sub.is_multiple_of(2) { |
| byte & 0x0f |
| } else { |
| byte >> 4 |
| }; |
| distance += distance_table[sub * 16 + code as usize]; |
| } |
| distance |
| } |
| |
| #[inline] |
| fn pq_distance_four_packed_4bit( |
| distance_table: &[f32], |
| codes: &[u8], |
| pq_m: usize, |
| offsets: [usize; 4], |
| ) -> [f32; 4] { |
| let mut distances = [0.0f32; 4]; |
| for sub in 0..pq_m { |
| let table_start = sub * 16; |
| for vector in 0..4 { |
| let byte = codes[offsets[vector] + sub / 2]; |
| let code = if sub.is_multiple_of(2) { |
| byte & 0x0f |
| } else { |
| byte >> 4 |
| }; |
| distances[vector] += distance_table[table_start + code as usize]; |
| } |
| } |
| distances |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use crate::diskann::{ |
| DiskAnnBuildParams, DiskAnnIndex, DiskAnnRawVectorEncoding, DiskAnnStorageLayout, |
| }; |
| use crate::diskann_io::{write_diskann_index, DiskAnnIndexReader}; |
| use crate::distance::MetricType; |
| use crate::index::VectorIndexReaderOptions; |
| use crate::io::{PosWriter, ReadRequest, SeekRead, SeekReadCapabilities}; |
| use crate::read_options::DeploymentProfile; |
| use roaring::RoaringTreemap; |
| use std::io::Cursor; |
| use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering as AtomicOrdering}; |
| use std::sync::{Arc, Mutex}; |
| |
| type ReadRounds = Arc<Mutex<Vec<Vec<(u64, usize)>>>>; |
| |
| #[test] |
| fn diskann_compact_vector_windows_pack_complete_records_without_page_padding() { |
| let section = SectionRange::new(8192, 7 * 3840); |
| let local = |
| VectorWindowPlanner::new(DeploymentProfile::LocalStorage.read_plan(), section, 3840) |
| .unwrap(); |
| assert_eq!( |
| local.plan_nodes([6, 1, 0, 6]), |
| vec![ |
| ReadWindow::new(8192, 4 * 3840), |
| ReadWindow::new(8192 + 4 * 3840, 3 * 3840), |
| ] |
| ); |
| |
| let remote = |
| VectorWindowPlanner::new(DeploymentProfile::RemoteStorage.read_plan(), section, 3840) |
| .unwrap(); |
| assert_eq!( |
| remote.plan_nodes([6, 0, 6]), |
| vec![ReadWindow::new(8192, 26880)] |
| ); |
| assert_eq!( |
| remote.window_for_node(6), |
| Some(ReadWindow::new(8192, 26880)) |
| ); |
| assert_eq!(remote.window_for_node(7), None); |
| } |
| |
| #[test] |
| fn diskann_f16_raw_vector_distance_decodes_little_endian_components() { |
| let vector = [1.25f32, -2.5, 4.0]; |
| let bytes = vector |
| .iter() |
| .flat_map(|&value| half::f16::from_f32(value).to_bits().to_le_bytes()) |
| .collect::<Vec<_>>(); |
| let distance = raw_vector_distance( |
| &[1.0, -2.0, 5.0], |
| &bytes, |
| DiskAnnRawVectorEncoding::F16, |
| MetricType::L2, |
| RawVectorDistanceKernel::Scalar, |
| ) |
| .unwrap(); |
| assert!((distance - 1.3125).abs() < 1e-6); |
| } |
| |
| #[test] |
| fn diskann_raw_vector_distance_preserves_ip_and_cosine_score_semantics() { |
| let vector = [3.0f32, 4.0]; |
| let f32_bytes = vector |
| .iter() |
| .flat_map(|value| value.to_le_bytes()) |
| .collect::<Vec<_>>(); |
| let f16_bytes = vector |
| .iter() |
| .flat_map(|&value| half::f16::from_f32(value).to_bits().to_le_bytes()) |
| .collect::<Vec<_>>(); |
| |
| for (bytes, encoding) in [ |
| (f32_bytes.as_slice(), DiskAnnRawVectorEncoding::F32), |
| (f16_bytes.as_slice(), DiskAnnRawVectorEncoding::F16), |
| ] { |
| assert_eq!( |
| raw_vector_distance( |
| &[1.0, -2.0], |
| bytes, |
| encoding, |
| MetricType::InnerProduct, |
| RawVectorDistanceKernel::Scalar, |
| ) |
| .unwrap(), |
| 5.0 |
| ); |
| assert!( |
| (raw_vector_distance( |
| &[1.0, 0.0], |
| bytes, |
| encoding, |
| MetricType::Cosine, |
| RawVectorDistanceKernel::Scalar, |
| ) |
| .unwrap() |
| - 0.4) |
| .abs() |
| < 1e-6 |
| ); |
| } |
| } |
| |
| #[test] |
| fn diskann_f16_raw_vector_distance_handles_unaligned_wide_records() { |
| let query = (0..65).map(|index| index as f32 * 0.25).collect::<Vec<_>>(); |
| let vector = (0..65) |
| .map(|index| index as f32 * -0.5 + 3.0) |
| .collect::<Vec<_>>(); |
| let mut storage = vec![0x7f]; |
| storage.extend( |
| vector |
| .iter() |
| .flat_map(|&value| half::f16::from_f32(value).to_bits().to_le_bytes()), |
| ); |
| let bytes = &storage[1..]; |
| let expected = query |
| .iter() |
| .zip(&vector) |
| .map(|(left, right)| { |
| let decoded = half::f16::from_f32(*right).to_f32(); |
| let delta = left - decoded; |
| delta * delta |
| }) |
| .sum::<f32>(); |
| |
| let distance = l2_distance_from_f16_le_bytes(&query, bytes).unwrap(); |
| |
| assert!((distance - expected).abs() <= expected.abs() * 1.0e-5); |
| let mut non_finite = bytes.to_vec(); |
| let non_finite_offset = size_of::<[u16; 8]>(); |
| non_finite[non_finite_offset..non_finite_offset + size_of::<u16>()] |
| .copy_from_slice(&half::f16::INFINITY.to_bits().to_le_bytes()); |
| assert_eq!( |
| l2_distance_from_f16_le_bytes(&query, &non_finite) |
| .expect_err("SIMD F16 distance must reject infinity") |
| .kind(), |
| io::ErrorKind::InvalidData |
| ); |
| } |
| |
| #[test] |
| fn diskann_compact_f16_roundtrips_dense_vector_records() { |
| let dimension = 8; |
| let count = 64; |
| let data = (0..count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| raw_vector_encoding: DiskAnnRawVectorEncoding::F16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, count).unwrap(); |
| index.add(&data, &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let header = crate::diskann_io::DiskAnnHeader::decode(&bytes[..256]).unwrap(); |
| assert_eq!(header.raw_vector_encoding(), DiskAnnRawVectorEncoding::F16); |
| assert_eq!(header.vector_record_size, (dimension * 2) as u32); |
| assert_eq!( |
| header.sections.vectors.length, |
| (count * dimension * 2) as u64 |
| ); |
| |
| let mut reader = DiskAnnIndexReader::open(Cursor::new(bytes)).unwrap(); |
| let (result_ids, distances) = reader.search(&data[..dimension], 1, 100).unwrap(); |
| assert_eq!(result_ids, vec![ids[0]]); |
| assert_eq!(distances, vec![0.0]); |
| } |
| |
| #[test] |
| fn diskann_interleaved_layout_searches_and_batches_without_a_vector_section() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| storage_layout: DiskAnnStorageLayout::Interleaved, |
| raw_vector_encoding: DiskAnnRawVectorEncoding::F16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let header = crate::diskann_io::DiskAnnHeader::decode(&bytes[..256]).unwrap(); |
| assert!(header.is_interleaved()); |
| assert_eq!(header.raw_vector_encoding(), DiskAnnRawVectorEncoding::F16); |
| assert_eq!(header.sections.vectors.length, 0); |
| assert_eq!(header.file_len, header.sections.vectors.offset); |
| |
| let queries = &data[..dimension * 4]; |
| let mut batch_reader = DiskAnnIndexReader::open(CloneCountingReader { |
| bytes: Arc::from(bytes.clone()), |
| clone_count: Arc::new(AtomicUsize::new(0)), |
| reads: Arc::new(Mutex::new(Vec::new())), |
| }) |
| .unwrap(); |
| let batch = rayon::ThreadPoolBuilder::new() |
| .num_threads(4) |
| .build() |
| .unwrap() |
| .install(|| batch_reader.search_batch(queries, 5, 100)) |
| .unwrap(); |
| assert_eq!(batch_reader.last_search_stats().parallel_session_queries, 4); |
| |
| let mut single_reader = DiskAnnIndexReader::open(Cursor::new(bytes.clone())).unwrap(); |
| let mut expected_ids = Vec::new(); |
| let mut expected_distances = Vec::new(); |
| for query in queries.chunks_exact(dimension) { |
| let (query_ids, query_distances) = single_reader.search(query, 5, 100).unwrap(); |
| expected_ids.extend(query_ids); |
| expected_distances.extend(query_distances); |
| } |
| assert_eq!(batch.0, expected_ids); |
| assert_eq!(batch.1, expected_distances); |
| assert_eq!(batch.0[0], ids[0]); |
| |
| let mut filter = RoaringTreemap::new(); |
| filter.extend([ids[0] as u64, ids[1] as u64]); |
| let mut filter_bytes = Vec::new(); |
| filter.serialize_into(&mut filter_bytes).unwrap(); |
| let filtered_batch = rayon::ThreadPoolBuilder::new() |
| .num_threads(4) |
| .build() |
| .unwrap() |
| .install(|| { |
| batch_reader.search_batch_with_roaring_filter(queries, 3, 100, &filter_bytes) |
| }) |
| .unwrap(); |
| let mut filtered_single = DiskAnnIndexReader::open(Cursor::new(bytes)).unwrap(); |
| let mut filtered_ids = Vec::new(); |
| let mut filtered_distances = Vec::new(); |
| for query in queries.chunks_exact(dimension) { |
| let (query_ids, query_distances) = filtered_single |
| .search_with_roaring_filter(query, 3, 100, &filter_bytes) |
| .unwrap(); |
| filtered_ids.extend(query_ids); |
| filtered_distances.extend(query_distances); |
| } |
| assert_eq!(filtered_batch, (filtered_ids, filtered_distances)); |
| } |
| |
| #[test] |
| fn diskann_pq_neighbor_scoring_batches_four_codes_and_matches_scalar_distance() { |
| let pq_m = 4; |
| let distance_table = (0..pq_m * 256) |
| .map(|index| index as f32 * 0.25) |
| .collect::<Vec<_>>(); |
| let pq_codes = vec![ |
| 1, 2, 3, 4, // |
| 5, 6, 7, 8, // |
| 9, 10, 11, 12, // |
| 13, 14, 15, 16, // |
| 17, 18, 19, 20, |
| ]; |
| let nodes = [4_u32, 1, 3, 0, 2]; |
| let mut scored = Vec::new(); |
| |
| let four_code_batches = |
| score_pq_neighbors(&distance_table, &pq_codes, pq_m, 8, &nodes, &mut scored).unwrap(); |
| |
| assert_eq!(four_code_batches, 1); |
| assert_eq!( |
| scored |
| .iter() |
| .map(|candidate| candidate.node) |
| .collect::<Vec<_>>(), |
| nodes.iter().map(|node| *node as usize).collect::<Vec<_>>() |
| ); |
| for (candidate, node) in scored.iter().zip(nodes) { |
| let start = node as usize * pq_m; |
| let expected = |
| pq_distance_from_table(&distance_table, &pq_codes[start..start + pq_m], pq_m, 256); |
| assert_eq!(candidate.distance, expected); |
| } |
| |
| assert!(score_pq_neighbors( |
| &distance_table, |
| &pq_codes[..pq_codes.len() - 1], |
| pq_m, |
| 8, |
| &nodes, |
| &mut scored, |
| ) |
| .is_err()); |
| } |
| |
| #[test] |
| fn diskann_packed_4bit_neighbor_scoring_uses_vector_code_size() { |
| let pq_m = 4; |
| let pq_bits = 4; |
| let distance_table = (0..pq_m * 16) |
| .map(|index| index as f32 * 0.25) |
| .collect::<Vec<_>>(); |
| let pq_codes = vec![ |
| 0x10, 0x32, // node 0: [0, 1, 2, 3] |
| 0x54, 0x76, // node 1: [4, 5, 6, 7] |
| 0x98, 0xBA, // node 2: [8, 9, 10, 11] |
| 0xDC, 0xFE, // node 3: [12, 13, 14, 15] |
| 0x21, 0x43, // node 4: [1, 2, 3, 4] |
| ]; |
| let nodes = [4, 1, 3, 2, 0]; |
| let mut scored = Vec::new(); |
| |
| let batches = score_pq_neighbors( |
| &distance_table, |
| &pq_codes, |
| pq_m, |
| pq_bits, |
| &nodes, |
| &mut scored, |
| ) |
| .unwrap(); |
| |
| assert_eq!(batches, 1); |
| for (actual, &node) in scored.iter().zip(&nodes) { |
| let start = node as usize * 2; |
| let expected = [0, 1, 2, 3] |
| .into_iter() |
| .map(|sub| { |
| let byte = pq_codes[start + sub / 2]; |
| let code = if sub.is_multiple_of(2) { |
| byte & 0x0f |
| } else { |
| byte >> 4 |
| }; |
| distance_table[sub * 16 + code as usize] |
| }) |
| .sum::<f32>(); |
| assert_eq!(actual.node, node as usize); |
| assert_eq!(actual.distance, expected); |
| } |
| |
| let error = score_pq_neighbors( |
| &distance_table, |
| &pq_codes[..pq_codes.len() - 1], |
| pq_m, |
| pq_bits, |
| &nodes, |
| &mut scored, |
| ) |
| .expect_err("a packed code truncated after the last low nibble must fail closed"); |
| assert!(error.to_string().contains("truncated")); |
| } |
| |
| #[test] |
| fn diskann_filtered_candidate_selection_stays_bounded_and_deterministic() { |
| let mut candidates = std::collections::BinaryHeap::new(); |
| for node in (0..1000).rev() { |
| push_bounded_candidate( |
| &mut candidates, |
| SearchCandidate { |
| node, |
| distance: (node / 2) as f32, |
| }, |
| 10, |
| ) |
| .unwrap(); |
| assert!(candidates.len() <= 10); |
| } |
| let mut selected = candidates.into_vec(); |
| sort_candidates(&mut selected); |
| |
| assert_eq!( |
| selected |
| .iter() |
| .map(|candidate| candidate.node) |
| .collect::<Vec<_>>(), |
| (0..10).collect::<Vec<_>>() |
| ); |
| } |
| |
| #[test] |
| fn diskann_filtered_candidate_target_is_bounded_by_matches() { |
| assert_eq!(desired_filtered_candidate_count(1_000, 10), 64); |
| assert_eq!(desired_filtered_candidate_count(20, 10), 20); |
| assert_eq!(desired_filtered_candidate_count(1_000, usize::MAX), 1_000); |
| } |
| |
| #[test] |
| fn diskann_filtered_pq_tile_reuses_four_queries_without_exceeding_table_budget() { |
| assert_eq!(filtered_pq_query_tile_size(16, 256), 4); |
| assert_eq!(filtered_pq_query_tile_size(1024, 256), 2); |
| assert_eq!(filtered_pq_query_tile_size(1024, 16), 4); |
| for (pq_m, pq_ksub) in [(16, 256), (1024, 256), (1024, 16)] { |
| let tile_size = filtered_pq_query_tile_size(pq_m, pq_ksub); |
| let table_bytes = tile_size * pq_m * pq_ksub * size_of::<f32>(); |
| assert!(table_bytes <= FILTERED_PQ_TILE_TABLE_LIMIT_BYTES); |
| } |
| } |
| |
| #[test] |
| fn diskann_rerank_expands_candidates_only_within_seed_windows() { |
| let planner = ReadWindowPlanner::new( |
| DeploymentProfile::LocalStorage.read_plan(), |
| SectionRange::new(4096, 4 * DISKANN_PAGE_SIZE as u64), |
| ); |
| let candidates = [ |
| SearchCandidate { |
| node: 1, |
| distance: 1.0, |
| }, |
| SearchCandidate { |
| node: 16, |
| distance: 2.0, |
| }, |
| SearchCandidate { |
| node: 2, |
| distance: 3.0, |
| }, |
| SearchCandidate { |
| node: 31, |
| distance: 4.0, |
| }, |
| SearchCandidate { |
| node: 32, |
| distance: 5.0, |
| }, |
| ]; |
| let mut selected = Vec::new(); |
| let mut selected_windows = HashSet::new(); |
| |
| expand_rerank_candidates_within_seed_windows( |
| &candidates, |
| 2, |
| |node| { |
| planner |
| .window_for_logical_page(node / 16) |
| .map(|window| window.offset) |
| .ok_or_else(|| invalid_data("test vector page is out of range")) |
| }, |
| &mut selected_windows, |
| &mut selected, |
| ) |
| .unwrap(); |
| |
| assert_eq!( |
| selected |
| .iter() |
| .map(|candidate| candidate.node) |
| .collect::<Vec<_>>(), |
| vec![1, 16, 2, 31, 32] |
| ); |
| } |
| |
| #[test] |
| fn diskann_bounded_exact_heap_matches_full_sort_with_row_id_ties() { |
| let mut results = (0..100) |
| .map(|row_id| ExactSearchResult { |
| row_id, |
| distance: ((row_id * 37) % 11) as f32, |
| }) |
| .collect::<Vec<_>>(); |
| let mut expected = results.clone(); |
| expected.sort(); |
| expected.truncate(7); |
| let mut heap = BinaryHeap::new(); |
| |
| for result in results.drain(..).rev() { |
| push_bounded_exact_result(&mut heap, result, 7).unwrap(); |
| assert!(heap.len() <= 7); |
| } |
| |
| assert_eq!(heap.into_sorted_vec(), expected); |
| } |
| |
| #[test] |
| fn diskann_adaptive_filter_strategy_gates_selectivity_cost_and_access_pattern() { |
| let scan = FilteredCandidateStrategy::Exhaustive { |
| target_candidates: 64, |
| }; |
| assert_eq!( |
| select_filtered_candidate_strategy( |
| 1_000_000, |
| 499_999, |
| 10, |
| 100, |
| 8, |
| DeploymentProfile::LocalStorage.read_plan(), |
| false, |
| ), |
| scan |
| ); |
| assert_eq!( |
| select_filtered_candidate_strategy( |
| 10_000, |
| 10_000, |
| 10, |
| 100, |
| 64, |
| DeploymentProfile::LocalStorage.read_plan(), |
| false, |
| ), |
| scan |
| ); |
| assert_eq!( |
| select_filtered_candidate_strategy( |
| 1_000_000, |
| 1_000_000, |
| 10, |
| 100, |
| 64, |
| DeploymentProfile::LocalStorage.read_plan(), |
| false, |
| ), |
| scan |
| ); |
| |
| let graph = FilteredCandidateStrategy::Graph { |
| target_candidates: 64, |
| search_list_size: 200, |
| }; |
| assert_eq!( |
| select_filtered_candidate_strategy( |
| 1_000_000, |
| 1_000_000, |
| 10, |
| 200, |
| 64, |
| DeploymentProfile::LocalStorage.read_plan(), |
| false, |
| ), |
| graph |
| ); |
| assert_eq!( |
| select_filtered_candidate_strategy( |
| 1_000_000, |
| 1_000_000, |
| 10, |
| 200, |
| 64, |
| DeploymentProfile::ObjectStore.read_plan(), |
| false, |
| ), |
| scan |
| ); |
| assert_eq!( |
| select_filtered_candidate_strategy( |
| 1_000_000, |
| 1_000_000, |
| 10, |
| 200, |
| 64, |
| DeploymentProfile::ObjectStore.read_plan(), |
| true, |
| ), |
| graph |
| ); |
| } |
| |
| #[test] |
| fn diskann_adaptive_filter_strategy_scales_and_caps_search_list_safely() { |
| assert_eq!( |
| adaptive_filtered_search_list_size(1_000_000, 500_000, 64, 100), |
| 256 |
| ); |
| assert_eq!(adaptive_filtered_search_list_size(10, 5, 5, usize::MAX), 10); |
| assert_eq!( |
| adaptive_filtered_search_list_size( |
| usize::MAX, |
| usize::MAX, |
| usize::MAX, |
| resolve_diskann_l_search(usize::MAX, 0), |
| ), |
| usize::MAX |
| ); |
| } |
| |
| #[test] |
| fn diskann_automatic_l_search_scales_with_top_k_and_preserves_explicit_values() { |
| assert_eq!(resolve_diskann_l_search(10, 0), 100); |
| assert_eq!(resolve_diskann_l_search(100, 0), 200); |
| assert_eq!(resolve_diskann_l_search(usize::MAX, 0), usize::MAX); |
| assert_eq!(resolve_diskann_l_search(100, 64), 100); |
| assert_eq!(resolve_diskann_l_search(10, 64), 64); |
| } |
| |
| #[test] |
| fn diskann_graph_candidates_are_post_filtered_and_require_target_count() { |
| let graph = vec![ |
| SearchCandidate { |
| node: 1, |
| distance: 1.0, |
| }, |
| SearchCandidate { |
| node: 2, |
| distance: 2.0, |
| }, |
| SearchCandidate { |
| node: 3, |
| distance: 3.0, |
| }, |
| ]; |
| let matching = RoaringBitmap::from_iter([2, 3]); |
| |
| assert!(post_filter_graph_candidates(&graph, &matching, 3).is_none()); |
| assert_eq!( |
| post_filter_graph_candidates(&graph, &matching, 2) |
| .unwrap() |
| .iter() |
| .map(|candidate| candidate.node) |
| .collect::<Vec<_>>(), |
| vec![2, 3] |
| ); |
| } |
| |
| #[test] |
| fn diskann_filtered_batch_rerank_chunks_bound_bytes_and_ranges() { |
| let range_limited = (0..1025) |
| .map(|index| ReadWindow::new(index * 4096, 4096)) |
| .collect::<Vec<_>>(); |
| assert_eq!( |
| plan_streaming_window_chunks(&range_limited), |
| vec![0..1024, 1024..1025] |
| ); |
| |
| let byte_limited = vec![ |
| ReadWindow::new(0, 40 * 1024 * 1024), |
| ReadWindow::new(40 * 1024 * 1024, 30 * 1024 * 1024), |
| ]; |
| assert_eq!( |
| plan_streaming_window_chunks(&byte_limited), |
| vec![0..1, 1..2] |
| ); |
| } |
| |
| #[test] |
| fn diskann_filtered_lookup_returns_duplicate_rows_and_ignores_oversized_ids() { |
| let row_ids = [7, -1, 7, 3, 9]; |
| let order = vec![1, 3, 0, 2, 4]; |
| let mut filter = RoaringTreemap::new(); |
| filter.insert(7); |
| filter.insert(i64::MAX as u64 + 1); |
| |
| let ranges = |
| matching_ranges_from_row_id_order(&order, &filter, |node| Ok(row_ids[node])).unwrap(); |
| let nodes = ranges |
| .into_iter() |
| .flat_map(|range| order[range].iter().copied()) |
| .collect::<Vec<_>>(); |
| |
| assert_eq!(nodes, vec![0, 2]); |
| assert!(!use_row_id_order(0, 64)); |
| assert!(use_row_id_order(4, 64)); |
| assert!(!use_row_id_order(5, 64)); |
| } |
| |
| #[test] |
| fn diskann_filtered_batch_dense_translation_visits_each_row_id_once() { |
| let row_ids = [-5, 7, 7, 3, 11]; |
| let mut filter = RoaringTreemap::new(); |
| filter.insert(7); |
| filter.insert(i64::MAX as u64 + 1); |
| let mut visits = 0usize; |
| |
| let matching = matching_nodes_from_sequential_row_ids(&filter, |visitor| { |
| for (node, &row_id) in row_ids.iter().enumerate() { |
| visits += 1; |
| visitor(node, row_id)?; |
| } |
| Ok(()) |
| }) |
| .unwrap(); |
| |
| assert_eq!(visits, row_ids.len()); |
| assert_eq!(matching.iter().collect::<Vec<_>>(), vec![1, 2]); |
| } |
| |
| #[test] |
| fn diskann_bounded_heap_frontier_reuses_allocation_and_retains_total_order() { |
| let mut scratch = DiskAnnQueryScratch::default(); |
| scratch.begin_search(1_000); |
| for node in (0..1_000).rev() { |
| scratch |
| .insert_graph_candidate( |
| SearchCandidate { |
| node, |
| distance: (node / 2) as f32, |
| }, |
| 10, |
| ) |
| .unwrap(); |
| assert!(scratch.retained_candidates.len() <= 10); |
| assert!(scratch.frontier.len() <= 20); |
| } |
| let frontier_capacity = scratch.frontier.capacity(); |
| assert!(frontier_capacity <= 40); |
| |
| scratch.finish_graph_candidates(); |
| |
| assert_eq!( |
| scratch |
| .candidates |
| .iter() |
| .map(|candidate| candidate.node) |
| .collect::<Vec<_>>(), |
| (0..10).collect::<Vec<_>>() |
| ); |
| |
| scratch.begin_search(1_000); |
| assert_eq!(scratch.frontier.capacity(), frontier_capacity); |
| } |
| |
| #[test] |
| fn diskann_bounded_frontier_matches_round_by_round_vector_oracle() { |
| let discovered_rounds = (0..12) |
| .map(|round| { |
| (0..9) |
| .map(|slot| { |
| let node = 1 + round * 9 + ((slot * 5 + round) % 9); |
| SearchCandidate { |
| node, |
| distance: ((node * 17) % 23) as f32, |
| } |
| }) |
| .collect::<Vec<_>>() |
| }) |
| .collect::<Vec<_>>(); |
| for (limit, beam_width) in [(8, 1), (16, 4), (32, 16)] { |
| let entry = SearchCandidate { |
| node: 0, |
| distance: 11.0, |
| }; |
| let mut scratch = DiskAnnQueryScratch::default(); |
| scratch.begin_search(256); |
| scratch.insert_graph_candidate(entry, limit).unwrap(); |
| let mut oracle = vec![(entry, false)]; |
| |
| for discovered in &discovered_rounds { |
| scratch.select_round(beam_width); |
| let oracle_selected = oracle |
| .iter_mut() |
| .filter(|(_, expanded)| !*expanded) |
| .take(beam_width) |
| .map(|(candidate, expanded)| { |
| *expanded = true; |
| candidate.node |
| }) |
| .collect::<Vec<_>>(); |
| assert_eq!(scratch.selected_nodes, oracle_selected); |
| if scratch.selected_nodes.is_empty() { |
| break; |
| } |
| for &candidate in discovered { |
| scratch.insert_graph_candidate(candidate, limit).unwrap(); |
| oracle.push((candidate, false)); |
| } |
| oracle.sort_by_key(|entry| entry.0); |
| oracle.truncate(limit); |
| } |
| |
| scratch.finish_graph_candidates(); |
| let oracle_candidates = oracle |
| .into_iter() |
| .map(|(candidate, _)| candidate) |
| .collect::<Vec<_>>(); |
| assert_eq!(scratch.candidates, oracle_candidates); |
| } |
| } |
| |
| #[test] |
| fn diskann_query_scratch_reuses_visited_bitmap_and_clears_touched_nodes() { |
| let mut scratch = DiskAnnQueryScratch::default(); |
| scratch.begin_search(1_000_000); |
| let capacity = scratch.visited_capacity(); |
| |
| assert!(scratch.mark_visited(7)); |
| assert!(!scratch.mark_visited(7)); |
| assert!(scratch.mark_visited(999_999)); |
| |
| scratch.begin_search(1_000_000); |
| |
| assert_eq!(scratch.visited_capacity(), capacity); |
| assert!(!scratch.is_visited(7)); |
| assert!(!scratch.is_visited(999_999)); |
| } |
| |
| #[test] |
| fn diskann_adaptive_visited_switches_between_dense_and_sparse_storage() { |
| let mut scratch = DiskAnnQueryScratch::default(); |
| scratch.begin_graph_search(1024, 100, 8).unwrap(); |
| assert!(!scratch.uses_sparse_visited()); |
| assert!(scratch.mark_visited(7)); |
| assert!(!scratch.mark_visited(7)); |
| |
| scratch.begin_graph_search(1_000_000, 100, 64).unwrap(); |
| assert!(!scratch.uses_sparse_visited()); |
| assert!(scratch.mark_visited(999_999)); |
| assert!(!scratch.mark_visited(999_999)); |
| |
| scratch.begin_graph_search(100_000_000, 100, 64).unwrap(); |
| assert!(scratch.uses_sparse_visited()); |
| assert!( |
| std::any::type_name_of_val(&scratch.sparse_visited).contains("SparseTable"), |
| "DiskANN query hot-path sparse visited state must use the internal table" |
| ); |
| assert!(scratch.mark_visited(99_999_999)); |
| assert!(!scratch.mark_visited(99_999_999)); |
| |
| scratch.begin_graph_search(100_000_000, 100, 64).unwrap(); |
| assert!(scratch.mark_visited(99_999_999)); |
| assert_eq!(scratch.visited_capacity(), 1_000_000); |
| } |
| |
| #[test] |
| fn diskann_query_scratch_clears_graph_buffers_and_keeps_vector_working_set() { |
| let mut scratch = DiskAnnQueryScratch::default(); |
| scratch.begin_search(1024); |
| scratch.prepare_distance_table(512).fill(1.0); |
| scratch.candidates.reserve(128); |
| scratch.candidates.push(SearchCandidate { |
| node: 7, |
| distance: 1.0, |
| }); |
| scratch.loaded_adjacency_pages.reserve(16); |
| scratch.loaded_adjacency_pages.insert(0); |
| scratch.adjacency_windows.reserve(16); |
| scratch.adjacency_windows.insert(0, vec![0; 4096].into()); |
| scratch.vector_windows.insert(4096, vec![0; 64 * 1024]); |
| let candidate_capacity = scratch.candidates.capacity(); |
| let page_cache_capacity = scratch.loaded_adjacency_pages.capacity(); |
| let window_cache_capacity = scratch.adjacency_windows.capacity(); |
| |
| scratch.begin_search(1024); |
| |
| assert!(scratch.candidates.is_empty()); |
| assert!(scratch.loaded_adjacency_pages.is_empty()); |
| assert!(scratch.adjacency_windows.is_empty()); |
| assert_eq!(scratch.vector_windows.get(&4096).unwrap().len(), 64 * 1024); |
| assert_eq!(scratch.retained_window_capacity(), 4 * 1024); |
| assert_eq!(scratch.candidates.capacity(), candidate_capacity); |
| assert_eq!( |
| scratch.loaded_adjacency_pages.capacity(), |
| page_cache_capacity |
| ); |
| assert_eq!(scratch.adjacency_windows.capacity(), window_cache_capacity); |
| assert_eq!(scratch.distance_table.len(), 512); |
| } |
| |
| #[test] |
| fn diskann_query_scratch_drops_windows_over_its_retained_capacity_limit() { |
| let mut scratch = DiskAnnQueryScratch::with_window_buffer_limit(4096); |
| scratch.adjacency_windows.insert(0, vec![0; 8192].into()); |
| |
| scratch.begin_search(1); |
| |
| assert_eq!(scratch.retained_window_capacity(), 0); |
| } |
| |
| #[test] |
| fn diskann_query_adjacency_cache_evicts_oldest_window_to_fit_budget() { |
| let mut cache = AdjacencyWindowCache::default(); |
| let mut pool = WindowBufferPool::with_retained_capacity_limit(4096); |
| cache.insert(0, vec![0; 4096].into()); |
| cache.insert(4096, vec![0; 4096].into()); |
| |
| let evictions = cache.trim(&mut pool, 4096); |
| |
| assert_eq!(evictions, 1); |
| assert!(!cache.contains_key(&0)); |
| assert!(cache.contains_key(&4096)); |
| assert_eq!(cache.retained_capacity(), 4096); |
| } |
| |
| #[test] |
| fn diskann_raw_vector_working_set_is_not_retained_over_query_limit() { |
| let windows = [ |
| ReadWindow::new(0, QUERY_WINDOW_BUFFER_LIMIT_BYTES), |
| ReadWindow::new(QUERY_WINDOW_BUFFER_LIMIT_BYTES as u64, 1), |
| ]; |
| let mut cache = VectorWindowCache::default(); |
| cache.insert(0, vec![0; DISKANN_PAGE_SIZE as usize]); |
| cache.touch(0); |
| let mut pool = WindowBufferPool::default(); |
| |
| let (retain, _) = prepare_vector_window_cache( |
| &windows, |
| &mut cache, |
| &mut pool, |
| QUERY_WINDOW_BUFFER_LIMIT_BYTES, |
| ); |
| |
| assert!(!retain); |
| assert!(cache.is_empty()); |
| assert!(cache.recency.is_empty()); |
| } |
| |
| #[test] |
| fn diskann_raw_vector_working_set_drops_overallocated_buffers() { |
| let windows = [ReadWindow::new(0, 1)]; |
| let mut oversized = Vec::with_capacity(QUERY_WINDOW_BUFFER_LIMIT_BYTES + 1); |
| oversized.push(0); |
| let mut cache = VectorWindowCache::default(); |
| cache.insert(0, oversized); |
| cache.touch(0); |
| let mut pool = WindowBufferPool::default(); |
| |
| let (retain, _) = prepare_vector_window_cache( |
| &windows, |
| &mut cache, |
| &mut pool, |
| QUERY_WINDOW_BUFFER_LIMIT_BYTES, |
| ); |
| |
| assert!(retain, "the requested one-byte window is cacheable"); |
| assert!( |
| cache.is_empty(), |
| "an oversized allocation must be replaced before the read" |
| ); |
| } |
| |
| #[test] |
| fn diskann_raw_vector_cache_evicts_only_least_recently_used_windows() { |
| let window_bytes = 4 * 1024 * 1024; |
| let offsets = [0, window_bytes as u64, (2 * window_bytes) as u64]; |
| let mut cache = VectorWindowCache::default(); |
| for offset in offsets { |
| cache.insert(offset, vec![0; window_bytes]); |
| cache.touch(offset); |
| } |
| let mut pool = WindowBufferPool::default(); |
| |
| let evictions = cache.trim(&mut pool, QUERY_WINDOW_BUFFER_LIMIT_BYTES); |
| |
| assert_eq!(evictions, 1); |
| assert!(!cache.contains_key(&offsets[0])); |
| assert!(cache.contains_key(&offsets[1])); |
| assert!(cache.contains_key(&offsets[2])); |
| assert_eq!(cache.retained_capacity(), QUERY_WINDOW_BUFFER_LIMIT_BYTES); |
| assert_eq!(cache.recency.oldest_offsets(), offsets[1..]); |
| } |
| |
| #[test] |
| fn diskann_adjacency_read_keeps_cached_windows_required_by_current_round() { |
| let window_bytes = 1024 * 1024; |
| let mut cache = AdjacencyWindowCache::default(); |
| for window in 0..9 { |
| cache.insert((window * window_bytes) as u64, vec![0; window_bytes].into()); |
| } |
| let required = [ReadWindow::new(0, window_bytes)]; |
| let mut pool = WindowBufferPool::default(); |
| |
| let evictions = |
| prepare_adjacency_window_cache(&required, window_bytes, &mut cache, &mut pool); |
| |
| assert_eq!(evictions, 2); |
| assert!( |
| cache.contains_key(&0), |
| "a cached window selected for this graph round must survive preparation trimming" |
| ); |
| assert!(!cache.contains_key(&(window_bytes as u64))); |
| assert!(!cache.contains_key(&((2 * window_bytes) as u64))); |
| } |
| |
| #[test] |
| fn diskann_vector_window_cache_tracks_capacity_incrementally() { |
| let mut cache = VectorWindowCache::default(); |
| let mut first = Vec::with_capacity(64); |
| first.resize(4, 1); |
| let mut second = Vec::with_capacity(128); |
| second.resize(4, 2); |
| cache.insert(10, first); |
| cache.insert(20, second); |
| cache.touch(10); |
| assert_eq!(cache.retained_capacity(), 192); |
| |
| let mut buffers = WindowBufferPool::with_retained_capacity_limit(0); |
| assert_eq!(cache.trim(&mut buffers, 64), 1); |
| assert!(cache.contains_key(&10)); |
| assert!(!cache.contains_key(&20)); |
| assert_eq!(cache.retained_capacity(), 64); |
| |
| cache.remove(10); |
| assert_eq!(cache.retained_capacity(), 0); |
| } |
| |
| #[test] |
| fn diskann_raw_vector_cache_uses_constant_time_recency_updates() { |
| let cache = VectorWindowCache::default(); |
| |
| assert!( |
| std::any::type_name_of_val(&cache.recency).ends_with("OffsetLru"), |
| "raw-vector cache recency must use linked hash updates instead of scans" |
| ); |
| } |
| |
| #[test] |
| fn diskann_window_buffer_pool_reuses_capacity_without_reusing_content() { |
| let mut pool = WindowBufferPool::with_retained_capacity_limit(8192); |
| let mut original = vec![7u8; 4096]; |
| let allocation = original.as_mut_ptr(); |
| pool.recycle(original); |
| |
| let reused = pool.take(4096).unwrap(); |
| |
| assert_eq!(reused.as_ptr(), allocation); |
| assert_eq!(reused.len(), 4096); |
| assert!(reused.iter().all(|value| *value == 0)); |
| assert_eq!(pool.retained_capacity, 0); |
| } |
| |
| #[test] |
| fn diskann_batch_workers_split_the_aggregate_window_buffer_budget() { |
| assert_eq!(window_buffer_limit_per_worker(1), 8 * 1024 * 1024); |
| assert_eq!(window_buffer_limit_per_worker(8), 8 * 1024 * 1024); |
| assert_eq!(window_buffer_limit_per_worker(16), 4 * 1024 * 1024); |
| } |
| |
| #[test] |
| fn diskann_query_scratch_reuses_round_selection_buffers() { |
| let mut scratch = DiskAnnQueryScratch::default(); |
| for (node, distance) in [(7, 2.0), (11, 3.0), (13, 4.0)] { |
| scratch |
| .insert_graph_candidate(SearchCandidate { node, distance }, 4) |
| .unwrap(); |
| } |
| |
| scratch.select_round(2); |
| assert_eq!(scratch.selected_nodes, vec![7, 11]); |
| let nodes_capacity = scratch.selected_nodes.capacity(); |
| |
| scratch.select_round(1); |
| |
| assert_eq!(scratch.selected_nodes, vec![13]); |
| assert_eq!(scratch.selected_nodes.capacity(), nodes_capacity); |
| } |
| |
| #[test] |
| fn diskann_raw_vector_distance_decodes_page_bytes_directly() { |
| let bytes = [1.0f32, 3.0f32] |
| .into_iter() |
| .flat_map(f32::to_le_bytes) |
| .collect::<Vec<_>>(); |
| |
| let distance = l2_distance_from_le_bytes(&[2.0, 5.0], &bytes).unwrap(); |
| |
| assert_eq!(distance, 5.0); |
| } |
| |
| #[test] |
| fn diskann_raw_vector_distance_uses_available_simd_kernel() { |
| let query = (0..33).map(|index| index as f32 * 0.25).collect::<Vec<_>>(); |
| let vector = (0..33) |
| .map(|index| index as f32 * -0.5 + 3.0) |
| .collect::<Vec<_>>(); |
| let mut storage = vec![0x7f]; |
| storage.extend(vector.iter().flat_map(|value| value.to_le_bytes())); |
| let unaligned = &storage[1..]; |
| let expected = query |
| .iter() |
| .zip(&vector) |
| .map(|(left, right)| { |
| let delta = left - right; |
| delta * delta |
| }) |
| .sum::<f32>(); |
| |
| let kernel = selected_raw_vector_distance_kernel(query.len()); |
| let distance = l2_distance_from_le_bytes(&query, unaligned).unwrap(); |
| let explicit_distance = |
| l2_distance_from_le_bytes_with_kernel(&query, unaligned, kernel).unwrap(); |
| |
| assert!((distance - expected).abs() <= expected.abs() * 1.0e-5); |
| assert_eq!(explicit_distance, distance); |
| #[cfg(all(target_endian = "little", target_arch = "x86_64"))] |
| assert_eq!( |
| selected_raw_vector_distance_kernel(query.len()), |
| if is_x86_feature_detected!("avx2") { |
| RawVectorDistanceKernel::Avx2 |
| } else { |
| RawVectorDistanceKernel::Scalar |
| } |
| ); |
| #[cfg(all(target_endian = "little", target_arch = "aarch64"))] |
| assert_eq!( |
| selected_raw_vector_distance_kernel(query.len()), |
| RawVectorDistanceKernel::Neon |
| ); |
| #[cfg(not(all( |
| target_endian = "little", |
| any(target_arch = "x86_64", target_arch = "aarch64") |
| )))] |
| assert_eq!( |
| selected_raw_vector_distance_kernel(query.len()), |
| RawVectorDistanceKernel::Scalar |
| ); |
| |
| let mut non_finite = unaligned.to_vec(); |
| non_finite[8 * size_of::<f32>()..9 * size_of::<f32>()] |
| .copy_from_slice(&f32::INFINITY.to_le_bytes()); |
| assert_eq!( |
| l2_distance_from_le_bytes(&query, &non_finite) |
| .expect_err("SIMD exact distance must reject infinity") |
| .kind(), |
| io::ErrorKind::InvalidData |
| ); |
| |
| non_finite[8 * size_of::<f32>()..9 * size_of::<f32>()] |
| .copy_from_slice(&f32::NAN.to_le_bytes()); |
| assert_eq!( |
| l2_distance_from_le_bytes(&query, &non_finite) |
| .expect_err("SIMD exact distance must reject NaN") |
| .kind(), |
| io::ErrorKind::InvalidData |
| ); |
| } |
| |
| struct RoundRecordingReader { |
| inner: Cursor<Vec<u8>>, |
| rounds: ReadRounds, |
| } |
| |
| impl SeekRead for RoundRecordingReader { |
| fn pread(&mut self, ranges: &mut [ReadRequest<'_>]) -> io::Result<()> { |
| self.rounds.lock().unwrap().push( |
| ranges |
| .iter() |
| .map(|range| (range.pos, range.buf.len())) |
| .collect(), |
| ); |
| for range in ranges { |
| self.inner.set_position(range.pos); |
| io::Read::read_exact(&mut self.inner, range.buf)?; |
| } |
| Ok(()) |
| } |
| } |
| |
| struct CapabilityRecordingReader { |
| inner: Cursor<Vec<u8>>, |
| rounds: ReadRounds, |
| capabilities: SeekReadCapabilities, |
| } |
| |
| impl SeekRead for CapabilityRecordingReader { |
| fn pread(&mut self, ranges: &mut [ReadRequest<'_>]) -> io::Result<()> { |
| self.rounds.lock().unwrap().push( |
| ranges |
| .iter() |
| .map(|range| (range.pos, range.buf.len())) |
| .collect(), |
| ); |
| for range in ranges { |
| self.inner.set_position(range.pos); |
| io::Read::read_exact(&mut self.inner, range.buf)?; |
| } |
| Ok(()) |
| } |
| |
| fn read_capabilities(&self) -> SeekReadCapabilities { |
| self.capabilities |
| } |
| } |
| |
| #[derive(Clone)] |
| struct CloneCountingReader { |
| bytes: Arc<[u8]>, |
| clone_count: Arc<AtomicUsize>, |
| reads: Arc<Mutex<Vec<(u64, usize)>>>, |
| } |
| |
| impl SeekRead for CloneCountingReader { |
| fn pread(&mut self, ranges: &mut [ReadRequest<'_>]) -> io::Result<()> { |
| for range in ranges { |
| self.reads |
| .lock() |
| .unwrap() |
| .push((range.pos, range.buf.len())); |
| let start = usize::try_from(range.pos) |
| .map_err(|_| io::Error::other("test read offset exceeds usize"))?; |
| let end = start |
| .checked_add(range.buf.len()) |
| .ok_or_else(|| io::Error::other("test read range overflows"))?; |
| range.buf.copy_from_slice( |
| self.bytes |
| .get(start..end) |
| .ok_or(io::ErrorKind::UnexpectedEof)?, |
| ); |
| } |
| Ok(()) |
| } |
| |
| fn try_clone_reader(&self) -> io::Result<Option<Self>> { |
| self.clone_count.fetch_add(1, AtomicOrdering::SeqCst); |
| Ok(Some(self.clone())) |
| } |
| } |
| |
| #[derive(Clone)] |
| struct ToggleFailReader { |
| inner: Cursor<Vec<u8>>, |
| fail_reads: Arc<AtomicBool>, |
| } |
| |
| impl SeekRead for ToggleFailReader { |
| fn pread(&mut self, ranges: &mut [ReadRequest<'_>]) -> io::Result<()> { |
| if self.fail_reads.load(AtomicOrdering::SeqCst) { |
| return Err(io::Error::other("injected query read failure")); |
| } |
| for range in ranges { |
| self.inner.set_position(range.pos); |
| io::Read::read_exact(&mut self.inner, range.buf)?; |
| } |
| Ok(()) |
| } |
| |
| fn try_clone_reader(&self) -> io::Result<Option<Self>> { |
| Ok(Some(self.clone())) |
| } |
| } |
| |
| #[test] |
| fn diskann_read_tiers_plan_aligned_deduplicated_and_clipped_windows() { |
| let section = SectionRange::new(139_264, 20 * 4096); |
| |
| let local = ReadWindowPlanner::new(DeploymentProfile::LocalStorage.read_plan(), section); |
| assert_eq!(local.beam_width(), 4); |
| assert_eq!( |
| local.plan_logical_pages([2, 0, 2, 0]), |
| vec![ReadWindow::new(139_264, 16 * 1024)] |
| ); |
| |
| let object_store = |
| ReadWindowPlanner::new(DeploymentProfile::ObjectStore.read_plan(), section); |
| assert_eq!(object_store.beam_width(), 16); |
| assert_eq!( |
| object_store.plan_logical_pages([19, 1, 16, 0, 15, 19]), |
| vec![ |
| ReadWindow::new(139_264, 64 * 1024), |
| ReadWindow::new(139_264 + 64 * 1024, 16 * 1024), |
| ] |
| ); |
| } |
| |
| #[test] |
| fn diskann_reader_capabilities_refine_windows_and_beam_width() { |
| let plan = DeploymentProfile::RemoteStorage |
| .read_plan() |
| .with_capabilities(SeekReadCapabilities { |
| estimated_random_read_latency_nanos: 0, |
| preferred_window_bytes: 16 * 1024, |
| max_ranges_per_pread: 2, |
| }); |
| let planner = ReadWindowPlanner::new(plan, SectionRange::new(128 * 1024, 64 * 1024)); |
| |
| assert_eq!(planner.beam_width(), 2); |
| assert_eq!( |
| planner.plan_logical_pages([0, 1, 3, 4]), |
| vec![ |
| ReadWindow::new(128 * 1024, 16 * 1024), |
| ReadWindow::new(144 * 1024, 16 * 1024), |
| ] |
| ); |
| } |
| |
| #[test] |
| fn diskann_reader_capabilities_bound_ranges_per_pread_call() { |
| let dimension = 8; |
| let count = 32; |
| let data = (0..count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (0..count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, count).unwrap(); |
| index.add(&data, &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| |
| let rounds = Arc::new(Mutex::new(Vec::new())); |
| let source = CapabilityRecordingReader { |
| inner: Cursor::new(bytes), |
| rounds: Arc::clone(&rounds), |
| capabilities: SeekReadCapabilities { |
| estimated_random_read_latency_nanos: 0, |
| preferred_window_bytes: 0, |
| max_ranges_per_pread: 2, |
| }, |
| }; |
| let mut reader = DiskAnnIndexReader::open(source).unwrap(); |
| rounds.lock().unwrap().clear(); |
| let mut buffers = [[0u8; 1]; 5]; |
| let mut requests = buffers |
| .iter_mut() |
| .enumerate() |
| .map(|(offset, buffer)| ReadRequest::new(offset as u64, buffer)) |
| .collect::<Vec<_>>(); |
| |
| reader.pread_ranges(&mut requests).unwrap(); |
| |
| assert_eq!( |
| rounds |
| .lock() |
| .unwrap() |
| .iter() |
| .map(Vec::len) |
| .collect::<Vec<_>>(), |
| vec![2, 2, 1] |
| ); |
| } |
| |
| #[test] |
| fn diskann_pq_prefetch_lookahead_is_bounded_by_code_work() { |
| assert_eq!(pq_prefetch_lookahead(1), 16); |
| assert_eq!(pq_prefetch_lookahead(16), 16); |
| assert_eq!(pq_prefetch_lookahead(64), 4); |
| assert_eq!(pq_prefetch_lookahead(1024), 4); |
| } |
| |
| #[test] |
| fn diskann_representative_query_warmup_preserves_observed_search_stats() { |
| let dimension = 8; |
| let count = 64; |
| let data = (0..count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (0..count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, count).unwrap(); |
| index.add(&data, &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let mut reader = DiskAnnIndexReader::open(Cursor::new(bytes)).unwrap(); |
| reader.search(&data[..dimension], 3, 16).unwrap(); |
| let stats = reader.last_search_stats(); |
| |
| reader |
| .warmup_queries(&data[dimension..5 * dimension], 16) |
| .unwrap(); |
| |
| assert_eq!(reader.last_search_stats(), stats); |
| let result = reader |
| .search(&data[dimension..2 * dimension], 1, 16) |
| .unwrap(); |
| assert_eq!(result.0.len(), 1); |
| assert!(result.1[0].is_finite()); |
| } |
| |
| #[test] |
| fn diskann_window_buffers_are_recycled_after_read_failure() { |
| let dimension = 8; |
| let count = 256; |
| let data = (0..count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (0..count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, count).unwrap(); |
| index.add(&data, &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let fail_reads = Arc::new(AtomicBool::new(false)); |
| let mut reader = DiskAnnIndexReader::open(ToggleFailReader { |
| inner: Cursor::new(bytes), |
| fail_reads: Arc::clone(&fail_reads), |
| }) |
| .unwrap(); |
| reader.ensure_resident().unwrap(); |
| fail_reads.store(true, AtomicOrdering::SeqCst); |
| let planner = ReadWindowPlanner::new( |
| DeploymentProfile::LocalStorage.read_plan(), |
| reader.header.sections.vectors, |
| ); |
| let window = planner.window_for_logical_page(0).unwrap(); |
| let mut cache = VectorWindowCache::default(); |
| let mut pool = WindowBufferPool::with_retained_capacity_limit(window.length); |
| |
| let error = reader |
| .load_vector_windows(&[window], &mut cache, &mut pool) |
| .expect_err("injected read failure must propagate"); |
| |
| assert_eq!(error.kind(), io::ErrorKind::Other); |
| assert!(cache.is_empty()); |
| assert_eq!(pool.retained_capacity, window.length); |
| } |
| |
| #[test] |
| fn diskann_batch_read_failure_clears_query_local_raw_vector_cache() { |
| let dimension = 128; |
| let count = 512; |
| let data = (0..count * dimension) |
| .map(|offset| ((offset * 31) % 997) as f32 * 0.01) |
| .collect::<Vec<_>>(); |
| let ids = (0..count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 8, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, count).unwrap(); |
| index.add(&data, &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let fail_reads = Arc::new(AtomicBool::new(false)); |
| let mut reader = DiskAnnIndexReader::open(ToggleFailReader { |
| inner: Cursor::new(bytes), |
| fail_reads: Arc::clone(&fail_reads), |
| }) |
| .unwrap(); |
| reader.ensure_resident().unwrap(); |
| reader |
| .rerank_with_query_scratch( |
| &data[..dimension], |
| &[SearchCandidate { |
| node: 0, |
| distance: 0.0, |
| }], |
| 1, |
| ) |
| .unwrap(); |
| assert!(!reader.query_scratch.vector_windows.is_empty()); |
| |
| let second_page_node = |
| DISKANN_PAGE_SIZE as usize / reader.header.vector_record_size as usize; |
| fail_reads.store(true, AtomicOrdering::SeqCst); |
| let error = reader |
| .rerank_candidate_batch_streaming( |
| &data[second_page_node * dimension..(second_page_node + 1) * dimension], |
| 1, |
| vec![vec![(0, vec![second_page_node])]], |
| ) |
| .expect_err("injected batch rerank read failure must propagate"); |
| |
| assert_eq!(error.kind(), io::ErrorKind::Other); |
| assert!(reader.query_scratch.vector_windows.is_empty()); |
| assert!(reader.query_scratch.vector_windows.recency.is_empty()); |
| } |
| |
| #[test] |
| fn diskann_graph_search_finds_and_exactly_reranks_query_vector() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let query_index = 37; |
| let query = &data[query_index * dimension..(query_index + 1) * dimension]; |
| let mut reader = DiskAnnIndexReader::open(Cursor::new(bytes)).unwrap(); |
| |
| let (result_ids, distances) = reader.search(query, 5, 100).unwrap(); |
| |
| assert_eq!(result_ids.len(), 5); |
| assert_eq!(result_ids[0], ids[query_index]); |
| assert_eq!(distances[0], 0.0); |
| assert!(distances.windows(2).all(|pair| pair[0] <= pair[1])); |
| let cached_vector_capacity = reader |
| .query_scratch |
| .vector_windows |
| .entries |
| .values() |
| .map(WindowPayload::capacity) |
| .sum::<usize>(); |
| assert!(cached_vector_capacity > 0); |
| assert!(cached_vector_capacity <= QUERY_WINDOW_BUFFER_LIMIT_BYTES); |
| assert!(reader.query_scratch.adjacency_windows.is_empty()); |
| assert!(reader.query_scratch.retained_window_capacity() <= QUERY_WINDOW_BUFFER_LIMIT_BYTES); |
| } |
| |
| #[test] |
| fn diskann_4bit_roundtrip_supports_graph_and_filtered_search() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::with_pq_bits( |
| dimension, |
| MetricType::L2, |
| 2, |
| 4, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let query_index = 37; |
| let query = &data[query_index * dimension..(query_index + 1) * dimension]; |
| let mut reader = DiskAnnIndexReader::open(Cursor::new(bytes)).unwrap(); |
| |
| reader.ensure_resident().unwrap(); |
| assert_eq!(reader.header.pq_bits, 4); |
| assert_eq!(reader.pq().unwrap().ksub, 16); |
| assert_eq!(reader.pq_codes().unwrap().len(), indexed_count); |
| |
| let (result_ids, distances) = reader.search(query, 5, 100).unwrap(); |
| assert_eq!(result_ids[0], ids[query_index]); |
| assert_eq!(distances[0], 0.0); |
| |
| let mut filter = RoaringTreemap::new(); |
| filter.extend(ids[32..48].iter().map(|row_id| *row_id as u64)); |
| let mut filter_bytes = Vec::new(); |
| filter.serialize_into(&mut filter_bytes).unwrap(); |
| let (filtered_ids, filtered_distances) = reader |
| .search_with_roaring_filter(query, 5, 100, &filter_bytes) |
| .unwrap(); |
| assert_eq!(filtered_ids[0], ids[query_index]); |
| assert_eq!(filtered_distances[0], 0.0); |
| |
| let mut queries = Vec::new(); |
| queries.extend_from_slice(query); |
| queries.extend_from_slice(query); |
| let (batch_ids, batch_distances) = reader |
| .search_batch_with_roaring_filter(&queries, 5, 100, &filter_bytes) |
| .unwrap(); |
| assert_eq!(batch_ids[0], ids[query_index]); |
| assert_eq!(batch_ids[5], ids[query_index]); |
| assert_eq!(batch_distances[0], 0.0); |
| assert_eq!(batch_distances[5], 0.0); |
| } |
| |
| #[test] |
| fn diskann_repeated_single_query_reuses_raw_vector_windows() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let header = crate::diskann_io::DiskAnnHeader::decode(&bytes[..256]).unwrap(); |
| let reads = Arc::new(Mutex::new(Vec::new())); |
| let mut reader = DiskAnnIndexReader::open(CloneCountingReader { |
| bytes: Arc::from(bytes), |
| clone_count: Arc::new(AtomicUsize::new(0)), |
| reads: Arc::clone(&reads), |
| }) |
| .unwrap(); |
| let query = &data[..dimension]; |
| |
| reads.lock().unwrap().clear(); |
| let first = reader.search(query, 5, 100).unwrap(); |
| let first_vector_reads = reads |
| .lock() |
| .unwrap() |
| .iter() |
| .filter(|(offset, _)| { |
| *offset >= header.sections.vectors.offset |
| && *offset < header.sections.vectors.offset + header.sections.vectors.length |
| }) |
| .count(); |
| assert!(first_vector_reads > 0); |
| let first_stats = reader.last_search_stats(); |
| assert_eq!(first_stats.raw_vector_cache_hits, 0); |
| assert_eq!( |
| first_stats.raw_vector_cache_misses, |
| first_stats.rerank_unique_windows |
| ); |
| |
| reads.lock().unwrap().clear(); |
| let second = reader.search(query, 5, 100).unwrap(); |
| let second_vector_reads = reads |
| .lock() |
| .unwrap() |
| .iter() |
| .filter(|(offset, _)| { |
| *offset >= header.sections.vectors.offset |
| && *offset < header.sections.vectors.offset + header.sections.vectors.length |
| }) |
| .count(); |
| |
| assert_eq!(second, first); |
| let second_stats = reader.last_search_stats(); |
| assert_eq!( |
| second_stats.raw_vector_cache_hits, |
| second_stats.rerank_unique_windows |
| ); |
| assert_eq!(second_stats.raw_vector_cache_misses, 0); |
| assert_eq!( |
| second_vector_reads, 0, |
| "the previous single-query raw-vector working set should be reused" |
| ); |
| } |
| |
| #[test] |
| fn diskann_repeated_query_reuses_shared_adjacency_windows() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let header = crate::diskann_io::DiskAnnHeader::decode(&bytes[..256]).unwrap(); |
| let reads = Arc::new(Mutex::new(Vec::new())); |
| let mut reader = DiskAnnIndexReader::open_with_options( |
| CloneCountingReader { |
| bytes: Arc::from(bytes), |
| clone_count: Arc::new(AtomicUsize::new(0)), |
| reads: Arc::clone(&reads), |
| }, |
| VectorIndexReaderOptions::with_cache_budgets( |
| DeploymentProfile::Auto, |
| 0, |
| 16 * 1024 * 1024, |
| 4 * 1024 * 1024 * 1024, |
| 8 * 1024 * 1024, |
| ), |
| ) |
| .unwrap(); |
| let query = &data[..dimension]; |
| |
| reader.search(query, 5, 100).unwrap(); |
| reads.lock().unwrap().clear(); |
| reader.search(query, 5, 100).unwrap(); |
| let adjacency_reads = reads |
| .lock() |
| .unwrap() |
| .iter() |
| .filter(|(offset, _)| { |
| *offset >= header.sections.adjacency.offset |
| && *offset < header.sections.adjacency.offset + header.sections.adjacency.length |
| }) |
| .count(); |
| |
| assert_eq!( |
| adjacency_reads, 0, |
| "the shared cold-adjacency cache should serve a repeated graph traversal" |
| ); |
| let stats = reader.last_search_stats(); |
| assert!(stats.adjacency_cache_hits > 0); |
| assert_eq!(stats.adjacency_cache_misses, 0); |
| } |
| |
| #[test] |
| fn diskann_shared_adjacency_payload_is_zero_copy() { |
| let mut payload = vec![7u8; DISKANN_PAGE_SIZE as usize]; |
| let allocation = payload.as_mut_ptr(); |
| |
| let shared = share_window_payload(payload); |
| |
| assert_eq!(shared.as_ptr(), allocation); |
| assert_eq!(shared.len(), DISKANN_PAGE_SIZE as usize); |
| assert!(shared.iter().all(|value| *value == 7)); |
| } |
| |
| #[test] |
| fn diskann_zero_adjacency_cache_budget_disables_window_reuse() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let header = crate::diskann_io::DiskAnnHeader::decode(&bytes[..256]).unwrap(); |
| let reads = Arc::new(Mutex::new(Vec::new())); |
| let mut reader = DiskAnnIndexReader::open_with_options( |
| CloneCountingReader { |
| bytes: Arc::from(bytes), |
| clone_count: Arc::new(AtomicUsize::new(0)), |
| reads: Arc::clone(&reads), |
| }, |
| VectorIndexReaderOptions::with_cache_budgets( |
| DeploymentProfile::Auto, |
| 0, |
| 0, |
| 4 * 1024 * 1024 * 1024, |
| 8 * 1024 * 1024, |
| ), |
| ) |
| .unwrap(); |
| let query = &data[..dimension]; |
| |
| reader.search(query, 5, 100).unwrap(); |
| reads.lock().unwrap().clear(); |
| reader.search(query, 5, 100).unwrap(); |
| let adjacency_reads = reads |
| .lock() |
| .unwrap() |
| .iter() |
| .filter(|(offset, _)| { |
| *offset >= header.sections.adjacency.offset |
| && *offset < header.sections.adjacency.offset + header.sections.adjacency.length |
| }) |
| .count(); |
| |
| assert!(adjacency_reads > 0); |
| let stats = reader.last_search_stats(); |
| assert_eq!(stats.adjacency_cache_hits, 0); |
| assert!(stats.adjacency_cache_misses > 0); |
| } |
| |
| #[test] |
| fn diskann_zero_raw_vector_cache_budget_disables_window_reuse() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let header = crate::diskann_io::DiskAnnHeader::decode(&bytes[..256]).unwrap(); |
| let reads = Arc::new(Mutex::new(Vec::new())); |
| let mut reader = DiskAnnIndexReader::open_with_options( |
| CloneCountingReader { |
| bytes: Arc::from(bytes), |
| clone_count: Arc::new(AtomicUsize::new(0)), |
| reads: Arc::clone(&reads), |
| }, |
| VectorIndexReaderOptions::with_cache_budgets( |
| DeploymentProfile::Auto, |
| 16 * 1024 * 1024, |
| 16 * 1024 * 1024, |
| 4 * 1024 * 1024 * 1024, |
| 0, |
| ), |
| ) |
| .unwrap(); |
| let query = &data[..dimension]; |
| |
| reader.search(query, 5, 100).unwrap(); |
| reads.lock().unwrap().clear(); |
| reader.search(query, 5, 100).unwrap(); |
| let second_vector_reads = reads |
| .lock() |
| .unwrap() |
| .iter() |
| .filter(|(offset, _)| { |
| *offset >= header.sections.vectors.offset |
| && *offset < header.sections.vectors.offset + header.sections.vectors.length |
| }) |
| .count(); |
| |
| assert!(second_vector_reads > 0); |
| assert!(reader.query_scratch.vector_windows.is_empty()); |
| } |
| |
| #[test] |
| fn diskann_oversized_rerank_counts_evicted_cached_windows() { |
| let dimension = 8; |
| let count = 256; |
| let data = (0..count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| raw_vector_encoding: DiskAnnRawVectorEncoding::F32, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, count).unwrap(); |
| index.add(&data, &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let mut reader = DiskAnnIndexReader::open_with_options( |
| Cursor::new(bytes), |
| VectorIndexReaderOptions::with_cache_budgets( |
| DeploymentProfile::Auto, |
| 16 * 1024 * 1024, |
| 16 * 1024 * 1024, |
| 4 * 1024 * 1024 * 1024, |
| DISKANN_PAGE_SIZE as usize, |
| ), |
| ) |
| .unwrap(); |
| reader.optimize_for_search().unwrap(); |
| let second_page_node = |
| DISKANN_PAGE_SIZE as usize / reader.header.vector_record_size as usize; |
| let first_page = SearchCandidate { |
| node: 0, |
| distance: 0.0, |
| }; |
| |
| reader |
| .rerank_with_query_scratch(&data[..dimension], &[first_page], 1) |
| .unwrap(); |
| assert_eq!(reader.query_scratch.vector_windows.len(), 1); |
| |
| reader.last_search_stats = DiskAnnSearchStats::default(); |
| reader |
| .rerank_with_query_scratch( |
| &data[..dimension], |
| &[ |
| first_page, |
| SearchCandidate { |
| node: second_page_node, |
| distance: 0.0, |
| }, |
| ], |
| 1, |
| ) |
| .unwrap(); |
| |
| assert_eq!(reader.last_search_stats().raw_vector_cache_evictions, 2); |
| assert_eq!(reader.last_search_stats().raw_vector_cache_hits, 1); |
| assert_eq!(reader.last_search_stats().raw_vector_cache_misses, 1); |
| } |
| |
| #[test] |
| fn diskann_single_query_cache_reuses_nonconsecutive_hot_vector_window() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 256; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| raw_vector_encoding: DiskAnnRawVectorEncoding::F32, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data, &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let header = crate::diskann_io::DiskAnnHeader::decode(&bytes[..256]).unwrap(); |
| let second_page_node = DISKANN_PAGE_SIZE as usize / header.vector_record_size as usize; |
| assert!(second_page_node < indexed_count); |
| let reads = Arc::new(Mutex::new(Vec::new())); |
| let mut reader = DiskAnnIndexReader::open(CloneCountingReader { |
| bytes: Arc::from(bytes), |
| clone_count: Arc::new(AtomicUsize::new(0)), |
| reads: Arc::clone(&reads), |
| }) |
| .unwrap(); |
| reader.optimize_for_search().unwrap(); |
| let first_page_candidate = [SearchCandidate { |
| node: 0, |
| distance: 0.0, |
| }]; |
| let second_page_candidate = [SearchCandidate { |
| node: second_page_node, |
| distance: 0.0, |
| }]; |
| let vector_read_count = || { |
| reads |
| .lock() |
| .unwrap() |
| .iter() |
| .filter(|(offset, _)| { |
| *offset >= header.sections.vectors.offset |
| && *offset < header.sections.vectors.offset + header.sections.vectors.length |
| }) |
| .count() |
| }; |
| |
| reads.lock().unwrap().clear(); |
| reader |
| .rerank_with_query_scratch(&data[..dimension], &first_page_candidate, 1) |
| .unwrap(); |
| assert_eq!(vector_read_count(), 1); |
| |
| reads.lock().unwrap().clear(); |
| reader |
| .rerank_with_query_scratch( |
| &data[second_page_node * dimension..(second_page_node + 1) * dimension], |
| &second_page_candidate, |
| 1, |
| ) |
| .unwrap(); |
| assert_eq!(vector_read_count(), 1); |
| |
| reads.lock().unwrap().clear(); |
| reader |
| .rerank_with_query_scratch(&data[..dimension], &first_page_candidate, 1) |
| .unwrap(); |
| |
| assert_eq!( |
| vector_read_count(), |
| 0, |
| "a bounded immutable-window cache should retain nonconsecutive hot pages" |
| ); |
| } |
| |
| #[test] |
| fn diskann_coalesced_search_batches_graph_beam_and_exact_rerank_reads() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let header = crate::diskann_io::DiskAnnHeader::decode(&bytes[..256]).unwrap(); |
| let rounds = Arc::new(Mutex::new(Vec::new())); |
| let recording = RoundRecordingReader { |
| inner: Cursor::new(bytes), |
| rounds: Arc::clone(&rounds), |
| }; |
| let mut reader = DiskAnnIndexReader::open_with_options( |
| recording, |
| VectorIndexReaderOptions::with_cache_budgets( |
| DeploymentProfile::ObjectStore, |
| 0, |
| 16 * 1024 * 1024, |
| 4 * 1024 * 1024 * 1024, |
| 8 * 1024 * 1024, |
| ), |
| ) |
| .unwrap(); |
| |
| reader.search(&data[..dimension], 5, 100).unwrap(); |
| |
| let rounds = rounds.lock().unwrap(); |
| let adjacency_rounds = rounds |
| .iter() |
| .filter(|round| { |
| round.iter().any(|(offset, _)| { |
| *offset >= header.sections.adjacency.offset |
| && *offset |
| < header.sections.adjacency.offset + header.sections.adjacency.length |
| }) |
| }) |
| .count(); |
| let vector_rounds = rounds |
| .iter() |
| .filter(|round| { |
| round.iter().any(|(offset, length)| { |
| *offset >= header.sections.vectors.offset |
| && *offset < header.sections.vectors.offset + header.sections.vectors.length |
| && *length > 1 |
| }) |
| }) |
| .count(); |
| assert!(adjacency_rounds <= 7, "got {adjacency_rounds} graph rounds"); |
| assert_eq!(vector_rounds, 1, "rerank must use one batched pread"); |
| } |
| |
| #[test] |
| fn diskann_unfiltered_batch_shared_rerank_reads_overlapping_window_once() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let header = crate::diskann_io::DiskAnnHeader::decode(&bytes[..256]).unwrap(); |
| let reads = Arc::new(Mutex::new(Vec::new())); |
| let clone_count = Arc::new(AtomicUsize::new(0)); |
| let mut reader = DiskAnnIndexReader::open(CloneCountingReader { |
| bytes: Arc::from(bytes), |
| clone_count, |
| reads: Arc::clone(&reads), |
| }) |
| .unwrap(); |
| let query = &data[..dimension]; |
| let queries = [query, query].concat(); |
| |
| let (batch_ids, batch_distances) = rayon::ThreadPoolBuilder::new() |
| .num_threads(2) |
| .build() |
| .unwrap() |
| .install(|| reader.search_batch(&queries, 5, 100)) |
| .unwrap(); |
| |
| assert_eq!(&batch_ids[..5], &batch_ids[5..]); |
| assert_eq!(&batch_distances[..5], &batch_distances[5..]); |
| let vector_reads = reads |
| .lock() |
| .unwrap() |
| .iter() |
| .filter(|(offset, _)| { |
| *offset >= header.sections.vectors.offset |
| && *offset < header.sections.vectors.offset + header.sections.vectors.length |
| }) |
| .copied() |
| .collect::<Vec<_>>(); |
| assert!( |
| !vector_reads.is_empty(), |
| "small batches must run complete exact reranks in parallel sessions" |
| ); |
| assert_eq!( |
| vector_reads.len(), |
| vector_reads.iter().copied().collect::<HashSet<_>>().len(), |
| "parallel sessions must share immutable raw-vector windows" |
| ); |
| assert_eq!(reader.last_search_stats().parallel_session_queries, 2); |
| let retained_vector_capacity = reader |
| .batch_workers |
| .iter() |
| .flat_map(|worker| worker.query_scratch.vector_windows.entries.values()) |
| .map(WindowPayload::capacity) |
| .sum::<usize>(); |
| assert!(retained_vector_capacity > 0); |
| assert!( |
| retained_vector_capacity |
| <= reader.options().raw_vector_cache_bytes * reader.batch_workers.len() |
| ); |
| assert_eq!( |
| reader |
| .batch_workers |
| .iter() |
| .map(|worker| worker.query_scratch.vector_windows.recency.len()) |
| .sum::<usize>(), |
| reader |
| .batch_workers |
| .iter() |
| .map(|worker| worker.query_scratch.vector_windows.len()) |
| .sum::<usize>() |
| ); |
| assert_eq!(reader.last_search_stats().parallel_exact_rerank_chunks, 0); |
| } |
| |
| #[test] |
| fn diskann_batch_reuses_worker_readers() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let clone_count = Arc::new(AtomicUsize::new(0)); |
| let mut reader = DiskAnnIndexReader::open(CloneCountingReader { |
| bytes: Arc::from(bytes), |
| clone_count: Arc::clone(&clone_count), |
| reads: Arc::new(Mutex::new(Vec::new())), |
| }) |
| .unwrap(); |
| let queries = data[..dimension].repeat(4); |
| let pool = rayon::ThreadPoolBuilder::new() |
| .num_threads(4) |
| .build() |
| .unwrap(); |
| |
| pool.install(|| reader.search_batch(&queries, 5, 100)) |
| .unwrap(); |
| let first_batch_clones = clone_count.load(AtomicOrdering::SeqCst); |
| pool.install(|| reader.search_batch(&queries, 5, 100)) |
| .unwrap(); |
| |
| assert_eq!(first_batch_clones, 4); |
| assert_eq!( |
| clone_count.load(AtomicOrdering::SeqCst), |
| first_batch_clones, |
| "the second batch should reuse retained storage handles" |
| ); |
| } |
| |
| #[test] |
| fn diskann_batch_restores_worker_pool_after_read_failure() { |
| let dimension = 8; |
| let count = 256; |
| let data = (0..count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (0..count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, count).unwrap(); |
| index.add(&data, &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let fail_reads = Arc::new(AtomicBool::new(false)); |
| let mut reader = DiskAnnIndexReader::open_with_options( |
| ToggleFailReader { |
| inner: Cursor::new(bytes), |
| fail_reads: Arc::clone(&fail_reads), |
| }, |
| VectorIndexReaderOptions::with_cache_budgets( |
| DeploymentProfile::Auto, |
| 0, |
| 0, |
| 4 * 1024 * 1024 * 1024, |
| 0, |
| ), |
| ) |
| .unwrap(); |
| let queries = data[..dimension].repeat(4); |
| let pool = rayon::ThreadPoolBuilder::new() |
| .num_threads(4) |
| .build() |
| .unwrap(); |
| |
| pool.install(|| reader.search_batch(&queries, 5, 100)) |
| .unwrap(); |
| assert_eq!(reader.batch_workers.len(), 4); |
| fail_reads.store(true, AtomicOrdering::SeqCst); |
| pool.install(|| reader.search_batch(&queries, 5, 100)) |
| .expect_err("injected worker read failure must propagate"); |
| assert_eq!(reader.batch_workers.len(), 4); |
| fail_reads.store(false, AtomicOrdering::SeqCst); |
| pool.install(|| reader.search_batch(&queries, 5, 100)) |
| .unwrap(); |
| assert_eq!(reader.batch_workers.len(), 4); |
| } |
| |
| #[test] |
| fn diskann_repeated_batch_reuses_reader_raw_vector_cache() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let header = crate::diskann_io::DiskAnnHeader::decode(&bytes[..256]).unwrap(); |
| let reads = Arc::new(Mutex::new(Vec::new())); |
| let mut reader = DiskAnnIndexReader::open(CloneCountingReader { |
| bytes: Arc::from(bytes), |
| clone_count: Arc::new(AtomicUsize::new(0)), |
| reads: Arc::clone(&reads), |
| }) |
| .unwrap(); |
| let queries = [&data[..dimension], &data[..dimension]].concat(); |
| |
| reader.search_batch(&queries, 5, 100).unwrap(); |
| reads.lock().unwrap().clear(); |
| reader.search_batch(&queries, 5, 100).unwrap(); |
| let vector_reads = reads |
| .lock() |
| .unwrap() |
| .iter() |
| .filter(|(offset, _)| { |
| *offset >= header.sections.vectors.offset |
| && *offset < header.sections.vectors.offset + header.sections.vectors.length |
| }) |
| .count(); |
| |
| assert_eq!(vector_reads, 0); |
| let stats = reader.last_search_stats(); |
| assert!(stats.raw_vector_cache_hits > 0); |
| assert_eq!(stats.raw_vector_cache_misses, 0); |
| } |
| |
| #[test] |
| fn diskann_parallel_exact_rerank_matches_single_queries() { |
| let dimension = 128; |
| let training_count = 512; |
| let indexed_count = 256; |
| let data = (0..training_count * dimension) |
| .map(|offset| { |
| ((offset * 31) % 997) as f32 * 0.01 + (offset / dimension) as f32 * 0.0001 |
| }) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 16, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let query_indices = [0, 31, 127, 255]; |
| let queries = query_indices |
| .into_iter() |
| .flat_map(|query_index| { |
| data[query_index * dimension..(query_index + 1) * dimension] |
| .iter() |
| .copied() |
| }) |
| .collect::<Vec<_>>(); |
| let mut batch_reader = DiskAnnIndexReader::open(Cursor::new(bytes.clone())).unwrap(); |
| |
| let batch = rayon::ThreadPoolBuilder::new() |
| .num_threads(4) |
| .build() |
| .unwrap() |
| .install(|| batch_reader.search_batch(&queries, 10, 100)) |
| .unwrap(); |
| let mut single_reader = DiskAnnIndexReader::open(Cursor::new(bytes)).unwrap(); |
| let mut expected_ids = Vec::new(); |
| let mut expected_distances = Vec::new(); |
| for query in queries.chunks_exact(dimension) { |
| let (query_ids, query_distances) = single_reader.search(query, 10, 100).unwrap(); |
| expected_ids.extend(query_ids); |
| expected_distances.extend(query_distances); |
| } |
| |
| assert_eq!(batch.0, expected_ids); |
| assert_eq!(batch.1, expected_distances); |
| let stats = batch_reader.last_search_stats(); |
| assert!(stats.rerank_candidate_references >= 4 * 64); |
| assert_eq!(stats.parallel_exact_rerank_chunks, 1); |
| assert_eq!( |
| stats.parallel_exact_rerank_references, |
| stats.rerank_candidate_references |
| ); |
| } |
| |
| #[test] |
| fn diskann_batch_query_chunks_bound_live_candidates() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let mut reader = DiskAnnIndexReader::open(Cursor::new(bytes)).unwrap(); |
| let query_count = BATCH_QUERY_CHUNK_SIZE + 1; |
| let queries = data[..dimension].repeat(query_count); |
| |
| let (batch_ids, batch_distances) = reader.search_batch(&queries, 1, 100).unwrap(); |
| |
| assert_eq!(batch_ids, vec![ids[0]; query_count]); |
| assert!(batch_distances.iter().all(|distance| *distance == 0.0)); |
| let stats = reader.last_search_stats(); |
| assert_eq!(stats.query_count, query_count); |
| assert_eq!(stats.query_chunks, 2); |
| assert_eq!(stats.max_queries_per_chunk, BATCH_QUERY_CHUNK_SIZE); |
| } |
| |
| #[test] |
| fn diskann_hot_adjacency_does_not_copy_into_query_window_cache() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let mut reader = DiskAnnIndexReader::open_with_options( |
| Cursor::new(bytes), |
| VectorIndexReaderOptions::with_cache_budgets( |
| DeploymentProfile::ObjectStore, |
| 16 * 1024 * 1024, |
| 16 * 1024 * 1024, |
| 4 * 1024 * 1024 * 1024, |
| 8 * 1024 * 1024, |
| ), |
| ) |
| .unwrap(); |
| reader.optimize_for_search().unwrap(); |
| let mut window_cache = AdjacencyWindowCache::default(); |
| let mut page_cache = HashSet::new(); |
| let mut window_buffers = WindowBufferPool::default(); |
| |
| reader |
| .load_adjacency_pages( |
| &[reader.header.entry_node as usize], |
| &mut window_cache, |
| &mut page_cache, |
| &mut window_buffers, |
| ) |
| .unwrap(); |
| |
| assert!(window_cache.is_empty()); |
| assert!(!page_cache.is_empty()); |
| } |
| |
| #[test] |
| fn diskann_filtered_search_skips_graph_and_pads_sparse_results() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let header = crate::diskann_io::DiskAnnHeader::decode(&bytes[..256]).unwrap(); |
| let rounds = Arc::new(Mutex::new(Vec::new())); |
| let recording = RoundRecordingReader { |
| inner: Cursor::new(bytes), |
| rounds: Arc::clone(&rounds), |
| }; |
| let mut reader = DiskAnnIndexReader::open_with_options( |
| recording, |
| VectorIndexReaderOptions::with_cache_budgets( |
| DeploymentProfile::ObjectStore, |
| 0, |
| 0, |
| 4 * 1024 * 1024 * 1024, |
| 0, |
| ), |
| ) |
| .unwrap(); |
| let query_index = 37; |
| let query = &data[query_index * dimension..(query_index + 1) * dimension]; |
| let mut filter = RoaringTreemap::new(); |
| filter.insert(ids[query_index] as u64); |
| let mut filter_bytes = Vec::new(); |
| filter.serialize_into(&mut filter_bytes).unwrap(); |
| |
| let (result_ids, distances) = reader |
| .search_with_roaring_filter(query, 3, 100, &filter_bytes) |
| .unwrap(); |
| |
| assert_eq!(result_ids, vec![ids[query_index], -1, -1]); |
| assert_eq!(distances, vec![0.0, f32::MAX, f32::MAX]); |
| let rounds = rounds.lock().unwrap(); |
| assert!(!rounds.iter().flatten().any(|(offset, _)| { |
| *offset >= header.sections.adjacency.offset |
| && *offset < header.sections.adjacency.offset + header.sections.adjacency.length |
| })); |
| assert_eq!( |
| rounds |
| .iter() |
| .filter(|round| round.iter().any(|(offset, length)| { |
| *offset >= header.sections.vectors.offset |
| && *offset < header.sections.vectors.offset + header.sections.vectors.length |
| && *length > 1 |
| })) |
| .count(), |
| 1 |
| ); |
| } |
| |
| #[test] |
| fn diskann_broad_random_access_filter_uses_graph_candidate_io() { |
| let dimension = 8; |
| let indexed_count = 1024; |
| let data = (0..indexed_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32 + (offset / dimension) as f32 * 0.01) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 1, |
| build_search_list_size: 8, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, indexed_count).unwrap(); |
| index.add(&data, &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let header = crate::diskann_io::DiskAnnHeader::decode(&bytes[..256]).unwrap(); |
| let rounds = Arc::new(Mutex::new(Vec::new())); |
| let recording = RoundRecordingReader { |
| inner: Cursor::new(bytes), |
| rounds: Arc::clone(&rounds), |
| }; |
| let mut reader = DiskAnnIndexReader::open(recording).unwrap(); |
| let mut filter = RoaringTreemap::new(); |
| filter.extend(ids.iter().map(|row_id| *row_id as u64)); |
| let mut filter_bytes = Vec::new(); |
| filter.serialize_into(&mut filter_bytes).unwrap(); |
| |
| let (result_ids, distances) = reader |
| .search_with_roaring_filter(&data[..dimension], 10, 200, &filter_bytes) |
| .unwrap(); |
| |
| assert_eq!(result_ids[0], ids[0]); |
| assert_eq!(distances[0], 0.0); |
| assert!(rounds.lock().unwrap().iter().flatten().any(|(offset, _)| { |
| *offset >= header.sections.adjacency.offset |
| && *offset < header.sections.adjacency.offset + header.sections.adjacency.length |
| })); |
| } |
| |
| #[test] |
| fn diskann_adaptive_filtered_recall_matrix_stays_within_one_percentage_point() { |
| let dimension = 8; |
| let indexed_count = 10_000; |
| let data = (0..indexed_count) |
| .flat_map(|node| { |
| (0..dimension).map(move |component| { |
| let mut hash = (node as u64) |
| .wrapping_mul(0x9e37_79b9_7f4a_7c15) |
| .wrapping_add((component as u64).wrapping_mul(0xbf58_476d_1ce4_e5b9)); |
| hash ^= hash >> 30; |
| hash = hash.wrapping_mul(0xbf58_476d_1ce4_e5b9); |
| hash ^= hash >> 27; |
| let noise = (hash as u32) as f32 / u32::MAX as f32; |
| noise + (node / 256) as f32 * 0.25 |
| }) |
| }) |
| .collect::<Vec<_>>(); |
| let ids = (0..indexed_count) |
| .map(|node| (node / 2) as i64) |
| .collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 16, |
| build_search_list_size: 100, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, indexed_count).unwrap(); |
| index.add(&data, &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let mut reader = DiskAnnIndexReader::open_with_options( |
| Cursor::new(bytes), |
| VectorIndexReaderOptions::new(4 * 1024 * 1024 * 1024), |
| ) |
| .unwrap(); |
| reader.ensure_resident().unwrap(); |
| |
| for distribution in ["random", "clustered"] { |
| for selectivity_basis_points in [1usize, 10, 100, 1000, 5000, 10_000] { |
| let matching_count = indexed_count |
| .saturating_mul(selectivity_basis_points) |
| .div_ceil(10_000) |
| .max(1); |
| let ordered_nodes = if distribution == "random" { |
| (0..indexed_count) |
| .map(|index| (index * 4051) % indexed_count) |
| .collect::<Vec<_>>() |
| } else { |
| (0..indexed_count).collect::<Vec<_>>() |
| }; |
| let matching = RoaringBitmap::from_iter( |
| ordered_nodes[..matching_count] |
| .iter() |
| .map(|node| *node as u32), |
| ); |
| let strategy = select_filtered_candidate_strategy( |
| indexed_count, |
| matching_count, |
| 10, |
| 200, |
| 16, |
| DeploymentProfile::Memory.read_plan(), |
| false, |
| ); |
| assert_eq!( |
| matches!(strategy, FilteredCandidateStrategy::Graph { .. }), |
| selectivity_basis_points == 10_000, |
| "unexpected strategy for {distribution} at {selectivity_basis_points} bps" |
| ); |
| |
| let mut hits = 0usize; |
| let mut total = 0usize; |
| for &query_node in ordered_nodes[..10.min(matching_count)].iter() { |
| let query = &data[query_node * dimension..(query_node + 1) * dimension]; |
| let target = desired_filtered_candidate_count(matching_count, 10); |
| let baseline_candidates = reader |
| .exhaustive_filtered_candidates(query, &matching, target) |
| .unwrap(); |
| let baseline = reader |
| .rerank_with_query_scratch(query, &baseline_candidates, 10) |
| .unwrap() |
| .0; |
| let adaptive_candidates = reader |
| .generate_filtered_candidates(query, 10, 200, &matching) |
| .unwrap(); |
| let adaptive = reader |
| .rerank_with_query_scratch(query, &adaptive_candidates, 10) |
| .unwrap() |
| .0; |
| |
| let mut expected_counts = HashMap::<i64, usize>::new(); |
| for row_id in baseline.into_iter().filter(|row_id| *row_id >= 0) { |
| *expected_counts.entry(row_id).or_default() += 1; |
| total += 1; |
| } |
| for row_id in adaptive.into_iter().filter(|row_id| *row_id >= 0) { |
| if expected_counts.get_mut(&row_id).is_some_and(|count| { |
| if *count == 0 { |
| false |
| } else { |
| *count -= 1; |
| true |
| } |
| }) { |
| hits += 1; |
| } |
| } |
| } |
| let recall = hits as f64 / total as f64; |
| assert!( |
| recall + 0.01 + f64::EPSILON >= 1.0, |
| "{distribution} filter at {selectivity_basis_points} bps has Recall@10 {recall:.4} against exhaustive scan" |
| ); |
| } |
| } |
| } |
| |
| #[test] |
| fn diskann_zero_k_filtered_search_still_rejects_malformed_filter() { |
| let header = crate::diskann_io::DiskAnnHeader::for_layout( |
| 8, |
| 1, |
| 0, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 1, |
| build_search_list_size: 1, |
| ..DiskAnnBuildParams::default() |
| }, |
| ) |
| .unwrap(); |
| let mut bytes = vec![0u8; header.file_len as usize]; |
| bytes[..crate::diskann_io::DISKANN_HEADER_SIZE].copy_from_slice(&header.encode()); |
| let mut reader = DiskAnnIndexReader::open(Cursor::new(bytes)).unwrap(); |
| |
| let error = reader |
| .search_with_roaring_filter(&[0.0; 8], 0, 100, &[0xff]) |
| .expect_err("malformed filters must be rejected even for zero k"); |
| |
| assert_eq!(error.kind(), io::ErrorKind::InvalidInput); |
| } |
| |
| #[test] |
| fn diskann_filtered_batch_validates_filter_before_cloning_workers() { |
| let header = crate::diskann_io::DiskAnnHeader::for_layout( |
| 8, |
| 1, |
| 0, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 1, |
| build_search_list_size: 1, |
| ..DiskAnnBuildParams::default() |
| }, |
| ) |
| .unwrap(); |
| let mut bytes = vec![0u8; header.file_len as usize]; |
| bytes[..crate::diskann_io::DISKANN_HEADER_SIZE].copy_from_slice(&header.encode()); |
| let clone_count = Arc::new(AtomicUsize::new(0)); |
| let mut reader = DiskAnnIndexReader::open(CloneCountingReader { |
| bytes: Arc::from(bytes), |
| clone_count: Arc::clone(&clone_count), |
| reads: Arc::new(Mutex::new(Vec::new())), |
| }) |
| .unwrap(); |
| |
| let error = rayon::ThreadPoolBuilder::new() |
| .num_threads(2) |
| .build() |
| .unwrap() |
| .install(|| reader.search_batch_with_roaring_filter(&[0.0; 16], 1, 100, &[0xff])) |
| .expect_err("malformed filters must fail before batch fan-out"); |
| |
| assert_eq!(error.kind(), io::ErrorKind::InvalidInput); |
| assert_eq!(clone_count.load(AtomicOrdering::SeqCst), 0); |
| } |
| |
| #[test] |
| fn diskann_filtered_batch_shares_lookup_without_preloading_adjacency() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let header = crate::diskann_io::DiskAnnHeader::decode(&bytes[..256]).unwrap(); |
| let reads = Arc::new(Mutex::new(Vec::new())); |
| let clone_count = Arc::new(AtomicUsize::new(0)); |
| let mut reader = DiskAnnIndexReader::open_with_options( |
| CloneCountingReader { |
| bytes: Arc::from(bytes), |
| clone_count: Arc::clone(&clone_count), |
| reads: Arc::clone(&reads), |
| }, |
| VectorIndexReaderOptions::with_cache_budgets( |
| DeploymentProfile::ObjectStore, |
| 4096, |
| 16 * 1024 * 1024, |
| 4 * 1024 * 1024 * 1024, |
| 8 * 1024 * 1024, |
| ), |
| ) |
| .unwrap(); |
| let mut filter = RoaringTreemap::new(); |
| filter.insert(ids[0] as u64); |
| let mut filter_bytes = Vec::new(); |
| filter.serialize_into(&mut filter_bytes).unwrap(); |
| |
| rayon::ThreadPoolBuilder::new() |
| .num_threads(2) |
| .build() |
| .unwrap() |
| .install(|| { |
| reader |
| .search_batch_with_roaring_filter(&data[..dimension * 2], 1, 100, &filter_bytes) |
| .unwrap(); |
| }); |
| |
| assert_eq!( |
| clone_count.load(AtomicOrdering::SeqCst), |
| 0, |
| "resident-PQ batch scan must not clone the storage reader" |
| ); |
| assert!(reads |
| .lock() |
| .unwrap() |
| .iter() |
| .any(|(offset, _)| { *offset == header.sections.row_id_order.offset })); |
| assert!(!reads.lock().unwrap().iter().any(|(offset, _)| { |
| *offset >= header.sections.adjacency.offset |
| && *offset < header.sections.adjacency.offset + header.sections.adjacency.length |
| })); |
| assert_eq!( |
| reads |
| .lock() |
| .unwrap() |
| .iter() |
| .filter(|(offset, length)| { |
| *offset >= header.sections.vectors.offset |
| && *offset < header.sections.vectors.offset + header.sections.vectors.length |
| && *length > 1 |
| }) |
| .count(), |
| 1, |
| "the parent reranker must read a shared vector window once" |
| ); |
| } |
| |
| #[test] |
| fn diskann_filtered_streaming_batch_matches_single_queries_with_duplicate_row_ids() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let mut ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| ids[0] = 7; |
| ids[1] = 7; |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let mut filter = RoaringTreemap::new(); |
| filter.insert(7); |
| let mut filter_bytes = Vec::new(); |
| filter.serialize_into(&mut filter_bytes).unwrap(); |
| |
| let queries = &data[..dimension * 2]; |
| let mut batch_reader = DiskAnnIndexReader::open(Cursor::new(bytes.clone())).unwrap(); |
| let batch = batch_reader |
| .search_batch_with_roaring_filter(queries, 3, 100, &filter_bytes) |
| .unwrap(); |
| let mut single_reader = DiskAnnIndexReader::open(Cursor::new(bytes)).unwrap(); |
| let mut expected_ids = Vec::new(); |
| let mut expected_distances = Vec::new(); |
| for query in queries.chunks_exact(dimension) { |
| let (ids, distances) = single_reader |
| .search_with_roaring_filter(query, 3, 100, &filter_bytes) |
| .unwrap(); |
| expected_ids.extend(ids); |
| expected_distances.extend(distances); |
| } |
| |
| assert_eq!(batch.0, expected_ids); |
| assert_eq!(batch.1, expected_distances); |
| assert_eq!(&batch.0[..3], &[7, 7, -1]); |
| assert_eq!(&batch.0[3..], &[7, 7, -1]); |
| } |
| |
| #[test] |
| fn diskann_search_stats_report_tiled_filtered_batch() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let mut reader = DiskAnnIndexReader::open(CloneCountingReader { |
| bytes: Arc::from(bytes), |
| clone_count: Arc::new(AtomicUsize::new(0)), |
| reads: Arc::new(Mutex::new(Vec::new())), |
| }) |
| .unwrap(); |
| let mut filter = RoaringTreemap::new(); |
| filter.insert(ids[0] as u64); |
| let mut filter_bytes = Vec::new(); |
| filter.serialize_into(&mut filter_bytes).unwrap(); |
| |
| rayon::ThreadPoolBuilder::new() |
| .num_threads(2) |
| .build() |
| .unwrap() |
| .install(|| { |
| reader |
| .search_batch_with_roaring_filter(&data[..dimension * 2], 1, 100, &filter_bytes) |
| .unwrap(); |
| }); |
| |
| let stats = reader.last_search_stats(); |
| assert_eq!(stats.query_count, 2); |
| assert_eq!(stats.filtered_exhaustive_queries, 2); |
| assert_eq!(stats.filtered_graph_queries, 0); |
| assert_eq!(stats.pq_distance_evaluations, 2); |
| assert_eq!(stats.pq_code_loads, 1); |
| assert_eq!(stats.rerank_candidate_references, 2); |
| assert_eq!(stats.rerank_unique_windows, 1); |
| } |
| |
| #[test] |
| fn diskann_filtered_pq_batch_kernel_reuses_code_loads() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let mut filter = RoaringTreemap::new(); |
| filter.extend(ids[..32].iter().map(|row_id| *row_id as u64)); |
| let mut filter_bytes = Vec::new(); |
| filter.serialize_into(&mut filter_bytes).unwrap(); |
| let queries = &data[..dimension * 4]; |
| let mut batch_reader = DiskAnnIndexReader::open(Cursor::new(bytes.clone())).unwrap(); |
| |
| let batch = batch_reader |
| .search_batch_with_roaring_filter(queries, 3, 100, &filter_bytes) |
| .unwrap(); |
| let mut single_reader = DiskAnnIndexReader::open(Cursor::new(bytes)).unwrap(); |
| let mut expected_ids = Vec::new(); |
| let mut expected_distances = Vec::new(); |
| for query in queries.chunks_exact(dimension) { |
| let (query_ids, query_distances) = single_reader |
| .search_with_roaring_filter(query, 3, 100, &filter_bytes) |
| .unwrap(); |
| expected_ids.extend(query_ids); |
| expected_distances.extend(query_distances); |
| } |
| |
| assert_eq!(batch.0, expected_ids); |
| assert_eq!(batch.1, expected_distances); |
| let stats = batch_reader.last_search_stats(); |
| assert_eq!(stats.filtered_exhaustive_queries, 4); |
| assert_eq!(stats.pq_distance_evaluations, 4 * 32); |
| assert_eq!( |
| stats.pq_code_loads, 32, |
| "one four-query tile should load each matching PQ code once" |
| ); |
| } |
| |
| #[test] |
| fn diskann_filtered_search_clamps_extreme_l_search_without_panicking() { |
| let header = crate::diskann_io::DiskAnnHeader::for_layout( |
| 8, |
| 1, |
| 0, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 1, |
| build_search_list_size: 1, |
| ..DiskAnnBuildParams::default() |
| }, |
| ) |
| .unwrap(); |
| let mut bytes = vec![0u8; header.file_len as usize]; |
| bytes[..crate::diskann_io::DISKANN_HEADER_SIZE].copy_from_slice(&header.encode()); |
| let filter = RoaringTreemap::new(); |
| let mut filter_bytes = Vec::new(); |
| filter.serialize_into(&mut filter_bytes).unwrap(); |
| let mut reader = DiskAnnIndexReader::open(Cursor::new(bytes)).unwrap(); |
| |
| let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { |
| reader.search_with_roaring_filter(&[0.0; 8], 1, usize::MAX, &filter_bytes) |
| })) |
| .expect("public io::Result search API must not panic for extreme l_search") |
| .unwrap(); |
| |
| assert_eq!(result.0, vec![-1]); |
| assert_eq!(result.1, vec![f32::MAX]); |
| } |
| |
| #[test] |
| fn topk_stability_ignores_padding_and_aggregates_queries() { |
| let left = [10, 11, -1, 20, 21, 22]; |
| let right = [11, 10, -1, 20, 99, 22]; |
| let left_distances = [1.0, 2.0, f32::MAX, 1.0, 2.0, 3.0]; |
| let right_distances = [2.0, 1.0, f32::MAX, 1.0, 9.0, 3.0]; |
| |
| assert_eq!( |
| topk_result_stability(&left, &left_distances, &right, &right_distances, 3), |
| 4.0 / 5.0 |
| ); |
| assert_eq!(topk_result_stability(&[], &[], &[], &[], 0), 0.0); |
| assert_eq!( |
| topk_result_stability(&[1, 2], &[1.0, 2.0], &[1], &[1.0], 2), |
| 0.0 |
| ); |
| } |
| |
| #[test] |
| fn topk_stability_supports_negative_ids_and_duplicate_multiplicity() { |
| assert_eq!( |
| topk_result_stability(&[-1, -7], &[1.0, 2.0], &[-1, -8], &[1.0, 2.0], 2), |
| 0.5 |
| ); |
| assert_eq!( |
| topk_result_stability(&[7, 7], &[1.0, 2.0], &[7, 8], &[1.0, 2.0], 2), |
| 0.5 |
| ); |
| assert_eq!( |
| topk_result_stability(&[-1, 7], &[f32::MAX, 2.0], &[-1, 7], &[f32::MAX, 2.0], 2,), |
| 1.0 |
| ); |
| } |
| |
| #[test] |
| fn diskann_calibration_selects_and_remembers_smallest_stable_width() { |
| let dimension = 8; |
| let indexed_count = 64; |
| let data = (0..indexed_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32 + (offset / dimension) as f32 * 0.01) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, indexed_count).unwrap(); |
| index.add(&data, &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let mut reader = DiskAnnIndexReader::open(Cursor::new(bytes)).unwrap(); |
| let queries = &data[..4 * dimension]; |
| |
| assert_eq!(reader.calibrate_l_search(queries, 5).unwrap(), 100); |
| assert_eq!(reader.calibrated_l_search, Some(100)); |
| assert_eq!(reader.last_search_stats(), DiskAnnSearchStats::default()); |
| |
| let error = reader.calibrate_l_search(&[], 5).unwrap_err(); |
| assert!(error.to_string().contains("one or more complete vectors")); |
| } |
| |
| #[test] |
| fn diskann_search_stats_report_actual_filtered_scan() { |
| let dimension = 8; |
| let training_count = 256; |
| let indexed_count = 64; |
| let data = (0..training_count * dimension) |
| .map(|offset| ((offset * 31) % 127) as f32) |
| .collect::<Vec<_>>(); |
| let ids = (1000..1000 + indexed_count as i64).collect::<Vec<_>>(); |
| let mut index = DiskAnnIndex::new( |
| dimension, |
| MetricType::L2, |
| 2, |
| DiskAnnBuildParams { |
| max_degree: 8, |
| build_search_list_size: 16, |
| ..DiskAnnBuildParams::default() |
| }, |
| ); |
| index.train(&data, training_count).unwrap(); |
| index.add(&data[..indexed_count * dimension], &ids); |
| let mut bytes = Vec::new(); |
| write_diskann_index(&index, &mut PosWriter::new(&mut bytes)).unwrap(); |
| let mut reader = DiskAnnIndexReader::open(Cursor::new(bytes)).unwrap(); |
| let mut filter = RoaringTreemap::new(); |
| filter.insert(ids[0] as u64); |
| let mut filter_bytes = Vec::new(); |
| filter.serialize_into(&mut filter_bytes).unwrap(); |
| |
| reader |
| .search_with_roaring_filter(&data[..dimension], 1, 100, &filter_bytes) |
| .unwrap(); |
| |
| let stats = reader.last_search_stats(); |
| assert_eq!(stats.query_count, 1); |
| assert_eq!(stats.filtered_exhaustive_queries, 1); |
| assert_eq!(stats.filtered_graph_queries, 0); |
| assert_eq!(stats.filtered_graph_fallbacks, 0); |
| assert_eq!(stats.pq_distance_evaluations, 1); |
| assert_eq!(stats.rerank_candidate_references, 1); |
| assert_eq!(stats.rerank_unique_windows, 1); |
| assert_eq!(stats.rerank_chunks, 1); |
| assert_eq!( |
| reader.query_scratch.visited_capacity(), |
| 0, |
| "an exhaustive filtered rerank must not allocate graph visited storage" |
| ); |
| } |
| } |