use std::sync::Arc;
use crate::error::Result;
use crate::storage::Storage;
use crate::vector::core::vector::Vector;
use crate::vector::index::segment::manager::ManagedSegmentInfo;
use crate::vector::index::segment::merge::{MergeConfig, MergeResult, MergeStats};
use crate::maintenance::deletion::DeletionBitmap;
use crate::vector::index::config::HnswIndexConfig;
use crate::vector::index::hnsw::reader::HnswIndexReader;
use crate::vector::index::hnsw::writer::HnswIndexWriter;
use crate::vector::reader::VectorIndexReader;
use crate::vector::writer::{VectorIndexWriter, VectorIndexWriterConfig};
pub struct MergeEngine {
config: MergeConfig,
storage: Arc<dyn Storage>,
index_config: HnswIndexConfig,
writer_config: VectorIndexWriterConfig,
deletion_bitmap: Option<Arc<DeletionBitmap>>,
}
impl MergeEngine {
pub fn new(
config: MergeConfig,
storage: Arc<dyn Storage>,
index_config: HnswIndexConfig,
writer_config: VectorIndexWriterConfig,
) -> Self {
Self {
config,
storage,
index_config,
writer_config,
deletion_bitmap: None,
}
}
pub fn set_deletion_bitmap(&mut self, bitmap: Arc<DeletionBitmap>) {
self.deletion_bitmap = Some(bitmap);
}
pub fn merge_segments(
&self,
segments: Vec<ManagedSegmentInfo>,
new_segment_id: String,
) -> Result<MergeResult> {
let start_time = crate::util::time::Timer::now();
let segments_merged = segments.len() as u32;
let mut deletions_removed = 0;
let mut duplicates_removed = 0u64;
let mut all_vectors: Vec<(u64, String, Vector)> = Vec::new();
let mut sources = segments.clone();
sources.sort_by_key(|s| std::cmp::Reverse(s.generation));
let mut seen: std::collections::HashSet<(u64, String)> = std::collections::HashSet::new();
for segment in &sources {
let reader = HnswIndexReader::load(
self.storage.clone(),
&segment.segment_id,
self.index_config.distance_metric,
)?;
let rerank_pool = reader.rerank_storage().cloned();
let mut iterator = reader.vector_iterator()?;
while let Some((doc_id, field, vector)) = iterator.next()? {
if let Some(bitmap) = &self.deletion_bitmap
&& bitmap.is_deleted(doc_id)
{
deletions_removed += 1;
continue;
}
if !seen.insert((doc_id, field.clone())) {
duplicates_removed += 1;
continue;
}
let vector = match rerank_pool
.as_ref()
.and_then(|pool| pool.get_f32_slice(doc_id, &field))
{
Some(f32_vector) => Vector::new(f32_vector.to_vec()),
None => vector,
};
all_vectors.push((doc_id, field, vector));
}
}
let vectors_merged = all_vectors.len() as u64;
let total_size = vectors_merged * 128; let mut writer = HnswIndexWriter::with_storage(
self.index_config.clone(),
self.writer_config.clone(),
&new_segment_id,
self.storage.clone(),
)?;
writer.add_vectors(all_vectors)?;
writer.finalize()?;
writer.write()?;
let merge_time_ms = start_time.elapsed_ms();
let merged_segment = ManagedSegmentInfo {
segment_id: new_segment_id,
vector_count: vectors_merged,
vector_offset: 0,
generation: segments.iter().map(|s| s.generation).max().unwrap_or(0),
has_deletions: false,
size_bytes: total_size,
};
let stats = MergeStats {
segments_merged,
vectors_merged,
deletions_removed,
duplicates_removed,
merge_time_ms,
merged_size_bytes: total_size,
};
Ok(MergeResult {
merged_segment,
stats,
merged_segment_ids: segments.iter().map(|s| s.segment_id.clone()).collect(),
})
}
pub fn storage(&self) -> &Arc<dyn Storage> {
&self.storage
}
pub fn config(&self) -> &MergeConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::memory::{MemoryStorage, MemoryStorageConfig};
#[test]
fn test_merge_engine_basic() {
let config = MergeConfig::default();
let storage = Arc::new(MemoryStorage::new(MemoryStorageConfig::default()));
let index_config = HnswIndexConfig::default();
let writer_config = VectorIndexWriterConfig::default();
let engine = MergeEngine::new(config, storage, index_config, writer_config);
let _segments = [ManagedSegmentInfo {
segment_id: "seg1".to_string(),
vector_count: 1000,
vector_offset: 0,
generation: 0,
has_deletions: false,
size_bytes: 128000,
}];
assert_eq!(engine.config.max_merge_segments, 10);
}
#[test]
fn test_merge_stats() {
let stats = MergeStats {
segments_merged: 3,
vectors_merged: 1000,
deletions_removed: 200,
duplicates_removed: 0,
merge_time_ms: 100,
merged_size_bytes: 102400,
};
assert_eq!(stats.compression_ratio(), 0.8);
}
}