1use crate::{HawDBError, PropertyId, Result, TableId, VectorDimension, VectorDistance};
10
11mod codec;
12
13pub const MAX_VECTOR_INDEX_DESCRIPTOR_BYTES: usize = 4096;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct VectorIndexReadIdentity {
20 pub database: crate::Uuid,
21 pub branch: crate::Uuid,
22 pub consumer_id_hash: [u8; 32],
23 pub target_locator_hash: [u8; 32],
24 pub registration: crate::Uuid,
25 pub projection: crate::Uuid,
26 pub checkpoint: crate::Uuid,
27 pub target_incarnation: std::num::NonZeroU64,
28 pub generation: std::num::NonZeroU64,
29 pub operation: std::num::NonZeroU64,
30 pub source_epoch: u64,
31}
32
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
35pub enum VectorIndexQueryMode {
36 #[default]
38 AnnWindow,
39 Exact,
41 FilteredAnn,
43}
44
45impl VectorIndexQueryMode {
46 pub fn parse(value: &str) -> Result<Self> {
47 match value {
48 "ann_window" => Ok(Self::AnnWindow),
49 "exact" => Ok(Self::Exact),
50 "filtered_ann" => Ok(Self::FilteredAnn),
51 _ => Err(invalid(
52 "query mode must be 'ann_window', 'exact' or 'filtered_ann'",
53 )),
54 }
55 }
56
57 pub const fn as_str(self) -> &'static str {
58 match self {
59 Self::AnnWindow => "ann_window",
60 Self::Exact => "exact",
61 Self::FilteredAnn => "filtered_ann",
62 }
63 }
64
65 pub const fn requires_eligible_domain(self) -> bool {
66 matches!(self, Self::Exact | Self::FilteredAnn)
67 }
68
69 pub const fn filter_position(self) -> &'static str {
70 match self {
71 Self::AnnWindow => "after_window",
72 Self::Exact => "before_ranking",
73 Self::FilteredAnn => "before_candidates",
74 }
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum VectorIndexTarget {
83 Relational {
84 table: String,
85 column: String,
86 column_ordinal: u32,
87 },
88 NodeProperty {
89 table: String,
90 property: String,
91 table_id: TableId,
92 property_id: PropertyId,
93 },
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum VectorIndexBackend {
100 RaBitQ {
101 bit_width: u8,
102 segment_rows: u32,
103 transform_seed: u64,
104 },
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct VectorIndexDescriptor {
109 name: String,
110 target: VectorIndexTarget,
111 dimension: VectorDimension,
112 distance: VectorDistance,
113 backend: VectorIndexBackend,
114 schema_digest: [u8; 32],
115}
116
117impl VectorIndexDescriptor {
118 #[doc(hidden)]
119 pub fn retained_capacity_bytes(&self) -> usize {
120 let (table, field) = match &self.target {
121 VectorIndexTarget::Relational { table, column, .. } => (table, column),
122 VectorIndexTarget::NodeProperty {
123 table, property, ..
124 } => (table, property),
125 };
126 std::mem::size_of::<Self>()
127 .saturating_add(self.name.capacity())
128 .saturating_add(table.capacity())
129 .saturating_add(field.capacity())
130 }
131
132 pub fn new(
135 name: String,
136 target: VectorIndexTarget,
137 dimension: VectorDimension,
138 distance: VectorDistance,
139 backend: VectorIndexBackend,
140 schema_digest: [u8; 32],
141 ) -> Result<Self> {
142 if name.is_empty()
143 || name.len() > 128
144 || !name
145 .bytes()
146 .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
147 {
148 return Err(invalid(
149 "name must be 1..128 ASCII letters, digits, '.', '_' or '-'",
150 ));
151 }
152 let (table, field) = match &target {
153 VectorIndexTarget::Relational { table, column, .. } => (table, column),
154 VectorIndexTarget::NodeProperty {
155 table, property, ..
156 } => (table, property),
157 };
158 for name in [table, field] {
159 if name.is_empty() || name.len() > 1024 || name.contains('\0') {
160 return Err(invalid(
161 "target names must contain 1..1024 non-NUL UTF-8 bytes",
162 ));
163 }
164 }
165 let VectorIndexBackend::RaBitQ {
166 bit_width,
167 segment_rows,
168 ..
169 } = backend;
170 if !matches!(bit_width, 1 | 4) || segment_rows == 0 {
171 return Err(invalid(
172 "RaBitQ requires 1 or 4 bits and a positive segment size",
173 ));
174 }
175 if distance != VectorDistance::Cosine {
176 return Err(invalid(
177 "RaBitQ indexes currently support cosine distance only",
178 ));
179 }
180 Ok(Self {
181 name,
182 target,
183 dimension,
184 distance,
185 backend,
186 schema_digest,
187 })
188 }
189
190 pub fn name(&self) -> &str {
191 &self.name
192 }
193
194 pub fn target(&self) -> &VectorIndexTarget {
195 &self.target
196 }
197
198 pub const fn dimension(&self) -> VectorDimension {
199 self.dimension
200 }
201
202 pub const fn distance(&self) -> VectorDistance {
203 self.distance
204 }
205
206 pub const fn backend(&self) -> VectorIndexBackend {
207 self.backend
208 }
209
210 pub const fn schema_digest(&self) -> &[u8; 32] {
211 &self.schema_digest
212 }
213
214 pub fn retained_bytes(&self) -> usize {
215 let (table, field) = match &self.target {
216 VectorIndexTarget::Relational { table, column, .. } => (table, column),
217 VectorIndexTarget::NodeProperty {
218 table, property, ..
219 } => (table, property),
220 };
221 std::mem::size_of::<Self>() + self.name.capacity() + table.capacity() + field.capacity()
222 }
223}
224
225fn invalid(reason: &str) -> HawDBError {
226 HawDBError::Semantic(format!("invalid vector index: {reason}"))
227}