use std::collections::{BTreeMap, HashMap};
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use crate::common::budget::{ResourceBudget, ResourcePermit};
use crate::common::progress_tracker::ProgressTracker;
#[cfg(any(test, feature = "testing"))]
use itertools::Itertools;
use parking_lot::{Mutex, RwLock};
use crate::segment::common::operation_error::{OperationError, OperationResult};
use crate::segment::common::operation_time_statistics::OperationDurationsAggregator;
use crate::segment::entry::ReadSegmentEntry;
use crate::segment::index::hnsw_index::get_num_indexing_threads;
use crate::segment::index::sparse_index::sparse_index_config::SparseIndexType;
use crate::segment::segment::Segment;
use crate::segment::segment_constructor::build_segment;
use crate::segment::segment_constructor::segment_builder::SegmentBuilder;
use crate::segment::types::{HnswGlobalConfig, Indexes, VectorStorageType};
use uuid::Uuid;
use super::config::SegmentOptimizerConfig;
use crate::shard::locked_segment::LockedSegment;
use crate::shard::operations::optimization::OptimizerThresholds;
use crate::shard::optimize::{OptimizationPaths, OptimizationStrategy, execute_optimization};
use crate::shard::segment_holder::locked::LockedSegmentHolder;
use crate::shard::segment_holder::{SegmentHolder, SegmentId};
const BYTES_IN_KB: usize = 1024;
pub fn max_num_indexing_threads(segment_optimizer_config: &SegmentOptimizerConfig) -> usize {
let segment_resolution = segment_optimizer_config
.dense_vector
.values()
.map(|cfg| get_num_indexing_threads(cfg.hnsw_config.max_indexing_threads))
.max();
if let Some(segment_resolution) = segment_resolution {
segment_resolution
} else {
get_num_indexing_threads(0)
}
}
pub type Optimizer = dyn SegmentOptimizer + Sync + Send;
struct ShardOptimizationStrategy<'a, O: SegmentOptimizer + ?Sized> {
optimizer: &'a O,
}
impl<O: SegmentOptimizer + ?Sized> OptimizationStrategy for ShardOptimizationStrategy<'_, O> {
fn create_segment_builder(
&self,
input_segments: &[LockedSegment],
) -> OperationResult<SegmentBuilder> {
self.optimizer.optimized_segment_builder(input_segments)
}
fn create_temp_segment(&self) -> OperationResult<LockedSegment> {
self.optimizer.temp_segment(false)
}
}
pub trait SegmentOptimizer: Sync {
fn name(&self) -> &'static str;
fn segments_path(&self) -> &Path;
fn temp_path(&self) -> &Path;
fn segment_optimizer_config(&self) -> &SegmentOptimizerConfig;
fn num_indexing_threads(&self) -> usize {
max_num_indexing_threads(self.segment_optimizer_config())
}
fn hnsw_global_config(&self) -> &HnswGlobalConfig;
fn threshold_config(&self) -> &OptimizerThresholds;
fn plan_optimizations(&self, planner: &mut OptimizationPlanner);
#[cfg(any(test, feature = "testing"))]
fn plan_optimizations_for_test(&self, segments: &LockedSegmentHolder) -> Vec<Vec<SegmentId>> {
let segments = segments.read();
let mut planner = OptimizationPlanner::new(0, segments.iter_original());
self.plan_optimizations(&mut planner);
let result = planner.into_scheduled_for_test();
let mut remaining: BTreeMap<_, _> = segments.iter_original().collect();
for (i, batch) in result.iter().enumerate() {
for &id in batch {
remaining.remove(&id);
}
let mut planner =
OptimizationPlanner::new(i + 1, remaining.iter().map(|(&id, &seg)| (id, seg)));
self.plan_optimizations(&mut planner);
let actual = planner.into_scheduled_for_test();
let expected = &result[i + 1..];
if self.name() == "merge"
&& actual.is_empty()
&& expected.len() == 1
&& expected[0].len() == 2
{
continue;
}
assert_eq!(actual, expected);
}
result
}
fn get_telemetry_counter(&self) -> &Mutex<OperationDurationsAggregator>;
fn temp_segment(&self, save_version: bool) -> OperationResult<LockedSegment> {
let config = self.segment_optimizer_config().plain_segment_config();
Ok(LockedSegment::new(build_segment(
self.segments_path(),
&config,
self.threshold_config().deferred_internal_id,
save_version,
)?))
}
fn optimized_segment_builder(
&self,
optimizing_segments: &[LockedSegment],
) -> OperationResult<SegmentBuilder> {
let mut bytes_count_by_vector_name = HashMap::new();
for segment in optimizing_segments {
let segment = match segment {
LockedSegment::Original(segment) => segment,
LockedSegment::Proxy(_) => {
return Err(OperationError::service_error(
"Proxy segment is not expected here",
));
}
};
let locked_segment = segment.read();
for vector_name in locked_segment.vector_names() {
let vector_size = locked_segment.available_vectors_size_in_bytes(&vector_name)?;
let size = bytes_count_by_vector_name.entry(vector_name).or_insert(0);
*size += vector_size;
}
}
let maximal_vector_store_size_bytes = bytes_count_by_vector_name
.values()
.max()
.copied()
.unwrap_or(0);
let thresholds = self.threshold_config();
let segment_optimizer_config = self.segment_optimizer_config();
let threshold_is_indexed = maximal_vector_store_size_bytes
>= thresholds.indexing_threshold_kb.saturating_mul(BYTES_IN_KB);
let threshold_is_on_disk = maximal_vector_store_size_bytes
>= thresholds.memmap_threshold_kb.saturating_mul(BYTES_IN_KB);
let mut vector_data = segment_optimizer_config.plain_dense_vector_config.clone();
let mut sparse_vector_data = segment_optimizer_config.plain_sparse_vector_config.clone();
if threshold_is_indexed {
vector_data.iter_mut().for_each(|(vector_name, config)| {
if let Some(vector_cfg) = segment_optimizer_config.dense_vector.get(vector_name) {
config.index = Indexes::Hnsw(vector_cfg.hnsw_config);
config.quantization_config = vector_cfg.quantization_config.clone();
}
});
}
if threshold_is_on_disk || threshold_is_indexed {
vector_data.iter_mut().for_each(|(vector_name, config)| {
let config_on_disk = segment_optimizer_config
.dense_vector
.get(vector_name)
.and_then(|cfg| cfg.on_disk);
match config_on_disk {
Some(true) => config.storage_type = VectorStorageType::Mmap, Some(false) => {
if crate::common::flags::feature_flags().single_file_mmap_vector_storage {
config.storage_type = VectorStorageType::InRamMmap;
}
} None => {
if threshold_is_on_disk {
config.storage_type = VectorStorageType::Mmap
} else if crate::common::flags::feature_flags().single_file_mmap_vector_storage {
config.storage_type = VectorStorageType::InRamMmap;
}
} }
if let Some(config_on_disk) = config_on_disk
&& config_on_disk != config.storage_type.is_on_disk()
{
log::warn!(
"Collection config for vector {vector_name} has on_disk={config_on_disk:?} configured, but storage type for segment doesn't match it"
);
}
});
}
sparse_vector_data
.iter_mut()
.for_each(|(vector_name, config)| {
let config_on_disk = segment_optimizer_config
.sparse_vector
.get(vector_name)
.and_then(|cfg| cfg.on_disk)
.unwrap_or(threshold_is_on_disk);
let is_big = threshold_is_on_disk || threshold_is_indexed;
let index_type = match (is_big, config_on_disk) {
(true, true) => SparseIndexType::Mmap,
(true, false) => SparseIndexType::ImmutableRam,
(false, _) => SparseIndexType::MutableRam,
};
config.index.index_type = index_type;
});
let optimized_config = crate::segment::types::SegmentConfig {
vector_data,
sparse_vector_data,
payload_storage_type: segment_optimizer_config.payload_storage_type,
};
SegmentBuilder::new(
self.temp_path(),
&optimized_config,
self.hnsw_global_config(),
)
}
#[cfg(any(test, feature = "testing"))]
fn optimize_for_test(&self, segments: LockedSegmentHolder, ids: Vec<SegmentId>) -> usize {
let permit_cpu_count = self.num_indexing_threads();
let budget = ResourceBudget::new(permit_cpu_count, permit_cpu_count);
self.optimize(
segments,
ids,
Uuid::new_v4(),
budget.try_acquire(0, permit_cpu_count).unwrap(),
budget,
&AtomicBool::new(false),
ProgressTracker::new_for_test(),
Box::new(|| ()),
)
.unwrap()
}
#[expect(clippy::too_many_arguments)]
fn optimize(
&self,
segment_holder: LockedSegmentHolder,
input_segment_ids: Vec<SegmentId>, output_segment_uuid: Uuid, permit: ResourcePermit,
resource_budget: ResourceBudget,
stopped: &AtomicBool,
progress: ProgressTracker,
on_successful_start: Box<dyn FnOnce()>,
) -> OperationResult<usize>
where
Self: Sync,
{
let paths = OptimizationPaths {
segments_path: self.segments_path().to_path_buf(),
temp_path: self.temp_path().to_path_buf(),
};
let optimization_strategy = ShardOptimizationStrategy { optimizer: self };
let result = execute_optimization(
self.name(),
segment_holder,
input_segment_ids,
output_segment_uuid,
self.threshold_config().deferred_internal_id,
&paths,
permit,
resource_budget,
stopped,
progress,
self.get_telemetry_counter(),
&optimization_strategy,
on_successful_start,
)?;
Ok(result.points_count)
}
}
pub struct OptimizationPlanner<'a> {
remaining: BTreeMap<SegmentId, &'a Arc<RwLock<Segment>>>,
scheduled: Vec<(Option<Arc<Optimizer>>, Vec<SegmentId>)>,
running: usize,
optimizer: Option<Arc<Optimizer>>,
}
impl<'a> OptimizationPlanner<'a> {
pub fn new<I>(running: usize, segments: I) -> Self
where
I: IntoIterator<Item = (SegmentId, &'a Arc<RwLock<Segment>>)>,
{
Self {
remaining: segments.into_iter().collect(),
scheduled: Vec::new(),
running,
optimizer: None,
}
}
pub fn remaining(&self) -> &BTreeMap<SegmentId, &'a Arc<RwLock<Segment>>> {
&self.remaining
}
#[cfg(any(test, feature = "testing"))]
pub fn into_scheduled_for_test(self) -> Vec<Vec<SegmentId>> {
self.scheduled
.into_iter()
.map(|(_, segments)| segments)
.collect_vec()
}
pub fn expected_segments_number(&self) -> usize {
self.remaining.len() + self.scheduled.len() + self.running
}
pub fn plan(&mut self, segments: Vec<SegmentId>) {
debug_assert!(!segments.is_empty());
for segment_id in &segments {
let removed = self.remaining.remove(segment_id).is_some();
debug_assert!(removed);
}
self.scheduled.push((self.optimizer.clone(), segments));
}
}
pub fn plan_optimizations(
segments: &SegmentHolder,
optimizers: &[Arc<Optimizer>],
) -> Vec<(Arc<Optimizer>, Vec<SegmentId>)> {
let mut planner = OptimizationPlanner::new(
segments.running_optimizations.count(),
segments.iter_original(),
);
for optimizer in optimizers {
planner.optimizer = Some(Arc::clone(optimizer));
optimizer.plan_optimizations(&mut planner);
}
planner
.scheduled
.into_iter()
.inspect(|(optimizer, _segments)| debug_assert!(optimizer.is_some()))
.filter_map(|(optimizer, segments)| Some((optimizer?, segments)))
.collect()
}