Spaces:
Build error
Build error
use std::fs::create_dir_all; | |
use std::ops::Deref as _; | |
use std::path::{Path, PathBuf}; | |
use std::sync::atomic::AtomicBool; | |
use std::sync::Arc; | |
use std::thread; | |
use atomic_refcell::AtomicRefCell; | |
use bitvec::prelude::BitSlice; | |
use bitvec::vec::BitVec; | |
use common::counter::hardware_counter::HardwareCounterCell; | |
use common::cpu::linux_low_thread_priority; | |
use common::cpu::{get_num_cpus, CpuPermit}; | |
use common::types::{PointOffsetType, ScoredPointOffset, TelemetryDetail}; | |
use log::debug; | |
use memory::mmap_ops; | |
use parking_lot::Mutex; | |
use rand::thread_rng; | |
use rayon::prelude::*; | |
use rayon::ThreadPool; | |
use super::graph_links::{GraphLinks, GraphLinksMmap}; | |
use crate::common::operation_error::{check_process_stopped, OperationError, OperationResult}; | |
use crate::common::operation_time_statistics::{ | |
OperationDurationsAggregator, ScopeDurationMeasurer, | |
}; | |
use crate::common::BYTES_IN_KB; | |
use crate::data_types::query_context::VectorQueryContext; | |
use crate::data_types::vectors::{QueryVector, VectorInternal, VectorRef}; | |
use crate::id_tracker::IdTrackerSS; | |
use crate::index::hnsw_index::build_condition_checker::BuildConditionChecker; | |
use crate::index::hnsw_index::config::HnswGraphConfig; | |
use crate::index::hnsw_index::graph_layers::GraphLayers; | |
use crate::index::hnsw_index::graph_layers_builder::GraphLayersBuilder; | |
use crate::index::hnsw_index::point_scorer::FilteredScorer; | |
use crate::index::query_estimator::adjust_to_available_vectors; | |
use crate::index::sample_estimation::sample_check_cardinality; | |
use crate::index::struct_payload_index::StructPayloadIndex; | |
use crate::index::visited_pool::{VisitedListHandle, VisitedPool}; | |
use crate::index::{PayloadIndex, VectorIndex}; | |
use crate::telemetry::VectorIndexSearchesTelemetry; | |
use crate::types::Condition::Field; | |
use crate::types::{ | |
default_quantization_ignore_value, default_quantization_oversampling_value, FieldCondition, | |
Filter, HnswConfig, QuantizationSearchParams, SearchParams, | |
}; | |
use crate::vector_storage::quantized::quantized_vectors::QuantizedVectors; | |
use crate::vector_storage::query::DiscoveryQuery; | |
use crate::vector_storage::{ | |
new_raw_scorer, new_stoppable_raw_scorer, RawScorer, VectorStorage, VectorStorageEnum, | |
}; | |
const HNSW_USE_HEURISTIC: bool = true; | |
/// Build first N points in HNSW graph using only a single thread, to avoid | |
/// disconnected components in the graph. | |
const SINGLE_THREADED_HNSW_BUILD_THRESHOLD: usize = 32; | |
const SINGLE_THREADED_HNSW_BUILD_THRESHOLD: usize = 256; | |
pub struct HNSWIndex<TGraphLinks: GraphLinks> { | |
id_tracker: Arc<AtomicRefCell<IdTrackerSS>>, | |
vector_storage: Arc<AtomicRefCell<VectorStorageEnum>>, | |
quantized_vectors: Arc<AtomicRefCell<Option<QuantizedVectors>>>, | |
payload_index: Arc<AtomicRefCell<StructPayloadIndex>>, | |
config: HnswGraphConfig, | |
path: PathBuf, | |
graph: GraphLayers<TGraphLinks>, | |
searches_telemetry: HNSWSearchesTelemetry, | |
} | |
struct HNSWSearchesTelemetry { | |
unfiltered_plain: Arc<Mutex<OperationDurationsAggregator>>, | |
filtered_plain: Arc<Mutex<OperationDurationsAggregator>>, | |
unfiltered_hnsw: Arc<Mutex<OperationDurationsAggregator>>, | |
small_cardinality: Arc<Mutex<OperationDurationsAggregator>>, | |
large_cardinality: Arc<Mutex<OperationDurationsAggregator>>, | |
exact_filtered: Arc<Mutex<OperationDurationsAggregator>>, | |
exact_unfiltered: Arc<Mutex<OperationDurationsAggregator>>, | |
} | |
pub struct HnswIndexOpenArgs<'a> { | |
pub path: &'a Path, | |
pub id_tracker: Arc<AtomicRefCell<IdTrackerSS>>, | |
pub vector_storage: Arc<AtomicRefCell<VectorStorageEnum>>, | |
pub quantized_vectors: Arc<AtomicRefCell<Option<QuantizedVectors>>>, | |
pub payload_index: Arc<AtomicRefCell<StructPayloadIndex>>, | |
pub hnsw_config: HnswConfig, | |
pub permit: Option<Arc<CpuPermit>>, | |
pub stopped: &'a AtomicBool, | |
} | |
impl<TGraphLinks: GraphLinks> HNSWIndex<TGraphLinks> { | |
pub fn open(args: HnswIndexOpenArgs<'_>) -> OperationResult<Self> { | |
let HnswIndexOpenArgs { | |
path, | |
id_tracker, | |
vector_storage, | |
quantized_vectors, | |
payload_index, | |
hnsw_config, | |
permit, | |
stopped, | |
} = args; | |
create_dir_all(path)?; | |
let config_path = HnswGraphConfig::get_config_path(path); | |
let graph_path = GraphLayers::<TGraphLinks>::get_path(path); | |
let graph_links_path = GraphLayers::<TGraphLinks>::get_links_path(path); | |
let (config, graph) = if graph_path.exists() { | |
let config = if config_path.exists() { | |
HnswGraphConfig::load(&config_path)? | |
} else { | |
let vector_storage = vector_storage.borrow(); | |
let available_vectors = vector_storage.available_vector_count(); | |
let full_scan_threshold = vector_storage | |
.size_of_available_vectors_in_bytes() | |
.checked_div(available_vectors) | |
.and_then(|avg_vector_size| { | |
hnsw_config | |
.full_scan_threshold | |
.saturating_mul(BYTES_IN_KB) | |
.checked_div(avg_vector_size) | |
}) | |
.unwrap_or(1); | |
HnswGraphConfig::new( | |
hnsw_config.m, | |
hnsw_config.ef_construct, | |
full_scan_threshold, | |
hnsw_config.max_indexing_threads, | |
hnsw_config.payload_m, | |
available_vectors, | |
) | |
}; | |
(config, GraphLayers::load(&graph_path, &graph_links_path)?) | |
} else { | |
let num_cpus = match permit { | |
Some(p) => p.num_cpus as usize, | |
None => { | |
log::warn!("Rebuilding HNSW index"); | |
// We have no CPU permit, meaning this call is not triggered by the segment | |
// optimizer which is supposed to be the only entity that builds an HNSW index. | |
// This should never be executed unless files are removed manually. | |
debug_assert!(false); | |
get_num_cpus() | |
} | |
}; | |
let (config, graph) = Self::build_index( | |
path, | |
id_tracker.as_ref().borrow().deref(), | |
&vector_storage.borrow(), | |
&quantized_vectors.borrow(), | |
&payload_index.borrow(), | |
&hnsw_config, | |
num_cpus, | |
stopped, | |
)?; | |
config.save(&config_path)?; | |
graph.save(&graph_path)?; | |
(config, graph) | |
}; | |
Ok(HNSWIndex { | |
id_tracker, | |
vector_storage, | |
quantized_vectors, | |
payload_index, | |
config, | |
path: path.to_owned(), | |
graph, | |
searches_telemetry: HNSWSearchesTelemetry { | |
unfiltered_hnsw: OperationDurationsAggregator::new(), | |
unfiltered_plain: OperationDurationsAggregator::new(), | |
filtered_plain: OperationDurationsAggregator::new(), | |
small_cardinality: OperationDurationsAggregator::new(), | |
large_cardinality: OperationDurationsAggregator::new(), | |
exact_filtered: OperationDurationsAggregator::new(), | |
exact_unfiltered: OperationDurationsAggregator::new(), | |
}, | |
}) | |
} | |
pub(super) fn graph(&self) -> &GraphLayers<TGraphLinks> { | |
&self.graph | |
} | |
pub fn get_quantized_vectors(&self) -> Arc<AtomicRefCell<Option<QuantizedVectors>>> { | |
self.quantized_vectors.clone() | |
} | |
fn build_index( | |
path: &Path, | |
id_tracker: &IdTrackerSS, | |
vector_storage: &VectorStorageEnum, | |
quantized_vectors: &Option<QuantizedVectors>, | |
payload_index: &StructPayloadIndex, | |
hnsw_config: &HnswConfig, | |
num_cpus: usize, | |
stopped: &AtomicBool, | |
) -> OperationResult<(HnswGraphConfig, GraphLayers<TGraphLinks>)> { | |
let total_vector_count = vector_storage.total_vector_count(); | |
let full_scan_threshold = vector_storage | |
.size_of_available_vectors_in_bytes() | |
.checked_div(total_vector_count) | |
.and_then(|avg_vector_size| { | |
hnsw_config | |
.full_scan_threshold | |
.saturating_mul(BYTES_IN_KB) | |
.checked_div(avg_vector_size) | |
}) | |
.unwrap_or(1); | |
let mut config = HnswGraphConfig::new( | |
hnsw_config.m, | |
hnsw_config.ef_construct, | |
full_scan_threshold, | |
hnsw_config.max_indexing_threads, | |
hnsw_config.payload_m, | |
total_vector_count, | |
); | |
// Build main index graph | |
let mut rng = thread_rng(); | |
let deleted_bitslice = vector_storage.deleted_vector_bitslice(); | |
debug!("building HNSW for {total_vector_count} vectors with {num_cpus} CPUs"); | |
let mut graph_layers_builder = GraphLayersBuilder::new( | |
total_vector_count, | |
config.m, | |
config.m0, | |
config.ef_construct, | |
std::cmp::max( | |
1, | |
total_vector_count | |
.checked_div(full_scan_threshold) | |
.unwrap_or(0) | |
* 10, | |
), | |
HNSW_USE_HEURISTIC, | |
); | |
let pool = rayon::ThreadPoolBuilder::new() | |
.thread_name(|idx| format!("hnsw-build-{idx}")) | |
.num_threads(num_cpus) | |
.spawn_handler(|thread| { | |
let mut b = thread::Builder::new(); | |
if let Some(name) = thread.name() { | |
b = b.name(name.to_owned()); | |
} | |
if let Some(stack_size) = thread.stack_size() { | |
b = b.stack_size(stack_size); | |
} | |
b.spawn(|| { | |
// On Linux, use lower thread priority so we interfere less with serving traffic | |
if let Err(err) = linux_low_thread_priority() { | |
log::debug!( | |
"Failed to set low thread priority for HNSW building, ignoring: {err}" | |
); | |
} | |
thread.run() | |
})?; | |
Ok(()) | |
}) | |
.build()?; | |
for vector_id in id_tracker.iter_ids_excluding(deleted_bitslice) { | |
check_process_stopped(stopped)?; | |
let level = graph_layers_builder.get_random_layer(&mut rng); | |
graph_layers_builder.set_levels(vector_id, level); | |
} | |
let mut indexed_vectors = 0; | |
if config.m > 0 { | |
let mut ids_iterator = id_tracker.iter_ids_excluding(deleted_bitslice); | |
let first_few_ids: Vec<_> = ids_iterator | |
.by_ref() | |
.take(SINGLE_THREADED_HNSW_BUILD_THRESHOLD) | |
.collect(); | |
let ids: Vec<_> = ids_iterator.collect(); | |
indexed_vectors = ids.len() + first_few_ids.len(); | |
let insert_point = |vector_id| { | |
check_process_stopped(stopped)?; | |
let vector = vector_storage.get_vector(vector_id); | |
let vector = vector.as_vec_ref().into(); | |
let raw_scorer = if let Some(quantized_storage) = quantized_vectors.as_ref() { | |
quantized_storage.raw_scorer( | |
vector, | |
id_tracker.deleted_point_bitslice(), | |
vector_storage.deleted_vector_bitslice(), | |
stopped, | |
) | |
} else { | |
new_raw_scorer(vector, vector_storage, id_tracker.deleted_point_bitslice()) | |
}?; | |
let points_scorer = FilteredScorer::new(raw_scorer.as_ref(), None); | |
graph_layers_builder.link_new_point(vector_id, points_scorer); | |
// Ignore hardware counter, for internal operations | |
raw_scorer.take_hardware_counter().discard_results(); | |
Ok::<_, OperationError>(()) | |
}; | |
for vector_id in first_few_ids { | |
insert_point(vector_id)?; | |
} | |
if !ids.is_empty() { | |
pool.install(|| ids.into_par_iter().try_for_each(insert_point))?; | |
} | |
debug!("finish main graph"); | |
} else { | |
debug!("skip building main HNSW graph"); | |
} | |
let visited_pool = VisitedPool::new(); | |
let mut block_filter_list = visited_pool.get(total_vector_count); | |
let payload_m = config.payload_m.unwrap_or(config.m); | |
if payload_m > 0 { | |
// Calculate true average number of links per vertex in the HNSW graph | |
// to better estimate percolation threshold | |
let average_links_per_0_level = | |
graph_layers_builder.get_average_connectivity_on_level(0); | |
let average_links_per_0_level_int = (average_links_per_0_level as usize).max(1); | |
let mut indexed_vectors_set = if config.m != 0 { | |
// Every vector is already indexed in the main graph, so skip counting. | |
BitVec::new() | |
} else { | |
BitVec::repeat(false, total_vector_count) | |
}; | |
for (field, _) in payload_index.indexed_fields() { | |
debug!("building additional index for field {}", &field); | |
// It is expected, that graph will become disconnected less than | |
// $1/m$ points left. | |
// So blocks larger than $1/m$ are not needed. | |
// We add multiplier for the extra safety. | |
let percolation_multiplier = 4; | |
let max_block_size = if config.m > 0 { | |
total_vector_count / average_links_per_0_level_int * percolation_multiplier | |
} else { | |
usize::MAX | |
}; | |
for payload_block in payload_index.payload_blocks(&field, full_scan_threshold) { | |
check_process_stopped(stopped)?; | |
if payload_block.cardinality > max_block_size { | |
continue; | |
} | |
// ToDo: reuse graph layer for same payload | |
let mut additional_graph = GraphLayersBuilder::new_with_params( | |
total_vector_count, | |
payload_m, | |
config.payload_m0.unwrap_or(config.m0), | |
config.ef_construct, | |
1, | |
HNSW_USE_HEURISTIC, | |
false, | |
); | |
Self::build_filtered_graph( | |
id_tracker, | |
vector_storage, | |
quantized_vectors, | |
payload_index, | |
&pool, | |
stopped, | |
&mut additional_graph, | |
payload_block.condition, | |
&mut block_filter_list, | |
&mut indexed_vectors_set, | |
)?; | |
graph_layers_builder.merge_from_other(additional_graph); | |
} | |
} | |
let indexed_payload_vectors = indexed_vectors_set.count_ones(); | |
debug_assert!(indexed_vectors >= indexed_payload_vectors || config.m == 0); | |
indexed_vectors = indexed_vectors.max(indexed_payload_vectors); | |
debug_assert!(indexed_payload_vectors <= total_vector_count); | |
} else { | |
debug!("skip building additional HNSW links"); | |
} | |
config.indexed_vector_count.replace(indexed_vectors); | |
let graph_links_path = GraphLayers::<TGraphLinks>::get_links_path(path); | |
let graph: GraphLayers<TGraphLinks> = | |
graph_layers_builder.into_graph_layers(Some(&graph_links_path))?; | |
{ | |
for (idx, deleted) in deleted_bitslice.iter().enumerate() { | |
if *deleted { | |
debug_assert!(graph | |
.links | |
.links(idx as PointOffsetType, 0) | |
.next() | |
.is_none()); | |
} | |
} | |
} | |
debug!("finish additional payload field indexing"); | |
Ok((config, graph)) | |
} | |
fn build_filtered_graph( | |
id_tracker: &IdTrackerSS, | |
vector_storage: &VectorStorageEnum, | |
quantized_vectors: &Option<QuantizedVectors>, | |
payload_index: &StructPayloadIndex, | |
pool: &ThreadPool, | |
stopped: &AtomicBool, | |
graph_layers_builder: &mut GraphLayersBuilder, | |
condition: FieldCondition, | |
block_filter_list: &mut VisitedListHandle, | |
indexed_vectors_set: &mut BitVec, | |
) -> OperationResult<()> { | |
block_filter_list.next_iteration(); | |
let filter = Filter::new_must(Field(condition)); | |
let deleted_bitslice = vector_storage.deleted_vector_bitslice(); | |
let cardinality_estimation = payload_index.estimate_cardinality(&filter); | |
let points_to_index: Vec<_> = payload_index | |
.iter_filtered_points(&filter, id_tracker, &cardinality_estimation) | |
.filter(|&point_id| { | |
!deleted_bitslice | |
.get(point_id as usize) | |
.map(|x| *x) | |
.unwrap_or(false) | |
}) | |
.collect(); | |
for block_point_id in points_to_index.iter().copied() { | |
block_filter_list.check_and_update_visited(block_point_id); | |
if !indexed_vectors_set.is_empty() { | |
indexed_vectors_set.set(block_point_id as usize, true); | |
} | |
} | |
let insert_points = |block_point_id| { | |
check_process_stopped(stopped)?; | |
let vector = vector_storage.get_vector(block_point_id); | |
let vector = vector.as_vec_ref().into(); | |
let raw_scorer = match quantized_vectors.as_ref() { | |
Some(quantized_storage) => quantized_storage.raw_scorer( | |
vector, | |
id_tracker.deleted_point_bitslice(), | |
deleted_bitslice, | |
stopped, | |
), | |
None => new_raw_scorer(vector, vector_storage, id_tracker.deleted_point_bitslice()), | |
}?; | |
let block_condition_checker = BuildConditionChecker { | |
filter_list: block_filter_list, | |
current_point: block_point_id, | |
}; | |
let points_scorer = | |
FilteredScorer::new(raw_scorer.as_ref(), Some(&block_condition_checker)); | |
graph_layers_builder.link_new_point(block_point_id, points_scorer); | |
// Ignore hardware counter, for internal operations | |
raw_scorer.take_hardware_counter().discard_results(); | |
Ok::<_, OperationError>(()) | |
}; | |
let first_points = points_to_index | |
.len() | |
.min(SINGLE_THREADED_HNSW_BUILD_THRESHOLD); | |
// First index points in single thread so ensure warm start for parallel indexing process | |
for point_id in points_to_index[..first_points].iter().copied() { | |
insert_points(point_id)?; | |
} | |
// Once initial structure is built, index remaining points in parallel | |
// So that each thread will insert points in different parts of the graph, | |
// it is less likely that they will compete for the same locks | |
if points_to_index.len() > first_points { | |
pool.install(|| { | |
points_to_index | |
.into_par_iter() | |
.skip(first_points) | |
.try_for_each(insert_points) | |
})?; | |
} | |
Ok(()) | |
} | |
fn search_with_graph( | |
&self, | |
vector: &QueryVector, | |
filter: Option<&Filter>, | |
top: usize, | |
params: Option<&SearchParams>, | |
custom_entry_points: Option<&[PointOffsetType]>, | |
vector_query_context: &VectorQueryContext, | |
) -> OperationResult<Vec<ScoredPointOffset>> { | |
let ef = params | |
.and_then(|params| params.hnsw_ef) | |
.unwrap_or(self.config.ef); | |
let is_stopped = vector_query_context.is_stopped(); | |
let id_tracker = self.id_tracker.borrow(); | |
let payload_index = self.payload_index.borrow(); | |
let vector_storage = self.vector_storage.borrow(); | |
let quantized_vectors = self.quantized_vectors.borrow(); | |
let deleted_points = vector_query_context | |
.deleted_points() | |
.unwrap_or_else(|| id_tracker.deleted_point_bitslice()); | |
let raw_scorer = Self::construct_search_scorer( | |
vector, | |
&vector_storage, | |
quantized_vectors.as_ref(), | |
deleted_points, | |
params, | |
&is_stopped, | |
)?; | |
let oversampled_top = Self::get_oversampled_top(quantized_vectors.as_ref(), params, top); | |
let filter_context = filter.map(|f| payload_index.filter_context(f)); | |
let points_scorer = FilteredScorer::new(raw_scorer.as_ref(), filter_context.as_deref()); | |
let search_result = | |
self.graph | |
.search(oversampled_top, ef, points_scorer, custom_entry_points); | |
let hw_counter = HardwareCounterCell::new(); | |
let res = self.postprocess_search_result( | |
search_result, | |
vector, | |
params, | |
top, | |
&is_stopped, | |
&hw_counter, | |
)?; | |
vector_query_context.apply_hardware_counter(raw_scorer.take_hardware_counter()); | |
vector_query_context.apply_hardware_counter(hw_counter); | |
Ok(res) | |
} | |
fn search_vectors_with_graph( | |
&self, | |
vectors: &[&QueryVector], | |
filter: Option<&Filter>, | |
top: usize, | |
params: Option<&SearchParams>, | |
vector_query_context: &VectorQueryContext, | |
) -> OperationResult<Vec<Vec<ScoredPointOffset>>> { | |
vectors | |
.iter() | |
.map(|&vector| match vector { | |
QueryVector::Discovery(discovery_query) => self.discovery_search_with_graph( | |
discovery_query.clone(), | |
filter, | |
top, | |
params, | |
vector_query_context, | |
), | |
other => { | |
self.search_with_graph(other, filter, top, params, None, vector_query_context) | |
} | |
}) | |
.collect() | |
} | |
fn search_plain_iterator( | |
&self, | |
vector: &QueryVector, | |
points: &mut dyn Iterator<Item = PointOffsetType>, | |
top: usize, | |
params: Option<&SearchParams>, | |
vector_query_context: &VectorQueryContext, | |
) -> OperationResult<Vec<ScoredPointOffset>> { | |
let id_tracker = self.id_tracker.borrow(); | |
let vector_storage = self.vector_storage.borrow(); | |
let quantized_vectors = self.quantized_vectors.borrow(); | |
let deleted_points = vector_query_context | |
.deleted_points() | |
.unwrap_or_else(|| id_tracker.deleted_point_bitslice()); | |
let is_stopped = vector_query_context.is_stopped(); | |
let raw_scorer = Self::construct_search_scorer( | |
vector, | |
&vector_storage, | |
quantized_vectors.as_ref(), | |
deleted_points, | |
params, | |
&is_stopped, | |
)?; | |
let oversampled_top = Self::get_oversampled_top(quantized_vectors.as_ref(), params, top); | |
let search_result = raw_scorer.peek_top_iter(points, oversampled_top); | |
vector_query_context.apply_hardware_counter(raw_scorer.take_hardware_counter()); | |
let hw_counter = HardwareCounterCell::new(); | |
let res = self.postprocess_search_result( | |
search_result, | |
vector, | |
params, | |
top, | |
&is_stopped, | |
&hw_counter, | |
)?; | |
vector_query_context.apply_hardware_counter(hw_counter); | |
Ok(res) | |
} | |
fn search_plain( | |
&self, | |
vector: &QueryVector, | |
filtered_points: &[PointOffsetType], | |
top: usize, | |
params: Option<&SearchParams>, | |
vector_query_context: &VectorQueryContext, | |
) -> OperationResult<Vec<ScoredPointOffset>> { | |
self.search_plain_iterator( | |
vector, | |
&mut filtered_points.iter().copied(), | |
top, | |
params, | |
vector_query_context, | |
) | |
} | |
fn search_plain_unfiltered( | |
&self, | |
vector: &QueryVector, | |
top: usize, | |
params: Option<&SearchParams>, | |
vector_query_context: &VectorQueryContext, | |
) -> OperationResult<Vec<ScoredPointOffset>> { | |
let id_tracker = self.id_tracker.borrow(); | |
let mut ids_iterator = id_tracker.iter_internal(); | |
self.search_plain_iterator(vector, &mut ids_iterator, top, params, vector_query_context) | |
} | |
fn search_vectors_plain( | |
&self, | |
vectors: &[&QueryVector], | |
filter: &Filter, | |
top: usize, | |
params: Option<&SearchParams>, | |
vector_query_context: &VectorQueryContext, | |
) -> OperationResult<Vec<Vec<ScoredPointOffset>>> { | |
let payload_index = self.payload_index.borrow(); | |
// share filtered points for all query vectors | |
let filtered_points = payload_index.query_points(filter); | |
vectors | |
.iter() | |
.map(|vector| { | |
self.search_plain(vector, &filtered_points, top, params, vector_query_context) | |
}) | |
.collect() | |
} | |
fn discovery_search_with_graph( | |
&self, | |
discovery_query: DiscoveryQuery<VectorInternal>, | |
filter: Option<&Filter>, | |
top: usize, | |
params: Option<&SearchParams>, | |
vector_query_context: &VectorQueryContext, | |
) -> OperationResult<Vec<ScoredPointOffset>> { | |
// Stage 1: Find best entry points using Context search | |
let query_vector = QueryVector::Context(discovery_query.pairs.clone().into()); | |
const DISCOVERY_ENTRY_POINT_COUNT: usize = 10; | |
let custom_entry_points: Vec<_> = self | |
.search_with_graph( | |
&query_vector, | |
filter, | |
DISCOVERY_ENTRY_POINT_COUNT, | |
params, | |
None, | |
vector_query_context, | |
) | |
.map(|search_result| search_result.iter().map(|x| x.idx).collect())?; | |
// Stage 2: Discovery search with entry points | |
let query_vector = QueryVector::Discovery(discovery_query); | |
self.search_with_graph( | |
&query_vector, | |
filter, | |
top, | |
params, | |
Some(&custom_entry_points), | |
vector_query_context, | |
) | |
} | |
fn is_quantized_search( | |
quantized_storage: Option<&QuantizedVectors>, | |
params: Option<&SearchParams>, | |
) -> bool { | |
let ignore_quantization = params | |
.and_then(|p| p.quantization) | |
.map(|q| q.ignore) | |
.unwrap_or(default_quantization_ignore_value()); | |
quantized_storage.is_some() && !ignore_quantization | |
} | |
fn construct_search_scorer<'a>( | |
vector: &QueryVector, | |
vector_storage: &'a VectorStorageEnum, | |
quantized_storage: Option<&'a QuantizedVectors>, | |
deleted_points: &'a BitSlice, | |
params: Option<&SearchParams>, | |
is_stopped: &'a AtomicBool, | |
) -> OperationResult<Box<dyn RawScorer + 'a>> { | |
let quantization_enabled = Self::is_quantized_search(quantized_storage, params); | |
match quantized_storage { | |
Some(quantized_storage) if quantization_enabled => quantized_storage.raw_scorer( | |
vector.to_owned(), | |
deleted_points, | |
vector_storage.deleted_vector_bitslice(), | |
is_stopped, | |
), | |
_ => new_stoppable_raw_scorer( | |
vector.to_owned(), | |
vector_storage, | |
deleted_points, | |
is_stopped, | |
), | |
} | |
} | |
fn get_oversampled_top( | |
quantized_storage: Option<&QuantizedVectors>, | |
params: Option<&SearchParams>, | |
top: usize, | |
) -> usize { | |
let quantization_enabled = Self::is_quantized_search(quantized_storage, params); | |
let oversampling_value = params | |
.and_then(|p| p.quantization) | |
.map(|q| q.oversampling) | |
.unwrap_or(default_quantization_oversampling_value()); | |
match oversampling_value { | |
Some(oversampling) if quantization_enabled && oversampling > 1.0 => { | |
(oversampling * top as f64) as usize | |
} | |
_ => top, | |
} | |
} | |
fn postprocess_search_result( | |
&self, | |
search_result: Vec<ScoredPointOffset>, | |
vector: &QueryVector, | |
params: Option<&SearchParams>, | |
top: usize, | |
is_stopped: &AtomicBool, | |
hardware_counter: &HardwareCounterCell, | |
) -> OperationResult<Vec<ScoredPointOffset>> { | |
let id_tracker = self.id_tracker.borrow(); | |
let vector_storage = self.vector_storage.borrow(); | |
let quantized_vectors = self.quantized_vectors.borrow(); | |
let quantization_enabled = Self::is_quantized_search(quantized_vectors.as_ref(), params); | |
let default_rescoring = quantized_vectors | |
.as_ref() | |
.map(|q| q.default_rescoring()) | |
.unwrap_or(false); | |
let rescore = quantization_enabled | |
&& params | |
.and_then(|p| p.quantization) | |
.and_then(|q| q.rescore) | |
.unwrap_or(default_rescoring); | |
let mut postprocess_result = if rescore { | |
let raw_scorer = new_stoppable_raw_scorer( | |
vector.to_owned(), | |
&vector_storage, | |
id_tracker.deleted_point_bitslice(), | |
is_stopped, | |
)?; | |
let mut ids_iterator = search_result.iter().map(|x| x.idx); | |
let mut re_scored = raw_scorer.score_points_unfiltered(&mut ids_iterator); | |
hardware_counter.apply_from(raw_scorer.take_hardware_counter()); | |
re_scored.sort_unstable(); | |
re_scored.reverse(); | |
re_scored | |
} else { | |
search_result | |
}; | |
postprocess_result.truncate(top); | |
Ok(postprocess_result) | |
} | |
} | |
impl HNSWIndex<GraphLinksMmap> { | |
pub fn prefault_mmap_pages(&self) -> mmap_ops::PrefaultMmapPages { | |
self.graph.prefault_mmap_pages(&self.path) | |
} | |
} | |
impl<TGraphLinks: GraphLinks> VectorIndex for HNSWIndex<TGraphLinks> { | |
fn search( | |
&self, | |
vectors: &[&QueryVector], | |
filter: Option<&Filter>, | |
top: usize, | |
params: Option<&SearchParams>, | |
query_context: &VectorQueryContext, | |
) -> OperationResult<Vec<Vec<ScoredPointOffset>>> { | |
// If neither `m` nor `payload_m` is set, HNSW doesn't have any links. | |
// And if so, we need to fall back to plain search (optionally, with quantization). | |
let is_hnsw_disabled = self.config.m == 0 && self.config.payload_m.unwrap_or(0) == 0; | |
let exact = params.map(|params| params.exact).unwrap_or(false); | |
let exact_params = if exact { | |
params.map(|params| { | |
let mut params = *params; | |
params.quantization = Some(QuantizationSearchParams { | |
ignore: true, | |
rescore: Some(false), | |
oversampling: None, | |
}); // disable quantization for exact search | |
params | |
}) | |
} else { | |
None | |
}; | |
match filter { | |
None => { | |
let vector_storage = self.vector_storage.borrow(); | |
// Determine whether to do a plain or graph search, and pick search timer aggregator | |
// Because an HNSW graph is built, we'd normally always assume to search the graph. | |
// But because a lot of points may be deleted in this graph, it may just be faster | |
// to do a plain search instead. | |
let plain_search = exact | |
|| is_hnsw_disabled | |
|| vector_storage.available_vector_count() < self.config.full_scan_threshold; | |
// Do plain or graph search | |
if plain_search { | |
let _timer = ScopeDurationMeasurer::new(if exact { | |
&self.searches_telemetry.exact_unfiltered | |
} else { | |
&self.searches_telemetry.unfiltered_plain | |
}); | |
let params_ref = if exact { exact_params.as_ref() } else { params }; | |
vectors | |
.iter() | |
.map(|&vector| { | |
self.search_plain_unfiltered(vector, top, params_ref, query_context) | |
}) | |
.collect() | |
} else { | |
let _timer = | |
ScopeDurationMeasurer::new(&self.searches_telemetry.unfiltered_hnsw); | |
self.search_vectors_with_graph(vectors, None, top, params, query_context) | |
} | |
} | |
Some(query_filter) => { | |
// depending on the amount of filtered-out points the optimal strategy could be | |
// - to retrieve possible points and score them after | |
// - to use HNSW index with filtering condition | |
// if exact search is requested, we should not use HNSW index | |
if exact || is_hnsw_disabled { | |
let _timer = ScopeDurationMeasurer::new(if exact { | |
&self.searches_telemetry.exact_filtered | |
} else { | |
&self.searches_telemetry.filtered_plain | |
}); | |
let params_ref = if exact { exact_params.as_ref() } else { params }; | |
return self.search_vectors_plain( | |
vectors, | |
query_filter, | |
top, | |
params_ref, | |
query_context, | |
); | |
} | |
let payload_index = self.payload_index.borrow(); | |
let vector_storage = self.vector_storage.borrow(); | |
let id_tracker = self.id_tracker.borrow(); | |
let available_vector_count = vector_storage.available_vector_count(); | |
let query_point_cardinality = payload_index.estimate_cardinality(query_filter); | |
let query_cardinality = adjust_to_available_vectors( | |
query_point_cardinality, | |
available_vector_count, | |
id_tracker.available_point_count(), | |
); | |
if query_cardinality.max < self.config.full_scan_threshold { | |
// if cardinality is small - use plain index | |
let _timer = | |
ScopeDurationMeasurer::new(&self.searches_telemetry.small_cardinality); | |
return self.search_vectors_plain( | |
vectors, | |
query_filter, | |
top, | |
params, | |
query_context, | |
); | |
} | |
if query_cardinality.min > self.config.full_scan_threshold { | |
// if cardinality is high enough - use HNSW index | |
let _timer = | |
ScopeDurationMeasurer::new(&self.searches_telemetry.large_cardinality); | |
return self.search_vectors_with_graph( | |
vectors, | |
filter, | |
top, | |
params, | |
query_context, | |
); | |
} | |
let filter_context = payload_index.filter_context(query_filter); | |
// Fast cardinality estimation is not enough, do sample estimation of cardinality | |
let id_tracker = self.id_tracker.borrow(); | |
if sample_check_cardinality( | |
id_tracker.sample_ids(Some(vector_storage.deleted_vector_bitslice())), | |
|idx| filter_context.check(idx), | |
self.config.full_scan_threshold, | |
available_vector_count, // Check cardinality among available vectors | |
) { | |
// if cardinality is high enough - use HNSW index | |
let _timer = | |
ScopeDurationMeasurer::new(&self.searches_telemetry.large_cardinality); | |
self.search_vectors_with_graph(vectors, filter, top, params, query_context) | |
} else { | |
// if cardinality is small - use plain index | |
let _timer = | |
ScopeDurationMeasurer::new(&self.searches_telemetry.small_cardinality); | |
self.search_vectors_plain(vectors, query_filter, top, params, query_context) | |
} | |
} | |
} | |
} | |
fn get_telemetry_data(&self, detail: TelemetryDetail) -> VectorIndexSearchesTelemetry { | |
let tm = &self.searches_telemetry; | |
VectorIndexSearchesTelemetry { | |
index_name: None, | |
unfiltered_plain: tm.unfiltered_plain.lock().get_statistics(detail), | |
filtered_plain: tm.filtered_plain.lock().get_statistics(detail), | |
unfiltered_hnsw: tm.unfiltered_hnsw.lock().get_statistics(detail), | |
filtered_small_cardinality: tm.small_cardinality.lock().get_statistics(detail), | |
filtered_large_cardinality: tm.large_cardinality.lock().get_statistics(detail), | |
filtered_exact: tm.exact_filtered.lock().get_statistics(detail), | |
filtered_sparse: Default::default(), | |
unfiltered_exact: tm.exact_unfiltered.lock().get_statistics(detail), | |
unfiltered_sparse: Default::default(), | |
} | |
} | |
fn files(&self) -> Vec<PathBuf> { | |
[ | |
GraphLayers::<TGraphLinks>::get_path(&self.path), | |
GraphLayers::<TGraphLinks>::get_links_path(&self.path), | |
HnswGraphConfig::get_config_path(&self.path), | |
] | |
.into_iter() | |
.filter(|p| p.exists()) | |
.collect() | |
} | |
fn indexed_vector_count(&self) -> usize { | |
self.config | |
.indexed_vector_count | |
// If indexed vector count is unknown, fall back to number of points | |
.unwrap_or_else(|| self.graph.num_points()) | |
} | |
fn update_vector( | |
&mut self, | |
_id: PointOffsetType, | |
_vector: Option<VectorRef>, | |
) -> OperationResult<()> { | |
Err(OperationError::service_error("Cannot update HNSW index")) | |
} | |
} | |