Skip to main content

nodedb_vector/segment_backing/
plain.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! [`PlainMmapBacking`]: zero-copy [`VectorSegmentBacking`] over a plaintext
4//! NDVS mmap segment.
5
6use std::sync::Arc;
7
8use crate::mmap_segment::MmapVectorSegment;
9
10use super::VectorSegmentBacking;
11
12/// Zero-copy [`VectorSegmentBacking`] backed by a plaintext NDVS mmap segment.
13///
14/// Vectors and surrogate IDs are served as slices directly into the mmap
15/// region — no allocation on the read path.
16///
17/// # `Send + Sync` rationale
18///
19/// [`MmapVectorSegment`] is declared `!Send + !Sync` because it holds a
20/// `*const u8` field (`base`) pointing into the mmap region.  Raw pointers are
21/// conservative: the compiler cannot know whether the pointee is safe to share.
22///
23/// The mmap region behind `base` is:
24/// - mapped with `PROT_READ | MAP_PRIVATE` — never mutated through this
25///   pointer after construction,
26/// - valid for exactly the lifetime of the [`MmapVectorSegment`] (the
27///   descriptor `_fd` keeps the file open; `munmap` runs in `Drop`),
28/// - not thread-affine — the OS virtual-memory subsystem treats it as a
29///   process-global read-only region.
30///
31/// All access to `base` goes through `get_vector` / `get_surrogate` /
32/// `prefetch`, which derive shared borrows (`&[f32]`, `&[u8]`) that live no
33/// longer than `&self`.  No `&mut` path exists.  Multiple threads reading
34/// distinct vectors concurrently is safe for the same reason `&[T]` is `Sync`.
35///
36/// The `Arc<MmapVectorSegment>` wrapper ensures the segment (and therefore
37/// the mmap region) outlives any `&[f32]` slice handed out through this type.
38///
39/// `PlainMmapBacking` is `Send + Sync` automatically: [`MmapVectorSegment`]
40/// carries the `unsafe impl Send + Sync` (justified by the invariants above),
41/// so `Arc<MmapVectorSegment>` — and therefore this wrapper — is too.
42pub struct PlainMmapBacking {
43    inner: Arc<MmapVectorSegment>,
44}
45
46impl PlainMmapBacking {
47    /// Wrap a [`MmapVectorSegment`] that is not yet reference-counted.
48    pub fn new(seg: MmapVectorSegment) -> Self {
49        Self {
50            inner: Arc::new(seg),
51        }
52    }
53
54    /// Wrap an already reference-counted segment.
55    ///
56    /// Useful when the same segment is shared with other consumers (e.g. a
57    /// [`crate::collection::VectorCollection`] that also owns the segment for
58    /// direct SIMD scan).
59    pub fn from_arc(seg: Arc<MmapVectorSegment>) -> Self {
60        Self { inner: seg }
61    }
62
63    /// Access the underlying segment.
64    pub fn segment(&self) -> &Arc<MmapVectorSegment> {
65        &self.inner
66    }
67}
68
69impl VectorSegmentBacking for PlainMmapBacking {
70    #[inline]
71    fn len(&self) -> usize {
72        self.inner.count()
73    }
74
75    #[inline]
76    fn dim(&self) -> usize {
77        self.inner.dim()
78    }
79
80    #[inline]
81    fn get_vector(&self, id: u32) -> Option<&[f32]> {
82        self.inner.get_vector(id)
83    }
84
85    #[inline]
86    fn get_surrogate(&self, id: u32) -> Option<u64> {
87        self.inner.get_surrogate_id(id)
88    }
89
90    #[inline]
91    fn prefetch(&self, id: u32) {
92        self.inner.prefetch(id);
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use tempfile::tempdir;
99
100    use super::*;
101    use crate::mmap_segment::MmapVectorSegment;
102
103    fn make_backing(dim: usize, vecs: &[Vec<f32>]) -> PlainMmapBacking {
104        let dir = tempdir().unwrap();
105        let path = dir.path().join("test.ndvs");
106
107        let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
108        let surrogates: Vec<u64> = (0..vecs.len() as u64).collect();
109
110        let seg =
111            MmapVectorSegment::create_with_surrogates(&path, dim, &refs, &surrogates).unwrap();
112
113        // Keep the tempdir alive by leaking it for the test duration.
114        // The backing borrows from the mmap, which is already self-contained
115        // (fd kept open by the segment); dir can be dropped.
116        drop(dir);
117
118        PlainMmapBacking::new(seg)
119    }
120
121    #[test]
122    fn plain_backing_basic_roundtrip() {
123        let dim = 4;
124        let vecs = vec![
125            vec![1.0_f32, 2.0, 3.0, 4.0],
126            vec![5.0_f32, 6.0, 7.0, 8.0],
127            vec![9.0_f32, 10.0, 11.0, 12.0],
128        ];
129
130        let backing = make_backing(dim, &vecs);
131
132        assert_eq!(backing.len(), 3);
133        assert_eq!(backing.dim(), 4);
134        assert!(!backing.is_empty());
135
136        for (i, expected) in vecs.iter().enumerate() {
137            let got = backing
138                .get_vector(i as u32)
139                .expect("vector must be present");
140            assert_eq!(got, expected.as_slice(), "vector {i} mismatch");
141
142            let sid = backing
143                .get_surrogate(i as u32)
144                .expect("surrogate must be present");
145            assert_eq!(sid, i as u64, "surrogate {i} mismatch");
146        }
147
148        // prefetch must not panic
149        backing.prefetch(0);
150        backing.prefetch(1);
151        backing.prefetch(2);
152    }
153
154    /// Compile-time proof that `PlainMmapBacking` satisfies `Send + Sync`.
155    #[test]
156    fn plain_backing_is_send_sync() {
157        fn assert_send_sync<T: Send + Sync>(_: &T) {}
158
159        let dir = tempdir().unwrap();
160        let path = dir.path().join("check.ndvs");
161        let seg = MmapVectorSegment::create(&path, 2, &[&[1.0_f32, 2.0]]).unwrap();
162        let backing = PlainMmapBacking::new(seg);
163
164        assert_send_sync(&backing);
165    }
166
167    #[test]
168    fn plain_backing_out_of_bounds_returns_none() {
169        let backing = make_backing(3, &[vec![1.0_f32, 2.0, 3.0]]);
170
171        assert!(
172            backing.get_vector(1).is_none(),
173            "id=1 must be out of bounds"
174        );
175        assert!(
176            backing.get_surrogate(1).is_none(),
177            "id=1 surrogate must be out of bounds"
178        );
179        // prefetch on out-of-bounds must be a no-op (no panic)
180        backing.prefetch(1);
181    }
182
183    #[test]
184    fn plain_backing_empty_segment() {
185        let dir = tempdir().unwrap();
186        let path = dir.path().join("empty.ndvs");
187        let seg = MmapVectorSegment::create(&path, 4, &[]).unwrap();
188        let backing = PlainMmapBacking::new(seg);
189
190        assert_eq!(backing.len(), 0);
191        assert!(backing.is_empty());
192        assert!(backing.get_vector(0).is_none());
193        assert!(backing.get_surrogate(0).is_none());
194        backing.prefetch(0);
195    }
196}