1use alloc::vec::Vec;
9
10use crate::error::Error;
11
12const U64_BYTES: usize = core::mem::size_of::<u64>();
14const F32_BYTES: usize = core::mem::size_of::<f32>();
16const U32_BYTES: usize = core::mem::size_of::<u32>();
18const UUID_BYTES: usize = core::mem::size_of::<u128>();
20
21const USIZE_FIELDS: usize = 14;
23const F32_FIELDS: usize = 10;
25const U32_FIELDS: usize = 3;
27const F32S_AT: usize = USIZE_FIELDS * U64_BYTES;
29const U32S_AT: usize = F32S_AT + F32_FIELDS * F32_BYTES;
31const DB_UUID_AT: usize = U32S_AT + U32_FIELDS * U32_BYTES;
33pub const RESERVED_AT: usize = DB_UUID_AT + UUID_BYTES;
35const RESERVED_LEN: usize = 8;
37pub const ENCODED_LEN: usize = RESERVED_AT + RESERVED_LEN;
39
40#[derive(Clone, Debug, PartialEq)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47#[non_exhaustive]
48pub struct Config {
49 pub dim: usize,
51 pub max_bytes: usize,
53 pub max_text: usize,
55 pub max_blob: usize,
57 pub shards_facts: usize,
59 pub shards_entities: usize,
61 pub shards_edges: usize,
63 pub shards_temporal: usize,
65 pub shards_postings: usize,
67 pub bm25_k1: f32,
69 pub bm25_b: f32,
71 pub rrf_k: u32,
73 pub w_bm25: f32,
75 pub w_vec: f32,
77 pub w_graph: f32,
79 pub w_time: f32,
81 pub w_recency: f32,
83 pub half_life_days: u32,
85 pub graph_depth: u32,
87 pub graph_decay: f32,
89 pub similar_cos: f32,
91 pub similar_jaccard: f32,
93 pub hnsw_m: usize,
95 pub hnsw_m0: usize,
97 pub hnsw_ef_construction: usize,
99 pub hnsw_ef_search: usize,
101 pub flat_to_hnsw: usize,
103 pub db_uuid: u128,
111}
112
113impl Default for Config {
114 fn default() -> Self {
116 Self {
117 dim: 0,
118 max_bytes: 2 * 1024 * 1024 * 1024,
119 max_text: 4096,
120 max_blob: 64 * 1024,
121 shards_facts: 1024,
122 shards_entities: 256,
123 shards_edges: 512,
124 shards_temporal: 512,
125 shards_postings: 2048,
126 bm25_k1: 1.2,
127 bm25_b: 0.75,
128 rrf_k: 60,
129 w_bm25: 1.0,
130 w_vec: 1.0,
131 w_graph: 1.0,
132 w_time: 1.0,
133 w_recency: 0.25,
134 half_life_days: 180,
135 graph_depth: 2,
136 graph_decay: 0.5,
137 similar_cos: 0.85,
138 similar_jaccard: 0.5,
139 hnsw_m: 16,
140 hnsw_m0: 32,
141 hnsw_ef_construction: 200,
142 hnsw_ef_search: 64,
143 flat_to_hnsw: 24_000,
144 db_uuid: 0,
145 }
146 }
147}
148
149fn check_weight(v: f32, what: &'static str) -> Result<(), Error> {
151 if v.is_finite() && v >= 0.0 {
152 Ok(())
153 } else {
154 Err(Error::ConfigMismatch(what))
155 }
156}
157
158fn check_unit(v: f32, what: &'static str) -> Result<(), Error> {
160 if v.is_finite() && (0.0..=1.0).contains(&v) {
161 Ok(())
162 } else {
163 Err(Error::ConfigMismatch(what))
164 }
165}
166
167impl Config {
168 pub fn validate(&self) -> Result<(), Error> {
174 if self.dim > 4096 {
175 return Err(Error::ConfigMismatch("dim must be <= 4096"));
176 }
177 for (shards, what) in [
178 (self.shards_facts, "shards_facts must be a power of two"),
179 (
180 self.shards_entities,
181 "shards_entities must be a power of two",
182 ),
183 (self.shards_edges, "shards_edges must be a power of two"),
184 (
185 self.shards_temporal,
186 "shards_temporal must be a power of two",
187 ),
188 (
189 self.shards_postings,
190 "shards_postings must be a power of two",
191 ),
192 ] {
193 if !shards.is_power_of_two() {
194 return Err(Error::ConfigMismatch(what));
195 }
196 }
197 if self.max_text == 0 || self.max_text > self.max_blob {
198 return Err(Error::ConfigMismatch("max_text must be in 1..=max_blob"));
199 }
200 if self.max_blob > self.max_bytes {
201 return Err(Error::ConfigMismatch("max_blob must be <= max_bytes"));
202 }
203 if !(self.bm25_k1.is_finite() && self.bm25_k1 > 0.0) {
204 return Err(Error::ConfigMismatch("bm25_k1 must be positive"));
205 }
206 check_unit(self.bm25_b, "bm25_b must be in [0, 1]")?;
207 if self.rrf_k == 0 {
208 return Err(Error::ConfigMismatch("rrf_k must be >= 1"));
209 }
210 check_weight(self.w_bm25, "w_bm25 must be finite and >= 0")?;
211 check_weight(self.w_vec, "w_vec must be finite and >= 0")?;
212 check_weight(self.w_graph, "w_graph must be finite and >= 0")?;
213 check_weight(self.w_time, "w_time must be finite and >= 0")?;
214 check_weight(self.w_recency, "w_recency must be finite and >= 0")?;
215 if self.half_life_days == 0 {
216 return Err(Error::ConfigMismatch("half_life_days must be >= 1"));
217 }
218 if self.graph_depth > 4 {
219 return Err(Error::ConfigMismatch("graph_depth must be <= 4"));
220 }
221 if !(self.graph_decay.is_finite() && self.graph_decay > 0.0 && self.graph_decay <= 1.0) {
222 return Err(Error::ConfigMismatch("graph_decay must be in (0, 1]"));
223 }
224 check_unit(self.similar_cos, "similar_cos must be in [0, 1]")?;
225 check_unit(self.similar_jaccard, "similar_jaccard must be in [0, 1]")?;
226 if self.hnsw_m < 2 {
227 return Err(Error::ConfigMismatch("hnsw_m must be >= 2"));
228 }
229 if self.hnsw_m0 < self.hnsw_m {
230 return Err(Error::ConfigMismatch("hnsw_m0 must be >= hnsw_m"));
231 }
232 if self.hnsw_ef_construction < self.hnsw_m {
233 return Err(Error::ConfigMismatch(
234 "hnsw_ef_construction must be >= hnsw_m",
235 ));
236 }
237 if self.hnsw_ef_search == 0 {
238 return Err(Error::ConfigMismatch("hnsw_ef_search must be >= 1"));
239 }
240 if self.flat_to_hnsw == 0 {
241 return Err(Error::ConfigMismatch("flat_to_hnsw must be >= 1"));
242 }
243 Ok(())
244 }
245
246 pub fn encode(&self, out: &mut Vec<u8>) {
254 out.reserve(ENCODED_LEN);
255 for v in [
256 self.dim,
257 self.max_bytes,
258 self.max_text,
259 self.max_blob,
260 self.shards_facts,
261 self.shards_entities,
262 self.shards_edges,
263 self.shards_temporal,
264 self.shards_postings,
265 self.hnsw_m,
266 self.hnsw_m0,
267 self.hnsw_ef_construction,
268 self.hnsw_ef_search,
269 self.flat_to_hnsw,
270 ] {
271 out.extend_from_slice(&(v as u64).to_le_bytes());
272 }
273 for v in [
274 self.bm25_k1,
275 self.bm25_b,
276 self.w_bm25,
277 self.w_vec,
278 self.w_graph,
279 self.w_time,
280 self.w_recency,
281 self.graph_decay,
282 self.similar_cos,
283 self.similar_jaccard,
284 ] {
285 out.extend_from_slice(&v.to_le_bytes());
286 }
287 for v in [self.rrf_k, self.half_life_days, self.graph_depth] {
288 out.extend_from_slice(&v.to_le_bytes());
289 }
290 out.extend_from_slice(&self.db_uuid.to_le_bytes());
291 out.extend_from_slice(&[0u8; RESERVED_LEN]);
292 }
293
294 pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
308 if bytes.len() != ENCODED_LEN {
309 return Err(Error::Corrupt("config block length mismatch"));
310 }
311 let mut at = 0usize;
312 let mut take_usize = || -> Result<usize, Error> {
313 let v = u64::from_le_bytes(bytes[at..at + U64_BYTES].try_into().unwrap());
314 at += U64_BYTES;
315 usize::try_from(v)
316 .map_err(|_| Error::ConfigMismatch("database requires a 64-bit address space"))
317 };
318 let dim = take_usize()?;
319 let max_bytes = take_usize()?;
320 let max_text = take_usize()?;
321 let max_blob = take_usize()?;
322 let shards_facts = take_usize()?;
323 let shards_entities = take_usize()?;
324 let shards_edges = take_usize()?;
325 let shards_temporal = take_usize()?;
326 let shards_postings = take_usize()?;
327 let hnsw_m = take_usize()?;
328 let hnsw_m0 = take_usize()?;
329 let hnsw_ef_construction = take_usize()?;
330 let hnsw_ef_search = take_usize()?;
331 let flat_to_hnsw = take_usize()?;
332 let mut at = F32S_AT;
333 let mut take_f32 = || {
334 let v = f32::from_le_bytes(bytes[at..at + F32_BYTES].try_into().unwrap());
335 at += F32_BYTES;
336 v
337 };
338 let bm25_k1 = take_f32();
339 let bm25_b = take_f32();
340 let w_bm25 = take_f32();
341 let w_vec = take_f32();
342 let w_graph = take_f32();
343 let w_time = take_f32();
344 let w_recency = take_f32();
345 let graph_decay = take_f32();
346 let similar_cos = take_f32();
347 let similar_jaccard = take_f32();
348 let mut at = U32S_AT;
349 let mut take_u32 = || {
350 let v = u32::from_le_bytes(bytes[at..at + U32_BYTES].try_into().unwrap());
351 at += U32_BYTES;
352 v
353 };
354 let rrf_k = take_u32();
355 let half_life_days = take_u32();
356 let graph_depth = take_u32();
357 let db_uuid = u128::from_le_bytes(bytes[DB_UUID_AT..RESERVED_AT].try_into().unwrap());
358 if bytes[RESERVED_AT..ENCODED_LEN] != [0u8; RESERVED_LEN] {
359 return Err(Error::Corrupt("reserved config bytes must be zero"));
360 }
361 let cfg = Self {
362 dim,
363 max_bytes,
364 max_text,
365 max_blob,
366 shards_facts,
367 shards_entities,
368 shards_edges,
369 shards_temporal,
370 shards_postings,
371 bm25_k1,
372 bm25_b,
373 rrf_k,
374 w_bm25,
375 w_vec,
376 w_graph,
377 w_time,
378 w_recency,
379 half_life_days,
380 graph_depth,
381 graph_decay,
382 similar_cos,
383 similar_jaccard,
384 hnsw_m,
385 hnsw_m0,
386 hnsw_ef_construction,
387 hnsw_ef_search,
388 flat_to_hnsw,
389 db_uuid,
390 };
391 cfg.validate()?;
392 Ok(cfg)
393 }
394}