1use async_trait::async_trait;
8use common::{DakeraError, NamespaceId, Result, Vector, VectorId};
9use rocksdb::{BlockBasedOptions, Cache, Options, DB};
10use std::sync::Arc;
11
12use crate::traits::VectorStorage;
13
14#[derive(Debug, Clone)]
16pub struct RocksDbStorageConfig {
17 pub path: String,
19 pub block_cache_mb: usize,
21 pub compression: bool,
23 pub write_buffer_size: usize,
25 pub max_write_buffer_number: i32,
27}
28
29impl Default for RocksDbStorageConfig {
30 fn default() -> Self {
31 Self {
32 path: "/data/hot".to_string(),
33 block_cache_mb: 64,
34 compression: true,
35 write_buffer_size: 64 * 1024 * 1024,
36 max_write_buffer_number: 3,
37 }
38 }
39}
40
41pub struct RocksDbStorage {
46 db: Arc<DB>,
47}
48
49impl RocksDbStorage {
50 pub fn new(config: RocksDbStorageConfig) -> Result<Self> {
52 Self::open_at(&config.path, &config)
53 }
54
55 pub fn open_at(path: &str, config: &RocksDbStorageConfig) -> Result<Self> {
57 let mut opts = Options::default();
58 opts.create_if_missing(true);
59 opts.set_write_buffer_size(config.write_buffer_size);
60 opts.set_max_write_buffer_number(config.max_write_buffer_number);
61
62 if config.compression {
63 opts.set_compression_type(rocksdb::DBCompressionType::Lz4);
64 }
65
66 let cache = Cache::new_lru_cache(config.block_cache_mb * 1024 * 1024);
67 let mut block_opts = BlockBasedOptions::default();
68 block_opts.set_block_cache(&cache);
69 block_opts.set_optimize_filters_for_memory(true);
70 opts.set_block_based_table_factory(&block_opts);
71
72 opts.set_level_compaction_dynamic_level_bytes(true);
73 opts.set_max_background_jobs(4);
74
75 opts.set_prefix_extractor(rocksdb::SliceTransform::create_fixed_prefix(
77 3,
81 ));
82
83 let db = DB::open(&opts, path)
84 .map_err(|e| DakeraError::Storage(format!("RocksDB open failed: {e}")))?;
85
86 tracing::info!(path, "RocksDB hot tier initialized");
87
88 Ok(Self { db: Arc::new(db) })
89 }
90
91 fn vector_key(ns: &NamespaceId, id: &VectorId) -> Vec<u8> {
94 format!("ns:{ns}:v:{id}").into_bytes()
95 }
96
97 fn vector_prefix(ns: &NamespaceId) -> Vec<u8> {
98 format!("ns:{ns}:v:").into_bytes()
99 }
100
101 fn meta_key(ns: &NamespaceId) -> Vec<u8> {
102 format!("ns:{ns}:__meta__").into_bytes()
103 }
104
105 fn ns_prefix() -> Vec<u8> {
106 b"ns:".to_vec()
107 }
108
109 fn serialize_vector(v: &Vector) -> Result<Vec<u8>> {
112 serde_json::to_vec(v).map_err(|e| DakeraError::Storage(format!("vector serialize: {e}")))
113 }
114
115 fn deserialize_vector(bytes: &[u8]) -> Result<Vector> {
116 serde_json::from_slice(bytes)
117 .map_err(|e| DakeraError::Storage(format!("vector deserialize: {e}")))
118 }
119}
120
121fn now_secs() -> u64 {
122 std::time::SystemTime::now()
123 .duration_since(std::time::UNIX_EPOCH)
124 .unwrap_or_default()
125 .as_secs()
126}
127
128#[async_trait]
129impl VectorStorage for RocksDbStorage {
130 async fn upsert(&self, namespace: &NamespaceId, vectors: Vec<Vector>) -> Result<usize> {
131 if vectors.is_empty() {
132 return Ok(0);
133 }
134
135 let now = now_secs();
138 let prefix = Self::vector_prefix(namespace);
139 let expected_dim = {
140 let iter = self.db.prefix_iterator(&prefix);
141 let mut dim = None;
142 for item in iter {
143 match item {
144 Ok((k, v)) if k.starts_with(&prefix) => {
145 if let Ok(vec) = Self::deserialize_vector(&v) {
146 if !vec.is_expired_at(now) {
147 dim = Some(vec.values.len());
148 break;
149 }
150 }
151 }
152 _ => break,
153 }
154 }
155 dim.unwrap_or_else(|| vectors[0].values.len())
156 };
157
158 for v in &vectors {
159 if v.values.len() != expected_dim {
160 return Err(DakeraError::DimensionMismatch {
161 expected: expected_dim,
162 actual: v.values.len(),
163 });
164 }
165 }
166
167 let mut batch = rocksdb::WriteBatch::default();
168
169 batch.put(Self::meta_key(namespace), b"1");
171
172 let count = vectors.len();
173 for mut vector in vectors {
174 vector.apply_ttl();
175 let key = Self::vector_key(namespace, &vector.id);
176 let val = Self::serialize_vector(&vector)?;
177 batch.put(key, val);
178 }
179
180 self.db
181 .write(batch)
182 .map_err(|e| DakeraError::Storage(format!("RocksDB batch write: {e}")))?;
183
184 tracing::debug!(namespace = %namespace, count, "RocksDB upserted vectors");
185 Ok(count)
186 }
187
188 async fn get(&self, namespace: &NamespaceId, ids: &[VectorId]) -> Result<Vec<Vector>> {
189 if !self.namespace_exists(namespace).await? {
190 return Err(DakeraError::NamespaceNotFound(namespace.clone()));
191 }
192
193 let now = now_secs();
194 let keys: Vec<Vec<u8>> = ids
195 .iter()
196 .map(|id| Self::vector_key(namespace, id))
197 .collect();
198
199 let results = self.db.multi_get(&keys);
200 let mut out = Vec::with_capacity(ids.len());
201 for result in results {
202 if let Ok(Some(bytes)) = result {
203 if let Ok(v) = Self::deserialize_vector(&bytes) {
204 if !v.is_expired_at(now) {
205 out.push(v);
206 }
207 }
208 }
209 }
210 Ok(out)
211 }
212
213 async fn get_all(&self, namespace: &NamespaceId) -> Result<Vec<Vector>> {
214 if !self.namespace_exists(namespace).await? {
215 return Err(DakeraError::NamespaceNotFound(namespace.clone()));
216 }
217
218 let prefix = Self::vector_prefix(namespace);
219 let now = now_secs();
220 let mut out = Vec::new();
221
222 let iter = self.db.prefix_iterator(&prefix);
223 for item in iter {
224 match item {
225 Ok((k, v)) if k.starts_with(&prefix) => {
226 if let Ok(vec) = Self::deserialize_vector(&v) {
227 if !vec.is_expired_at(now) {
228 out.push(vec);
229 }
230 }
231 }
232 _ => break,
233 }
234 }
235 Ok(out)
236 }
237
238 async fn delete(&self, namespace: &NamespaceId, ids: &[VectorId]) -> Result<usize> {
239 if !self.namespace_exists(namespace).await? {
240 return Err(DakeraError::NamespaceNotFound(namespace.clone()));
241 }
242
243 let mut batch = rocksdb::WriteBatch::default();
244 let mut deleted = 0usize;
245
246 for id in ids {
247 let key = Self::vector_key(namespace, id);
248 if self.db.get(&key).ok().flatten().is_some() {
249 batch.delete(&key);
250 deleted += 1;
251 }
252 }
253
254 if deleted > 0 {
255 self.db
256 .write(batch)
257 .map_err(|e| DakeraError::Storage(format!("RocksDB batch delete: {e}")))?;
258 }
259
260 tracing::debug!(namespace = %namespace, deleted, "RocksDB deleted vectors");
261 Ok(deleted)
262 }
263
264 async fn namespace_exists(&self, namespace: &NamespaceId) -> Result<bool> {
265 Ok(self
266 .db
267 .get(Self::meta_key(namespace))
268 .ok()
269 .flatten()
270 .is_some())
271 }
272
273 async fn ensure_namespace(&self, namespace: &NamespaceId) -> Result<()> {
274 self.db
275 .put(Self::meta_key(namespace), b"1")
276 .map_err(|e| DakeraError::Storage(format!("RocksDB put meta: {e}")))?;
277 Ok(())
278 }
279
280 async fn count(&self, namespace: &NamespaceId) -> Result<usize> {
281 let prefix = Self::vector_prefix(namespace);
282 let now = now_secs();
283 let mut n = 0usize;
284 let iter = self.db.prefix_iterator(&prefix);
285 for item in iter {
286 match item {
287 Ok((k, v)) if k.starts_with(&prefix) => {
288 if let Ok(vec) = Self::deserialize_vector(&v) {
289 if !vec.is_expired_at(now) {
290 n += 1;
291 }
292 }
293 }
294 _ => break,
295 }
296 }
297 Ok(n)
298 }
299
300 async fn dimension(&self, namespace: &NamespaceId) -> Result<Option<usize>> {
301 let prefix = Self::vector_prefix(namespace);
302 let now = now_secs();
303 let iter = self.db.prefix_iterator(&prefix);
304 for item in iter {
305 match item {
306 Ok((k, v)) if k.starts_with(&prefix) => {
307 if let Ok(vec) = Self::deserialize_vector(&v) {
308 if !vec.is_expired_at(now) {
309 return Ok(Some(vec.values.len()));
310 }
311 }
312 }
313 _ => break,
314 }
315 }
316 Ok(None)
317 }
318
319 async fn list_namespaces(&self) -> Result<Vec<NamespaceId>> {
320 let ns_prefix = Self::ns_prefix();
321 let mut namespaces = Vec::new();
322
323 let iter = self.db.prefix_iterator(&ns_prefix);
324 for item in iter {
325 match item {
326 Ok((k, _)) if k.starts_with(&ns_prefix) => {
327 if let Ok(key_str) = std::str::from_utf8(&k) {
328 if key_str.ends_with(":__meta__") {
329 if let Some(name) = key_str
331 .strip_prefix("ns:")
332 .and_then(|s| s.strip_suffix(":__meta__"))
333 {
334 namespaces.push(name.to_string());
335 }
336 }
337 }
338 }
339 Ok((k, _)) if !k.starts_with(&ns_prefix) => break,
340 Err(_) => break,
341 _ => {}
342 }
343 }
344 namespaces.sort_unstable();
348 namespaces.dedup();
349 Ok(namespaces)
350 }
351
352 async fn delete_namespace(&self, namespace: &NamespaceId) -> Result<bool> {
353 let existed = self.namespace_exists(namespace).await?;
354 if !existed {
355 return Ok(false);
356 }
357
358 let mut batch = rocksdb::WriteBatch::default();
359
360 let prefix = Self::vector_prefix(namespace);
362 let iter = self.db.prefix_iterator(&prefix);
363 for item in iter {
364 match item {
365 Ok((k, _)) if k.starts_with(&prefix) => batch.delete(&k),
366 _ => break,
367 }
368 }
369
370 batch.delete(Self::meta_key(namespace));
372
373 self.db
374 .write(batch)
375 .map_err(|e| DakeraError::Storage(format!("RocksDB delete namespace: {e}")))?;
376
377 tracing::debug!(namespace = %namespace, "RocksDB deleted namespace");
378 Ok(true)
379 }
380
381 async fn cleanup_expired(&self, namespace: &NamespaceId) -> Result<usize> {
382 let prefix = Self::vector_prefix(namespace);
383 let now = now_secs();
384 let mut batch = rocksdb::WriteBatch::default();
385 let mut removed = 0usize;
386
387 let iter = self.db.prefix_iterator(&prefix);
388 for item in iter {
389 match item {
390 Ok((k, v)) if k.starts_with(&prefix) => {
391 if let Ok(vec) = Self::deserialize_vector(&v) {
392 if vec.is_expired_at(now) {
393 batch.delete(&k);
394 removed += 1;
395 }
396 }
397 }
398 _ => break,
399 }
400 }
401
402 if removed > 0 {
403 self.db
404 .write(batch)
405 .map_err(|e| DakeraError::Storage(format!("RocksDB cleanup expired: {e}")))?;
406 tracing::debug!(namespace = %namespace, removed, "RocksDB cleaned up expired vectors");
407 }
408 Ok(removed)
409 }
410
411 async fn cleanup_all_expired(&self) -> Result<usize> {
412 let namespaces = self.list_namespaces().await?;
413 let mut total = 0usize;
414 for ns in &namespaces {
415 total += self.cleanup_expired(ns).await?;
416 }
417 if total > 0 {
418 tracing::info!(
419 total,
420 "RocksDB cleaned up expired vectors across all namespaces"
421 );
422 }
423 Ok(total)
424 }
425
426 async fn get_page(
427 &self,
428 namespace: &NamespaceId,
429 offset: usize,
430 limit: usize,
431 ) -> Result<Vec<Vector>> {
432 let prefix = Self::vector_prefix(namespace);
433 let now = now_secs();
434 let mut out = Vec::with_capacity(limit);
435 let mut skipped = 0usize;
436
437 let iter = self.db.prefix_iterator(&prefix);
438 for item in iter {
439 match item {
440 Ok((k, v)) if k.starts_with(&prefix) => {
441 if let Ok(vec) = Self::deserialize_vector(&v) {
442 if !vec.is_expired_at(now) {
443 if skipped < offset {
444 skipped += 1;
445 } else {
446 out.push(vec);
447 if out.len() >= limit {
448 break;
449 }
450 }
451 }
452 }
453 }
454 _ => break,
455 }
456 }
457 Ok(out)
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use tempfile::TempDir;
465
466 fn test_config(dir: &TempDir) -> RocksDbStorageConfig {
467 RocksDbStorageConfig {
468 path: dir.path().to_string_lossy().to_string(),
469 block_cache_mb: 8,
470 compression: false,
471 ..Default::default()
472 }
473 }
474
475 #[tokio::test]
476 async fn test_upsert_and_get() {
477 let dir = TempDir::new().unwrap();
478 let storage = RocksDbStorage::new(test_config(&dir)).unwrap();
479 let ns = "test".to_string();
480
481 storage.ensure_namespace(&ns).await.unwrap();
482 let count = storage
483 .upsert(
484 &ns,
485 vec![Vector {
486 id: "v1".to_string(),
487 values: vec![1.0, 2.0, 3.0],
488 metadata: None,
489 ttl_seconds: None,
490 expires_at: None,
491 }],
492 )
493 .await
494 .unwrap();
495 assert_eq!(count, 1);
496
497 let got = storage.get(&ns, &["v1".to_string()]).await.unwrap();
498 assert_eq!(got.len(), 1);
499 assert_eq!(got[0].id, "v1");
500 assert_eq!(got[0].values, vec![1.0, 2.0, 3.0]);
501 }
502
503 #[tokio::test]
504 async fn test_dimension_mismatch() {
505 let dir = TempDir::new().unwrap();
506 let storage = RocksDbStorage::new(test_config(&dir)).unwrap();
507 let ns = "test".to_string();
508
509 storage.ensure_namespace(&ns).await.unwrap();
510 storage
511 .upsert(
512 &ns,
513 vec![Vector {
514 id: "v1".to_string(),
515 values: vec![1.0, 2.0, 3.0],
516 metadata: None,
517 ttl_seconds: None,
518 expires_at: None,
519 }],
520 )
521 .await
522 .unwrap();
523
524 let result = storage
525 .upsert(
526 &ns,
527 vec![Vector {
528 id: "v2".to_string(),
529 values: vec![1.0, 2.0],
530 metadata: None,
531 ttl_seconds: None,
532 expires_at: None,
533 }],
534 )
535 .await;
536 assert!(matches!(result, Err(DakeraError::DimensionMismatch { .. })));
537 }
538
539 #[tokio::test]
540 async fn test_delete() {
541 let dir = TempDir::new().unwrap();
542 let storage = RocksDbStorage::new(test_config(&dir)).unwrap();
543 let ns = "test".to_string();
544
545 storage.ensure_namespace(&ns).await.unwrap();
546 storage
547 .upsert(
548 &ns,
549 vec![
550 Vector {
551 id: "v1".to_string(),
552 values: vec![1.0],
553 metadata: None,
554 ttl_seconds: None,
555 expires_at: None,
556 },
557 Vector {
558 id: "v2".to_string(),
559 values: vec![2.0],
560 metadata: None,
561 ttl_seconds: None,
562 expires_at: None,
563 },
564 ],
565 )
566 .await
567 .unwrap();
568
569 let deleted = storage.delete(&ns, &["v1".to_string()]).await.unwrap();
570 assert_eq!(deleted, 1);
571 assert_eq!(storage.count(&ns).await.unwrap(), 1);
572 }
573
574 #[tokio::test]
575 async fn test_get_all_and_count() {
576 let dir = TempDir::new().unwrap();
577 let storage = RocksDbStorage::new(test_config(&dir)).unwrap();
578 let ns = "test".to_string();
579
580 storage.ensure_namespace(&ns).await.unwrap();
581 storage
582 .upsert(
583 &ns,
584 vec![
585 Vector {
586 id: "v1".to_string(),
587 values: vec![1.0, 2.0],
588 metadata: None,
589 ttl_seconds: None,
590 expires_at: None,
591 },
592 Vector {
593 id: "v2".to_string(),
594 values: vec![3.0, 4.0],
595 metadata: None,
596 ttl_seconds: None,
597 expires_at: None,
598 },
599 ],
600 )
601 .await
602 .unwrap();
603
604 let all = storage.get_all(&ns).await.unwrap();
605 assert_eq!(all.len(), 2);
606 assert_eq!(storage.count(&ns).await.unwrap(), 2);
607 }
608
609 #[tokio::test]
610 async fn test_namespace_lifecycle() {
611 let dir = TempDir::new().unwrap();
612 let storage = RocksDbStorage::new(test_config(&dir)).unwrap();
613 let ns = "lifecycle".to_string();
614
615 assert!(!storage.namespace_exists(&ns).await.unwrap());
616 storage.ensure_namespace(&ns).await.unwrap();
617 assert!(storage.namespace_exists(&ns).await.unwrap());
618
619 let nss = storage.list_namespaces().await.unwrap();
620 assert!(nss.contains(&ns));
621
622 storage.delete_namespace(&ns).await.unwrap();
623 assert!(!storage.namespace_exists(&ns).await.unwrap());
624 }
625
626 #[tokio::test]
627 async fn test_ttl_filtering() {
628 let dir = TempDir::new().unwrap();
629 let storage = RocksDbStorage::new(test_config(&dir)).unwrap();
630 let ns = "ttl".to_string();
631
632 storage.ensure_namespace(&ns).await.unwrap();
633 let past = now_secs().saturating_sub(100);
634
635 storage
636 .upsert(
637 &ns,
638 vec![
639 Vector {
640 id: "expired".to_string(),
641 values: vec![1.0, 2.0],
642 metadata: None,
643 ttl_seconds: None,
644 expires_at: Some(past),
645 },
646 Vector {
647 id: "valid".to_string(),
648 values: vec![3.0, 4.0],
649 metadata: None,
650 ttl_seconds: None,
651 expires_at: None,
652 },
653 ],
654 )
655 .await
656 .unwrap();
657
658 let got = storage
659 .get(&ns, &["expired".to_string(), "valid".to_string()])
660 .await
661 .unwrap();
662 assert_eq!(got.len(), 1);
663 assert_eq!(got[0].id, "valid");
664
665 let all = storage.get_all(&ns).await.unwrap();
666 assert_eq!(all.len(), 1);
667 assert_eq!(storage.count(&ns).await.unwrap(), 1);
668 }
669
670 #[tokio::test]
671 async fn test_cleanup_expired() {
672 let dir = TempDir::new().unwrap();
673 let storage = RocksDbStorage::new(test_config(&dir)).unwrap();
674 let ns = "cleanup".to_string();
675
676 storage.ensure_namespace(&ns).await.unwrap();
677 let past = now_secs().saturating_sub(100);
678
679 storage
680 .upsert(
681 &ns,
682 vec![
683 Vector {
684 id: "expired1".to_string(),
685 values: vec![1.0],
686 metadata: None,
687 ttl_seconds: None,
688 expires_at: Some(past),
689 },
690 Vector {
691 id: "valid".to_string(),
692 values: vec![2.0],
693 metadata: None,
694 ttl_seconds: None,
695 expires_at: None,
696 },
697 ],
698 )
699 .await
700 .unwrap();
701
702 let removed = storage.cleanup_expired(&ns).await.unwrap();
703 assert_eq!(removed, 1);
704
705 let all = storage.get_all(&ns).await.unwrap();
707 assert_eq!(all.len(), 1);
708 assert_eq!(all[0].id, "valid");
709 }
710
711 #[tokio::test]
712 async fn test_get_page() {
713 let dir = TempDir::new().unwrap();
714 let storage = RocksDbStorage::new(test_config(&dir)).unwrap();
715 let ns = "paged".to_string();
716
717 storage.ensure_namespace(&ns).await.unwrap();
718 let mut vecs = Vec::new();
719 for i in 0..10 {
720 vecs.push(Vector {
721 id: format!("v{i:02}"),
722 values: vec![i as f32],
723 metadata: None,
724 ttl_seconds: None,
725 expires_at: None,
726 });
727 }
728 storage.upsert(&ns, vecs).await.unwrap();
729
730 let page = storage.get_page(&ns, 3, 4).await.unwrap();
731 assert_eq!(page.len(), 4);
732 }
733
734 #[tokio::test]
735 async fn test_persistence_across_reopen() {
736 let dir = TempDir::new().unwrap();
737
738 {
740 let storage = RocksDbStorage::new(test_config(&dir)).unwrap();
741 let ns = "persist".to_string();
742 storage.ensure_namespace(&ns).await.unwrap();
743 storage
744 .upsert(
745 &ns,
746 vec![Vector {
747 id: "durable".to_string(),
748 values: vec![42.0],
749 metadata: None,
750 ttl_seconds: None,
751 expires_at: None,
752 }],
753 )
754 .await
755 .unwrap();
756 }
757
758 {
760 let storage = RocksDbStorage::new(test_config(&dir)).unwrap();
761 let ns = "persist".to_string();
762 assert!(storage.namespace_exists(&ns).await.unwrap());
763 let got = storage.get(&ns, &["durable".to_string()]).await.unwrap();
764 assert_eq!(got.len(), 1);
765 assert_eq!(got[0].values, vec![42.0]);
766 }
767 }
768
769 #[tokio::test]
770 async fn test_multi_namespace_isolation() {
771 let dir = TempDir::new().unwrap();
772 let storage = RocksDbStorage::new(test_config(&dir)).unwrap();
773
774 storage.ensure_namespace(&"ns1".to_string()).await.unwrap();
775 storage.ensure_namespace(&"ns2".to_string()).await.unwrap();
776
777 storage
778 .upsert(
779 &"ns1".to_string(),
780 vec![Vector {
781 id: "v1".to_string(),
782 values: vec![1.0],
783 metadata: None,
784 ttl_seconds: None,
785 expires_at: None,
786 }],
787 )
788 .await
789 .unwrap();
790
791 storage
792 .upsert(
793 &"ns2".to_string(),
794 vec![Vector {
795 id: "v1".to_string(),
796 values: vec![2.0],
797 metadata: None,
798 ttl_seconds: None,
799 expires_at: None,
800 }],
801 )
802 .await
803 .unwrap();
804
805 let ns1_vecs = storage.get_all(&"ns1".to_string()).await.unwrap();
806 let ns2_vecs = storage.get_all(&"ns2".to_string()).await.unwrap();
807 assert_eq!(ns1_vecs.len(), 1);
808 assert_eq!(ns2_vecs.len(), 1);
809 assert_eq!(ns1_vecs[0].values, vec![1.0]);
810 assert_eq!(ns2_vecs[0].values, vec![2.0]);
811
812 storage.delete_namespace(&"ns1".to_string()).await.unwrap();
814 assert!(!storage.namespace_exists(&"ns1".to_string()).await.unwrap());
815 assert!(storage.namespace_exists(&"ns2".to_string()).await.unwrap());
816 assert_eq!(storage.get_all(&"ns2".to_string()).await.unwrap().len(), 1);
817 }
818}