Plan a compaction: compute the expected output segment metadata. This is a dry-run — no I/O is performed. The caller uses this to decide whether the compaction is worth performing.
(segments: &[SegmentMeta], output_dir: &Path)
| 132 | /// This is a dry-run — no I/O is performed. The caller uses this to |
| 133 | /// decide whether the compaction is worth performing. |
| 134 | pub fn plan_compaction(segments: &[SegmentMeta], output_dir: &Path) -> Option<CompactionResult> { |
| 135 | if segments.is_empty() { |
| 136 | return None; |
| 137 | } |
| 138 | |
| 139 | let min_lsn = segments.iter().map(|s| s.min_lsn).min().unwrap_or(0); |
| 140 | let max_lsn = segments.iter().map(|s| s.max_lsn).max().unwrap_or(0); |
| 141 | let tombstones: u64 = segments.iter().map(|s| s.tombstone_entries).sum(); |
| 142 | let total_bytes: u64 = segments.iter().map(|s| s.size_bytes).sum(); |
| 143 | let tombstone_bytes = total_bytes * tombstones |
| 144 | / (segments |
| 145 | .iter() |
| 146 | .map(|s| s.live_entries + s.tombstone_entries) |
| 147 | .sum::<u64>()) |
| 148 | .max(1); |
| 149 | |
| 150 | let output_path = output_dir.join(format!("segment-{min_lsn}-{max_lsn}.dat")); |
| 151 | |
| 152 | Some(CompactionResult { |
| 153 | input_segments: segments.iter().map(|s| s.path.clone()).collect(), |
| 154 | output_segment: output_path, |
| 155 | tombstones_removed: tombstones, |
| 156 | bytes_reclaimed: tombstone_bytes, |
| 157 | min_lsn, |
| 158 | max_lsn, |
| 159 | }) |
| 160 | } |
| 161 | |
| 162 | /// HNSW segment merge: combine vectors from multiple sealed segments |
| 163 | /// into a single HNSW index, dropping tombstoned vectors. |