Skip to main content

vecdb_core/storage/
mmap.rs

1use std::fs::{File, OpenOptions};
2use std::path::{Path, PathBuf};
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use memmap2::MmapMut;
6
7use crate::errors::{Result, VecDbError};
8use crate::types::Vector;
9
10const MAGIC: u32 = 0x5645_4344;
11const VERSION: u32 = 1;
12const HEADER_SIZE: usize = 64;
13
14fn now_unix() -> u64 {
15    SystemTime::now()
16        .duration_since(UNIX_EPOCH)
17        .unwrap_or_default()
18        .as_secs()
19}
20
21pub struct MmapVectorStore {
22    pub path: PathBuf,
23    pub dimension: usize,
24    pub count: usize,
25    pub capacity: usize,
26    mmap: MmapMut,
27    file: File,
28}
29
30impl MmapVectorStore {
31    pub fn create(path: &Path, dimension: usize, initial_capacity: usize) -> Result<Self> {
32        let file = OpenOptions::new()
33            .read(true)
34            .write(true)
35            .create_new(true)
36            .open(path)
37            .map_err(|e| VecDbError::StorageError(format!("create failed: {e}")))?;
38
39        let total_size = (HEADER_SIZE + initial_capacity * dimension * 4) as u64;
40        file.set_len(total_size)
41            .map_err(|e| VecDbError::StorageError(format!("set_len failed: {e}")))?;
42
43        let mut mmap = unsafe {
44            MmapMut::map_mut(&file)
45                .map_err(|e| VecDbError::StorageError(format!("mmap failed: {e}")))?
46        };
47
48        let now = now_unix();
49        mmap[0..4].copy_from_slice(&MAGIC.to_le_bytes());
50        mmap[4..8].copy_from_slice(&VERSION.to_le_bytes());
51        mmap[8..16].copy_from_slice(&(dimension as u64).to_le_bytes());
52        mmap[16..24].copy_from_slice(&0u64.to_le_bytes());
53        mmap[24..32].copy_from_slice(&(initial_capacity as u64).to_le_bytes());
54        mmap[32..40].copy_from_slice(&now.to_le_bytes());
55        mmap[40..48].copy_from_slice(&now.to_le_bytes());
56        // bytes 48..64 reserved — zeroed by set_len
57
58        mmap.flush()
59            .map_err(|e| VecDbError::StorageError(format!("flush failed: {e}")))?;
60
61        Ok(Self {
62            path: path.to_path_buf(),
63            dimension,
64            count: 0,
65            capacity: initial_capacity,
66            mmap,
67            file,
68        })
69    }
70
71    pub fn open(path: &Path) -> Result<Self> {
72        let file = OpenOptions::new()
73            .read(true)
74            .write(true)
75            .open(path)
76            .map_err(|e| VecDbError::StorageError(format!("open failed: {e}")))?;
77
78        let mmap = unsafe {
79            MmapMut::map_mut(&file)
80                .map_err(|e| VecDbError::StorageError(format!("mmap failed: {e}")))?
81        };
82
83        // Performance: on Linux, tell the kernel to pre-fault pages into the page cache.
84        // Reduces first-read latency when the store is small enough to fit in RAM.
85        // Before: OS demand-faults pages one at a time on first access
86        // After:  kernel prefetches pages in the background via MADV_WILLNEED
87        #[cfg(target_os = "linux")]
88        {
89            let _ = mmap.advise(memmap2::Advice::WillNeed);
90        }
91
92        let magic = u32::from_le_bytes(mmap[0..4].try_into().unwrap());
93        if magic != MAGIC {
94            return Err(VecDbError::StorageError(format!(
95                "invalid magic: {magic:#x}"
96            )));
97        }
98        let version = u32::from_le_bytes(mmap[4..8].try_into().unwrap());
99        if version != VERSION {
100            return Err(VecDbError::StorageError(format!(
101                "unsupported version: {version}"
102            )));
103        }
104
105        let dimension = u64::from_le_bytes(mmap[8..16].try_into().unwrap()) as usize;
106        let count = u64::from_le_bytes(mmap[16..24].try_into().unwrap()) as usize;
107        let capacity = u64::from_le_bytes(mmap[24..32].try_into().unwrap()) as usize;
108
109        Ok(Self {
110            path: path.to_path_buf(),
111            dimension,
112            count,
113            capacity,
114            mmap,
115            file,
116        })
117    }
118
119    pub fn append(&mut self, vector: &Vector) -> Result<usize> {
120        if vector.len() != self.dimension {
121            return Err(VecDbError::DimensionMismatch {
122                expected: self.dimension,
123                got: vector.len(),
124            });
125        }
126        if self.count >= self.capacity {
127            self.grow()?;
128        }
129
130        let offset = HEADER_SIZE + self.count * self.dimension * 4;
131        for (i, &val) in vector.iter().enumerate() {
132            self.mmap[offset + i * 4..offset + i * 4 + 4].copy_from_slice(&val.to_le_bytes());
133        }
134
135        let index = self.count;
136        self.count += 1;
137        self.mmap[16..24].copy_from_slice(&(self.count as u64).to_le_bytes());
138        self.mmap[40..48].copy_from_slice(&now_unix().to_le_bytes());
139        self.mmap
140            .flush()
141            .map_err(|e| VecDbError::StorageError(format!("flush failed: {e}")))?;
142
143        Ok(index)
144    }
145
146    pub fn overwrite(&mut self, index: usize, vector: &Vector) -> Result<()> {
147        if vector.len() != self.dimension {
148            return Err(VecDbError::DimensionMismatch {
149                expected: self.dimension,
150                got: vector.len(),
151            });
152        }
153        if index >= self.count {
154            return Err(VecDbError::StorageError("index out of bounds".into()));
155        }
156
157        let offset = HEADER_SIZE + index * self.dimension * 4;
158        for (i, &val) in vector.iter().enumerate() {
159            self.mmap[offset + i * 4..offset + i * 4 + 4].copy_from_slice(&val.to_le_bytes());
160        }
161        self.mmap[40..48].copy_from_slice(&now_unix().to_le_bytes());
162        self.mmap
163            .flush()
164            .map_err(|e| VecDbError::StorageError(format!("flush failed: {e}")))?;
165        Ok(())
166    }
167
168    pub fn get(&self, index: usize) -> Result<Vector> {
169        if index >= self.count {
170            return Err(VecDbError::StorageError("index out of bounds".into()));
171        }
172        let offset = HEADER_SIZE + index * self.dimension * 4;
173        let mut vector = Vec::with_capacity(self.dimension);
174        for i in 0..self.dimension {
175            let bytes: [u8; 4] = self.mmap[offset + i * 4..offset + i * 4 + 4]
176                .try_into()
177                .unwrap();
178            vector.push(f32::from_le_bytes(bytes));
179        }
180        Ok(vector)
181    }
182
183    pub fn get_all(&self) -> Result<Vec<(usize, Vector)>> {
184        // Performance: on Linux, hint that pages will be read sequentially.
185        // Before: random-access page faults on each vector read
186        // After:  kernel uses read-ahead (MADV_SEQUENTIAL) to prefetch contiguous pages
187        #[cfg(target_os = "linux")]
188        {
189            let _ = self.mmap.advise(memmap2::Advice::Sequential);
190        }
191        let mut out = Vec::with_capacity(self.count);
192        for i in 0..self.count {
193            out.push((i, self.get(i)?));
194        }
195        Ok(out)
196    }
197
198    fn grow(&mut self) -> Result<()> {
199        let new_capacity = self.capacity * 2;
200        let new_size = (HEADER_SIZE + new_capacity * self.dimension * 4) as u64;
201
202        // Swap out the current mmap with a small anon mapping so the file can be resized.
203        let placeholder = MmapMut::map_anon(1)
204            .map_err(|e| VecDbError::StorageError(format!("anon mmap failed: {e}")))?;
205        drop(std::mem::replace(&mut self.mmap, placeholder));
206
207        self.file
208            .set_len(new_size)
209            .map_err(|e| VecDbError::StorageError(format!("set_len failed: {e}")))?;
210
211        self.mmap = unsafe {
212            MmapMut::map_mut(&self.file)
213                .map_err(|e| VecDbError::StorageError(format!("remap failed: {e}")))?
214        };
215
216        self.capacity = new_capacity;
217        self.mmap[24..32].copy_from_slice(&(new_capacity as u64).to_le_bytes());
218
219        tracing::info!("MmapVectorStore grew to capacity {}", new_capacity);
220        Ok(())
221    }
222
223    pub fn len(&self) -> usize {
224        self.count
225    }
226
227    pub fn is_empty(&self) -> bool {
228        self.count == 0
229    }
230
231    pub fn dimension(&self) -> usize {
232        self.dimension
233    }
234
235    pub fn flush(&self) -> Result<()> {
236        self.mmap
237            .flush()
238            .map_err(|e| VecDbError::StorageError(format!("flush failed: {e}")))
239    }
240}