weavatrix_search_vector/
graph.rs1use crate::error::SearchError;
2use crate::hit::SearchHit;
3use crate::hnsw::VectorIndex;
4use crate::parallel;
5
6#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct NeighborEdge {
9 pub source: u64,
10 pub target: u64,
11 pub distance: f32,
12}
13
14#[derive(Debug, Clone, PartialEq)]
19pub struct KnnGraph {
20 nodes: Vec<u64>,
21 offsets: Vec<usize>,
22 edges: Vec<NeighborEdge>,
23}
24
25impl KnnGraph {
26 pub fn build(index: &VectorIndex, neighbors: usize) -> Result<Self, SearchError> {
32 Self::build_with(index, neighbors, false)
33 }
34
35 pub fn build_exact(index: &VectorIndex, neighbors: usize) -> Result<Self, SearchError> {
41 Self::build_with(index, neighbors, true)
42 }
43
44 #[must_use]
45 pub fn len(&self) -> usize {
46 self.nodes.len()
47 }
48
49 #[must_use]
50 pub fn is_empty(&self) -> bool {
51 self.nodes.is_empty()
52 }
53
54 #[must_use]
55 pub fn edge_count(&self) -> usize {
56 self.edges.len()
57 }
58
59 #[must_use]
60 pub fn nodes(&self) -> impl ExactSizeIterator<Item = u64> + '_ {
61 self.nodes.iter().copied()
62 }
63
64 #[must_use]
65 pub fn neighbors(&self, source: u64) -> Option<&[NeighborEdge]> {
66 let index = self.nodes.binary_search(&source).ok()?;
67 Some(&self.edges[self.offsets[index]..self.offsets[index + 1]])
68 }
69
70 #[must_use]
71 pub fn edges(&self) -> impl ExactSizeIterator<Item = NeighborEdge> + '_ {
72 self.edges.iter().copied()
73 }
74
75 fn build_with(index: &VectorIndex, neighbors: usize, exact: bool) -> Result<Self, SearchError> {
76 let nodes = index.keys().collect::<Vec<_>>();
77 let per_node = neighbors.min(index.len().saturating_sub(1));
78 let queries = nodes
79 .iter()
80 .map(|key| {
81 index
82 .vector(*key)
83 .expect("index key iterator resolves to a vector")
84 })
85 .collect::<Vec<_>>();
86 let rows = if exact {
87 parallel::search_batch(&queries, index.config().query_threads, |query| {
88 index.search_exact(query, per_node.saturating_add(1))
89 })?
90 } else {
91 index.search_batch(&queries, per_node.saturating_add(1))?
92 };
93 Self::from_rows(nodes, rows, per_node)
94 }
95
96 fn from_rows(
97 nodes: Vec<u64>,
98 rows: Vec<Vec<SearchHit>>,
99 per_node: usize,
100 ) -> Result<Self, SearchError> {
101 let capacity = nodes
102 .len()
103 .checked_mul(per_node)
104 .ok_or(SearchError::CapacityOverflow)?;
105 let mut offsets = Vec::new();
106 offsets
107 .try_reserve_exact(nodes.len().saturating_add(1))
108 .map_err(|_| SearchError::AllocationFailed)?;
109 let mut edges = Vec::new();
110 edges
111 .try_reserve_exact(capacity)
112 .map_err(|_| SearchError::AllocationFailed)?;
113 offsets.push(0);
114 for (source, row) in nodes.iter().copied().zip(rows) {
115 edges.extend(
116 row.into_iter()
117 .filter(|hit| hit.key != source)
118 .take(per_node)
119 .map(|hit| NeighborEdge {
120 source,
121 target: hit.key,
122 distance: hit.distance,
123 }),
124 );
125 offsets.push(edges.len());
126 }
127 Ok(Self {
128 nodes,
129 offsets,
130 edges,
131 })
132 }
133}