sparse_vector/
segments.rs1use std::collections::HashSet;
27use std::path::{Path, PathBuf};
28
29use serde::{Deserialize, Serialize};
30
31use crate::mmap_index::{write_file_atomic, MmapPostingData};
32
33pub const META_FILE: &str = "meta.json";
35pub const META_VERSION: u32 = 1;
37
38#[derive(Clone, Debug, Serialize, Deserialize)]
40pub struct SegmentMeta {
41 pub id: String,
43 pub num_vectors: u32,
45 #[serde(default)]
47 pub deleted: Vec<u64>,
48}
49
50impl SegmentMeta {
51 pub fn live_vectors(&self) -> usize {
53 (self.num_vectors as usize).saturating_sub(self.deleted.len())
54 }
55}
56
57#[derive(Clone, Debug, Serialize, Deserialize)]
59pub struct IndexMeta {
60 #[serde(default)]
61 pub version: u32,
62 #[serde(default)]
63 pub segments: Vec<SegmentMeta>,
64}
65
66impl Default for IndexMeta {
67 fn default() -> Self {
68 Self { version: META_VERSION, segments: Vec::new() }
69 }
70}
71
72impl IndexMeta {
73 pub fn read(base: &Path) -> Result<Self, String> {
76 let path = base.join(META_FILE);
77 if !path.exists() {
78 return Ok(Self::default());
79 }
80 let data = std::fs::read(&path)
81 .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
82 let meta: Self = serde_json::from_slice(&data)
83 .map_err(|e| format!("cannot parse {}: {e}", path.display()))?;
84 if meta.version > META_VERSION {
85 return Err(format!(
86 "{} was written by a newer version ({}, this build writes {META_VERSION})",
87 path.display(), meta.version,
88 ));
89 }
90 Ok(meta)
91 }
92
93 pub fn write(&self, base: &Path) -> Result<(), String> {
96 let data = serde_json::to_vec_pretty(self)
97 .map_err(|e| format!("cannot serialize {META_FILE}: {e}"))?;
98 write_file_atomic(&base.join(META_FILE), &data)
99 }
100
101
102 pub fn live_vectors(&self) -> usize {
104 self.segments.iter().map(SegmentMeta::live_vectors).sum()
105 }
106
107 pub fn files(&self) -> Vec<String> {
109 let mut files: Vec<String> = Vec::with_capacity(self.segments.len() * 2 + 1);
110 for s in &self.segments {
111 files.push(segment_file(&s.id));
112 files.push(ids_file(&s.id));
113 }
114 files.push(META_FILE.to_string());
115 files
116 }
117}
118
119pub fn segment_file(id: &str) -> String {
121 format!("seg_{id}.mmap")
122}
123
124pub fn ids_file(id: &str) -> String {
126 format!("seg_{id}.ids")
127}
128
129pub fn encode_ids(ids: &[u64]) -> Vec<u8> {
131 let mut out = Vec::with_capacity(ids.len() * 8);
132 for id in ids {
133 out.extend_from_slice(&id.to_le_bytes());
134 }
135 out
136}
137
138fn decode_ids(data: &[u8]) -> Result<Vec<u64>, String> {
139 if data.len() % 8 != 0 {
140 return Err(format!("id list is {} bytes, not a multiple of 8", data.len()));
141 }
142 Ok(data.chunks_exact(8).map(|c| u64::from_le_bytes(c.try_into().unwrap())).collect())
143}
144
145pub fn new_segment_id(counter: u64) -> String {
148 let nanos = std::time::SystemTime::now()
149 .duration_since(std::time::UNIX_EPOCH)
150 .map(|d| d.as_nanos() as u64)
151 .unwrap_or(0);
152 format!("{:08x}{:04x}{:04x}", nanos & 0xffff_ffff, std::process::id() & 0xffff, counter & 0xffff)
153}
154
155pub struct Segment {
157 pub meta: SegmentMeta,
158 pub data: MmapPostingData,
159 deleted: HashSet<u64>,
161 ids: std::cell::OnceCell<Vec<u64>>,
164 base: PathBuf,
165}
166
167impl Segment {
168 pub fn open(base: &Path, meta: SegmentMeta) -> Result<Self, String> {
169 let data = MmapPostingData::open(&base.join(segment_file(&meta.id)))?;
170 let deleted = meta.deleted.iter().copied().collect();
171 Ok(Self { meta, data, deleted, ids: std::cell::OnceCell::new(), base: base.to_path_buf() })
172 }
173
174 pub fn is_live(&self, id: u64) -> bool {
176 !self.deleted.contains(&id)
177 }
178
179 pub fn ids(&self) -> Result<&[u64], String> {
181 if let Some(ids) = self.ids.get() {
182 return Ok(ids);
183 }
184 let path = self.base.join(ids_file(&self.meta.id));
185 let data = std::fs::read(&path)
186 .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
187 let ids = decode_ids(&data)?;
188 Ok(self.ids.get_or_init(|| ids))
189 }
190
191 pub fn holds(&self, id: u64) -> Result<bool, String> {
193 Ok(self.ids()?.binary_search(&id).is_ok())
194 }
195
196 pub fn tombstone(&mut self, id: u64) -> bool {
198 if !self.deleted.insert(id) {
199 return false;
200 }
201 if let Err(pos) = self.meta.deleted.binary_search(&id) {
202 self.meta.deleted.insert(pos, id);
203 }
204 true
205 }
206
207 pub fn path(&self) -> PathBuf {
208 self.base.join(segment_file(&self.meta.id))
209 }
210
211 pub fn ids_path(&self) -> PathBuf {
212 self.base.join(ids_file(&self.meta.id))
213 }
214}
215
216pub fn merge_segments(
234 base: &Path,
235 inputs: &[&Segment],
236 new_id: &str,
237) -> Result<SegmentMeta, String> {
238 use crate::wand::Postings;
239
240 let mut cursors: Vec<std::iter::Peekable<_>> =
243 inputs.iter().map(|s| s.data.tokens().peekable()).collect();
244 let mut tokens: Vec<u32> = Vec::new();
245 loop {
246 let next = cursors.iter_mut()
247 .filter_map(|c| c.peek().map(|(t, _)| *t))
248 .min();
249 let Some(token) = next else { break };
250 for c in cursors.iter_mut() {
251 if c.peek().is_some_and(|(t, _)| *t == token) {
252 c.next();
253 }
254 }
255 tokens.push(token);
256 }
257
258 let mut postings: Vec<Postings> = Vec::with_capacity(tokens.len());
260 let mut ids: std::collections::BTreeSet<u64> = std::collections::BTreeSet::new();
261 let mut pairs: Vec<(u64, f32)> = Vec::new();
262 for &token in &tokens {
263 pairs.clear();
264 for seg in inputs {
265 for e in seg.data.entries_of_token(token) {
266 if seg.is_live(e.record_id) {
267 pairs.push((e.record_id, e.weight));
268 ids.insert(e.record_id);
269 }
270 }
271 }
272 pairs.sort_by_key(|(id, _)| *id);
274 postings.push(Postings::from_sorted_pairs(&pairs));
275 }
276
277 let ids: Vec<u64> = ids.into_iter().collect();
278 crate::mmap_index::write_mmap_file(
279 &base.join(segment_file(new_id)),
280 &postings,
281 &tokens,
282 ids.len() as u32,
283 )?;
284 write_file_atomic(&base.join(ids_file(new_id)), &encode_ids(&ids))?;
285
286 Ok(SegmentMeta { id: new_id.to_string(), num_vectors: ids.len() as u32, deleted: Vec::new() })
287}