use std::io::Write;
use std::sync::Arc;
use super::OffsetWriter;
use super::SegmentMerger;
use super::TrainedVectorStructures;
use super::doc_offsets;
use crate::Result;
use crate::directories::{Directory, DirectoryWriter};
use crate::dsl::{DenseVectorQuantization, FieldType, VectorIndexType};
use crate::segment::format::{DenseVectorTocEntry, write_dense_toc_and_footer};
use crate::segment::reader::SegmentReader;
use crate::segment::types::SegmentFiles;
use crate::segment::vector_data::{FlatVectorData, dequantize_raw};
const VECTOR_BATCH_SIZE: usize = 1024;
const FLAT_VECTOR_CHUNK: u64 = 8 * 1024 * 1024;
const DOC_ID_CHUNK: usize = 64 * 1024;
async fn feed_segment(
segment: &SegmentReader,
field: crate::dsl::Field,
doc_id_offset: u32,
mut add_batch: impl FnMut(&[(u32, u16)], &[f32]) -> Result<()>,
) -> crate::Result<usize> {
let lazy_flat = match segment.flat_vectors().get(&field.0) {
Some(f) => f,
None => return Ok(0),
};
let n = lazy_flat.num_vectors;
if n == 0 {
return Ok(0);
}
let dim = lazy_flat.dim;
let quant = lazy_flat.quantization;
let mut count = 0;
let needs_dequant = quant != DenseVectorQuantization::F32;
let mut f32_buf: Vec<f32> = Vec::new();
let mut labels = Vec::with_capacity(VECTOR_BATCH_SIZE);
for batch_start in (0..n).step_by(VECTOR_BATCH_SIZE) {
let batch_count = VECTOR_BATCH_SIZE.min(n - batch_start);
let batch_bytes = lazy_flat
.read_vectors_batch(batch_start, batch_count)
.await
.map_err(crate::Error::Io)?;
let raw = batch_bytes.as_slice();
let batch_floats = batch_count.checked_mul(dim).ok_or_else(|| {
crate::Error::Corruption("dense merge batch size overflows usize".into())
})?;
let vectors: &[f32] = if needs_dequant {
f32_buf.resize(batch_floats, 0.0);
dequantize_raw(raw, quant, batch_floats, &mut f32_buf).map_err(crate::Error::Io)?;
&f32_buf
} else {
if !(raw.as_ptr() as usize).is_multiple_of(std::mem::align_of::<f32>()) {
return Err(crate::Error::Corruption(
"f32 flat vector data is not 4-byte aligned".into(),
));
}
unsafe { std::slice::from_raw_parts(raw.as_ptr() as *const f32, batch_floats) }
};
labels.clear();
for i in 0..batch_count {
let (doc_id, ordinal) = lazy_flat.get_doc_id(batch_start + i);
labels.push((doc_id_offset + doc_id, ordinal));
}
add_batch(&labels, vectors)?;
count += batch_count;
}
Ok(count)
}
async fn write_flat_entry(
field_id: u32,
dim: usize,
total_vectors: usize,
quantization: DenseVectorQuantization,
segments: &[SegmentReader],
doc_offs: &[u32],
writer: &mut OffsetWriter,
) -> Result<()> {
FlatVectorData::write_binary_header(dim, total_vectors, quantization, writer)?;
for segment in segments {
if let Some(lazy_flat) = segment.flat_vectors().get(&field_id) {
let total_bytes = lazy_flat.vector_bytes_len();
let base_offset = lazy_flat.vectors_byte_offset();
let handle = lazy_flat.handle();
for chunk_start in (0..total_bytes).step_by(FLAT_VECTOR_CHUNK as usize) {
let chunk_end = chunk_start
.saturating_add(FLAT_VECTOR_CHUNK)
.min(total_bytes);
let range_start = base_offset.checked_add(chunk_start).ok_or_else(|| {
crate::Error::Corruption("flat vector source offset exceeds u64".into())
})?;
let range_end = base_offset.checked_add(chunk_end).ok_or_else(|| {
crate::Error::Corruption("flat vector source range exceeds u64".into())
})?;
let bytes = handle
.read_bytes_range(range_start..range_end)
.await
.map_err(crate::Error::Io)?;
let expected_len = usize::try_from(chunk_end - chunk_start).map_err(|_| {
crate::Error::Corruption("flat vector merge chunk exceeds usize".into())
})?;
if bytes.len() != expected_len {
return Err(crate::Error::Corruption(format!(
"flat vector merge read returned {} bytes, expected {expected_len}",
bytes.len()
)));
}
super::block_in_place_if_multithread(|| writer.write_all(bytes.as_slice()))?;
}
}
}
let mut buf = Vec::with_capacity(DOC_ID_CHUNK * 6);
for (seg_idx, segment) in segments.iter().enumerate() {
if let Some(lazy_flat) = segment.flat_vectors().get(&field_id) {
let offset = doc_offs[seg_idx];
let count = lazy_flat.num_vectors;
for chunk_start in (0..count).step_by(DOC_ID_CHUNK) {
buf.clear();
let chunk_end = (chunk_start + DOC_ID_CHUNK).min(count);
for i in chunk_start..chunk_end {
let (doc_id, ordinal) = lazy_flat.get_doc_id(i);
buf.extend_from_slice(&(offset + doc_id).to_le_bytes());
buf.extend_from_slice(&ordinal.to_le_bytes());
}
super::block_in_place_if_multithread(|| writer.write_all(&buf))?;
}
}
}
Ok(())
}
impl SegmentMerger {
pub(crate) async fn merge_dense_vectors<D: Directory + DirectoryWriter>(
&self,
dir: &D,
segments: &[SegmentReader],
files: &SegmentFiles,
trained: Option<&TrainedVectorStructures>,
) -> Result<usize> {
let doc_offs = doc_offsets(segments)?;
struct FieldInfo {
field: crate::dsl::Field,
dim: usize,
total_vectors: usize,
quantization: DenseVectorQuantization,
}
let mut fields_to_write: Vec<FieldInfo> = Vec::new();
for (field, entry) in self.schema.fields() {
if !matches!(
entry.field_type,
FieldType::DenseVector | FieldType::BinaryDenseVector
) || !(entry.indexed || entry.stored)
{
continue;
}
let dim: usize = segments
.iter()
.filter_map(|s| s.flat_vectors().get(&field.0).map(|f| f.dim))
.find(|&d| d > 0)
.unwrap_or(0);
if dim == 0 {
continue;
}
let total_vectors = segments
.iter()
.filter_map(|s| s.flat_vectors().get(&field.0).map(|f| f.num_vectors))
.try_fold(0usize, |total, count| total.checked_add(count))
.ok_or_else(|| {
crate::Error::Corruption(format!(
"flat vector count overflows usize for field {}",
field.0
))
})?;
if total_vectors == 0 {
continue;
}
let quantization = if entry.field_type == FieldType::BinaryDenseVector {
DenseVectorQuantization::Binary
} else {
entry
.dense_vector_config
.as_ref()
.map(|c| c.quantization)
.or_else(|| {
segments
.iter()
.find_map(|s| s.flat_vectors().get(&field.0).map(|f| f.quantization))
})
.unwrap_or(DenseVectorQuantization::F32)
};
fields_to_write.push(FieldInfo {
field,
dim,
total_vectors,
quantization,
});
}
if fields_to_write.is_empty() {
return Ok(0);
}
let write_start = std::time::Instant::now();
let mut writer = OffsetWriter::new(dir.streaming_writer_cold(&files.vectors).await?);
let mut toc: Vec<DenseVectorTocEntry> = Vec::new();
for fi in &fields_to_write {
let field = fi.field;
let entry = self.schema.get_field_entry(field).unwrap();
let config = entry.dense_vector_config.as_ref();
if entry.field_type == FieldType::BinaryDenseVector {
if let Some(index) = self
.try_build_binary_ivf(
field,
entry,
segments,
&doc_offs,
fi.total_vectors,
trained,
)
.await?
{
let data_offset = writer.offset();
super::block_in_place_if_multithread(|| index.write_to(&mut writer))
.map_err(crate::Error::Io)?;
let data_size = writer.offset() - data_offset;
toc.push(DenseVectorTocEntry {
field_id: field.0,
index_type: crate::segment::ann_build::BINARY_IVF_TYPE,
offset: data_offset,
size: data_size,
});
drop(index);
let pad = (8 - (writer.offset() % 8)) % 8;
if pad > 0 {
super::block_in_place_if_multithread(|| {
writer.write_all(&[0u8; 8][..pad as usize])
})?;
}
}
} else if let Some((index_type, bytes)) = self
.try_build_ann(field, config, segments, &doc_offs, trained)
.await?
{
let data_offset = writer.offset();
super::block_in_place_if_multithread(|| writer.write_all(&bytes))?;
let data_size = writer.offset() - data_offset;
toc.push(DenseVectorTocEntry {
field_id: field.0,
index_type,
offset: data_offset,
size: data_size,
});
let pad = (8 - (writer.offset() % 8)) % 8;
if pad > 0 {
super::block_in_place_if_multithread(|| {
writer.write_all(&[0u8; 8][..pad as usize])
})?;
}
}
let data_offset = writer.offset();
write_flat_entry(
field.0,
fi.dim,
fi.total_vectors,
fi.quantization,
segments,
&doc_offs,
&mut writer,
)
.await?;
let data_size = writer.offset() - data_offset;
toc.push(DenseVectorTocEntry {
field_id: field.0,
index_type: crate::segment::ann_build::FLAT_TYPE,
offset: data_offset,
size: data_size,
});
let pad = (8 - (writer.offset() % 8)) % 8;
if pad > 0 {
super::block_in_place_if_multithread(|| {
writer.write_all(&[0u8; 8][..pad as usize])
})?;
}
}
let toc_offset = writer.offset();
write_dense_toc_and_footer(&mut writer, toc_offset, &toc)?;
let output_size = writer.offset() as usize;
super::block_in_place_if_multithread(move || writer.finish())?;
log::info!(
"[merge_vectors] file written: {} ({} entries) in {:.1}s",
super::format_bytes(output_size),
toc.len(),
write_start.elapsed().as_secs_f64()
);
Ok(output_size)
}
async fn try_build_binary_ivf(
&self,
field: crate::dsl::Field,
entry: &crate::dsl::FieldEntry,
segments: &[SegmentReader],
doc_offs: &[u32],
total_vectors: usize,
trained: Option<&TrainedVectorStructures>,
) -> Result<Option<crate::structures::BinaryIvfIndex>> {
let Some(cfg) = entry.binary_dense_vector_config.as_ref() else {
return Ok(None);
};
if cfg.index_type != crate::dsl::BinaryIndexType::Ivf {
return Ok(None);
}
let Some(quantizer) = trained.and_then(|trained| trained.binary_quantizers.get(&field.0))
else {
return Ok(None);
};
let mut merged: Option<crate::structures::BinaryIvfIndex> = None;
let mut compatible = true;
for (segment, &doc_offset) in segments.iter().zip(doc_offs) {
let Some(crate::segment::VectorIndex::BinaryIvf(lazy)) =
segment.vector_indexes().get(&field.0)
else {
compatible = false;
break;
};
let Some(source) = lazy.get() else {
compatible = false;
break;
};
if source.quantizer_version != quantizer.version {
compatible = false;
break;
}
if let Some(target) = &mut merged {
target.merge_into(source, doc_offset).map_err(|error| {
crate::Error::Corruption(format!(
"global binary IVF merge compatibility failure: {error}"
))
})?;
} else {
let mut first = (**source).clone();
if doc_offset != 0 {
let empty = crate::structures::BinaryIvfIndex::build(
quantizer,
cfg.ivf_routing,
&[],
&[],
)
.map_err(crate::Error::Io)?;
let mut shifted = empty;
shifted
.merge_into(&first, doc_offset)
.map_err(|error| crate::Error::Corruption(error.to_string()))?;
first = shifted;
}
merged = Some(first);
}
}
if compatible && let Some(index) = merged {
log::debug!(
"[merge_vectors] field {}: global binary IVF block-merged ({} vectors)",
field.0,
index.len(),
);
return Ok(Some(index));
}
let byte_len = cfg.dim.div_ceil(8);
let code_capacity = total_vectors.checked_mul(byte_len).ok_or_else(|| {
crate::Error::Internal("binary IVF code capacity exceeds usize".into())
})?;
let mut codes: Vec<u8> = Vec::with_capacity(code_capacity);
let mut labels: Vec<(u32, u16)> = Vec::with_capacity(total_vectors);
const CODE_BATCH: usize = 65536;
for (seg_idx, segment) in segments.iter().enumerate() {
let Some(lazy_flat) = segment.flat_vectors().get(&field.0) else {
continue;
};
let offset = doc_offs[seg_idx];
let n = lazy_flat.num_vectors;
for batch_start in (0..n).step_by(CODE_BATCH) {
let batch_count = CODE_BATCH.min(n - batch_start);
let bytes = lazy_flat
.read_vectors_batch(batch_start, batch_count)
.await
.map_err(crate::Error::Io)?;
codes.extend_from_slice(bytes.as_slice());
for i in 0..batch_count {
let (doc_id, ordinal) = lazy_flat.get_doc_id(batch_start + i);
let merged_doc_id = doc_id.checked_add(offset).ok_or_else(|| {
crate::Error::Corruption(format!(
"binary IVF doc-id offset overflow: {doc_id} + {offset}"
))
})?;
labels.push((merged_doc_id, ordinal));
}
}
}
if labels.is_empty() {
return Ok(None);
}
if codes.len() != labels.len().saturating_mul(byte_len) {
return Err(crate::Error::Corruption(format!(
"binary IVF code/label mismatch: {} bytes for {} labels × {} bytes",
codes.len(),
labels.len(),
byte_len,
)));
}
let vector_count = labels.len();
let num_clusters = quantizer.num_clusters;
let quantizer = Arc::clone(quantizer);
let routing = cfg.ivf_routing;
let pool = self.background_pool.clone();
let index = tokio::task::spawn_blocking(move || {
let build =
|| crate::structures::BinaryIvfIndex::build(&quantizer, routing, &codes, &labels);
let index = if let Some(pool) = pool {
pool.install(build)
} else {
build()
}?;
drop(codes);
drop(labels);
Ok::<_, std::io::Error>(index)
})
.await
.map_err(|error| crate::Error::Internal(format!("binary IVF build task failed: {error}")))?
.map_err(crate::Error::Io)?;
log::debug!(
"[merge_vectors] field {}: binary IVF rebuilt ({} vectors, {} clusters, estimated {} bytes)",
field.0,
vector_count,
num_clusters,
index.estimated_memory_bytes(),
);
Ok(Some(index))
}
async fn try_build_ann(
&self,
field: crate::dsl::Field,
config: Option<&crate::dsl::DenseVectorConfig>,
segments: &[SegmentReader],
doc_offs: &[u32],
trained: Option<&TrainedVectorStructures>,
) -> Result<Option<(u8, Vec<u8>)>> {
let segments_with_flat = segments
.iter()
.filter(|s| s.flat_vectors().contains_key(&field.0))
.count();
let ivf_pq_indexes: Vec<_> = segments
.iter()
.enumerate()
.filter_map(|(seg_idx, s)| {
s.get_ivf_pq_vector_index(field)
.map(|index| (doc_offs[seg_idx], index))
})
.collect();
let matches_published_generation = trained
.and_then(|trained| {
trained
.centroids
.get(&field.0)
.zip(trained.codebooks.get(&field.0))
})
.is_some_and(|(centroids, codebook)| {
ivf_pq_indexes.iter().all(|(_, index)| {
index.centroids_version == centroids.version
&& index.codebook_version == codebook.version
})
});
if matches_published_generation
&& ivf_pq_indexes.len() == segments_with_flat
&& !ivf_pq_indexes.is_empty()
{
let refs: Vec<&crate::structures::IVFPQIndex> = ivf_pq_indexes
.iter()
.map(|(_, index)| index.as_ref())
.collect();
let offsets: Vec<u32> = ivf_pq_indexes.iter().map(|(off, _)| *off).collect();
match crate::structures::IVFPQIndex::merge(&refs, &offsets) {
Ok(merged) => {
let bytes = merged
.to_bytes()
.map_err(|e| crate::Error::Serialization(e.to_string()))?;
return Ok(Some((crate::segment::ann_build::IVF_PQ_TYPE, bytes)));
}
Err(e) => {
log::warn!("IVF-PQ merge failed: {}, falling back to rebuild", e);
}
}
}
let ann_type = trained
.zip(config)
.and_then(|(trained, config)| match config.index_type {
VectorIndexType::IvfPq
if trained.centroids.contains_key(&field.0)
&& trained.codebooks.contains_key(&field.0) =>
{
Some(VectorIndexType::IvfPq)
}
_ => None,
});
let ann = match ann_type {
Some(ann) => ann,
None => {
log::debug!(
"[merge_vectors] field {}: no IVF-PQ path available (trained={}, config={}, existing={}/{})",
field.0,
trained.is_some(),
config
.map(|c| format!("{:?}", c.index_type))
.unwrap_or_else(|| "None".into()),
ivf_pq_indexes.len(),
segments_with_flat,
);
return Ok(None);
}
};
let trained = trained.unwrap();
let dim = segments
.iter()
.filter_map(|s| s.flat_vectors().get(&field.0).map(|f| f.dim))
.find(|&d| d > 0)
.unwrap();
let mut total_fed = 0usize;
let ann_start = std::time::Instant::now();
let (index_type, bytes) = match ann {
VectorIndexType::Flat => unreachable!(),
VectorIndexType::IvfPq => {
let centroids = &trained.centroids[&field.0];
let codebook = &trained.codebooks[&field.0];
let routing = config.map_or(crate::dsl::IvfRoutingMode::Auto, |config| {
config.ivf_routing
});
let mut index =
crate::segment::ann_build::new_ivf_pq(dim, routing, centroids, codebook);
for (seg_idx, segment) in segments.iter().enumerate() {
let offset = doc_offs[seg_idx];
let fed = feed_segment(segment, field, offset, |labels, vectors| {
super::block_in_place_if_multithread(|| {
let mut add =
|| index.add_vectors_parallel(centroids, codebook, labels, vectors);
if let Some(pool) = &self.background_pool {
pool.install(add)
} else {
add()
}
})
.map_err(|error| {
crate::Error::Internal(format!(
"parallel IVF-PQ rebuild failed for field {}: {error}",
field.0,
))
})
})
.await?;
total_fed += fed;
if fed > 0 {
log::debug!(
"[merge_vectors] field {} IVF-PQ: fed {} vectors from segment {} ({} total, {:.1}s)",
field.0,
fed,
seg_idx,
total_fed,
ann_start.elapsed().as_secs_f64()
);
}
}
log::info!(
"[merge_vectors] field {} IVF-PQ: serializing index ({} vectors, {:.1}s elapsed)",
field.0,
total_fed,
ann_start.elapsed().as_secs_f64()
);
let bytes = super::block_in_place_if_multithread(|| {
crate::segment::ann_build::serialize_ivf_pq(index)
})?;
(crate::segment::ann_build::IVF_PQ_TYPE, bytes)
}
};
log::info!(
"[merge_vectors] field {} ANN(type={}) rebuilt: {} vectors, blob={}, {:.1}s",
field.0,
index_type,
total_fed,
super::format_bytes(bytes.len()),
ann_start.elapsed().as_secs_f64()
);
Ok(Some((index_type, bytes)))
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::SegmentMerger;
use crate::directories::RamDirectory;
use crate::dsl::{DenseVectorConfig, Document, SchemaBuilder};
use crate::index::{IndexConfig, IndexWriter};
use crate::segment::reader::SegmentReader;
use crate::segment::types::SegmentId;
async fn committed_segment_ids(dir: &RamDirectory) -> Vec<String> {
crate::index::IndexMetadata::load(dir)
.await
.unwrap()
.segment_ids()
}
async fn newly_committed_segment(dir: &RamDirectory, known: &mut Vec<String>) -> String {
let ids = committed_segment_ids(dir).await;
let new: Vec<String> = ids
.iter()
.filter(|id| !known.contains(id))
.cloned()
.collect();
assert_eq!(
new.len(),
1,
"expected exactly one new segment, got {new:?}"
);
*known = ids;
new.into_iter().next().unwrap()
}
#[tokio::test]
async fn o1_ann_cluster_merge_skips_offsets_of_segments_without_the_field() {
let dim = 8;
let mut sb = SchemaBuilder::default();
let title = sb.add_text_field("title", true, true);
let embedding = sb.add_dense_vector_field_with_config(
"embedding",
true,
true,
DenseVectorConfig::with_ivf_pq(dim, Some(1), 1),
);
let schema = sb.build();
let dir = RamDirectory::new();
let config = IndexConfig {
merge_policy: Box::new(crate::merge::NoMergePolicy),
num_indexing_threads: 1,
..Default::default()
};
let mut writer = IndexWriter::create(dir.clone(), schema.clone(), config)
.await
.unwrap();
for i in 0..4 {
let mut doc = Document::new();
doc.add_text(title, format!("train {i}"));
doc.add_dense_vector(embedding, vec![i as f32 + 1.0; dim]);
writer.add_document(doc).unwrap();
}
writer.commit().await.unwrap();
writer.build_vector_index().await.unwrap();
let mut known = committed_segment_ids(&dir).await;
for i in 0..2 {
let mut doc = Document::new();
doc.add_text(title, format!("a {i}"));
doc.add_dense_vector(embedding, vec![0.5; dim]);
writer.add_document(doc).unwrap();
}
writer.commit().await.unwrap();
let seg_a = newly_committed_segment(&dir, &mut known).await;
for i in 0..3 {
let mut doc = Document::new();
doc.add_text(title, format!("b {i}"));
writer.add_document(doc).unwrap();
}
writer.commit().await.unwrap();
let seg_b = newly_committed_segment(&dir, &mut known).await;
for i in 0..2 {
let mut doc = Document::new();
doc.add_text(title, format!("c {i}"));
doc.add_dense_vector(embedding, vec![0.25; dim]);
writer.add_document(doc).unwrap();
}
writer.commit().await.unwrap();
let seg_c = newly_committed_segment(&dir, &mut known).await;
let schema = Arc::new(schema);
let mut readers = Vec::new();
for id in [&seg_a, &seg_b, &seg_c] {
readers.push(
SegmentReader::open(
&dir,
SegmentId::from_hex(id).unwrap(),
Arc::clone(&schema),
16,
)
.await
.unwrap(),
);
}
assert!(readers[0].get_ivf_pq_vector_index(embedding).is_some());
assert!(
readers[1].get_ivf_pq_vector_index(embedding).is_none(),
"segment B must not carry the dense field"
);
assert!(readers[2].get_ivf_pq_vector_index(embedding).is_some());
let merged_id = SegmentId::new();
let trained = writer.segment_manager().trained().unwrap();
SegmentMerger::new(Arc::clone(&schema))
.merge(&dir, &readers, merged_id, Some(trained.as_ref()))
.await
.unwrap();
let merged = SegmentReader::open(&dir, merged_id, Arc::clone(&schema), 16)
.await
.unwrap();
let ivf_pq = merged
.get_ivf_pq_vector_index(embedding)
.expect("homogeneous IVF-PQ sources must take the O(1) cluster-merge path");
let mut doc_ids: Vec<u32> = ivf_pq
.clusters
.values()
.flat_map(|cluster| cluster.doc_ids.iter().copied())
.collect();
doc_ids.sort_unstable();
assert_eq!(
doc_ids,
vec![0, 1, 5, 6],
"merged ANN doc ids must use each field-bearing segment's own offset"
);
}
}