1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
use crate::error::SearchError;
/// Distance function used by an index.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum DistanceMetric {
/// One minus cosine similarity.
#[default]
Cosine,
/// Negative inner product. Smaller values are better.
Dot,
/// Squared Euclidean distance.
SquaredEuclidean,
}
/// Immutable HNSW construction and query policy.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexConfig {
/// Number of scalar components in every stored vector and query.
pub dimensions: usize,
/// Distance metric used for graph construction and search.
pub metric: DistanceMetric,
/// Outgoing upper-layer link budget. Layer zero uses twice this budget;
/// retained reverse links may increase a node's final degree.
pub connectivity: usize,
/// Candidate width used while constructing graph links.
pub expansion_build: usize,
/// Candidate width used by approximate queries.
pub expansion_query: usize,
/// Independently seeded deterministic HNSW graphs.
pub replicas: usize,
/// Maximum worker budget shared by replicas and bulk-construction waves.
pub build_threads: usize,
/// Maximum workers used by [`crate::VectorIndex::search_batch`].
pub query_threads: usize,
/// Fixed seed for levels and insertion order.
pub seed: u64,
}
impl IndexConfig {
/// Creates a portable default configuration for `dimensions`.
#[must_use]
pub fn new(dimensions: usize) -> Self {
let workers = std::thread::available_parallelism()
.map_or(1, std::num::NonZeroUsize::get)
.min(16);
Self {
dimensions,
metric: DistanceMetric::Cosine,
connectivity: 12,
expansion_build: 48,
expansion_query: 24,
replicas: 1,
build_threads: workers,
query_threads: workers,
seed: 0x6a09_e667_f3bc_c909,
}
}
/// Validates dimensions, graph widths, and worker bounds.
///
/// # Errors
///
/// Returns [`SearchError::InvalidConfig`] for an unusable value or
/// [`SearchError::CapacityOverflow`] when degree arithmetic overflows.
pub fn validate(&self) -> Result<(), SearchError> {
if self.dimensions == 0 {
return Err(SearchError::InvalidConfig("dimensions must be non-zero"));
}
if self.connectivity < 2 {
return Err(SearchError::InvalidConfig(
"connectivity must be at least two",
));
}
if self.expansion_build < self.connectivity {
return Err(SearchError::InvalidConfig(
"expansion_build must be at least connectivity",
));
}
if self.expansion_query == 0 {
return Err(SearchError::InvalidConfig(
"expansion_query must be non-zero",
));
}
if self.replicas == 0 {
return Err(SearchError::InvalidConfig("replicas must be non-zero"));
}
if self.build_threads == 0 {
return Err(SearchError::InvalidConfig("build_threads must be non-zero"));
}
if self.query_threads == 0 {
return Err(SearchError::InvalidConfig("query_threads must be non-zero"));
}
self.connectivity
.checked_mul(2)
.ok_or(SearchError::CapacityOverflow)?;
Ok(())
}
}