Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions crates/ember-core/src/keyspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1884,10 +1884,13 @@ impl Keyspace {
return Err(VectorWriteError::WrongType);
}

// estimate memory for the new vector
// estimate memory for the new vector (saturating to avoid overflow)
let dim = vector.len();
let per_vector =
dim * quantization.bytes_per_element() + connectivity * 2 * 8 + element.len() + 80;
let per_vector = dim
.saturating_mul(quantization.bytes_per_element())
.saturating_add(connectivity.saturating_mul(16))
.saturating_add(element.len())
.saturating_add(80);
let estimated_increase = if is_new {
memory::ENTRY_OVERHEAD + key.len() + VectorSet::BASE_OVERHEAD + per_vector
} else {
Expand Down Expand Up @@ -1976,7 +1979,7 @@ impl Keyspace {

let removed = match entry.value {
Value::Vector(ref mut vs) => vs.remove(element),
_ => unreachable!(),
_ => return Err(WrongType),
};

if removed {
Expand Down
73 changes: 55 additions & 18 deletions crates/ember-core/src/types/vector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,10 @@ impl VectorSet {
.search(query, count)
.map_err(|e| VectorError::Index(e.to_string()))?;

let mut results = Vec::with_capacity(matches.keys.len());
// usearch should always return matching key/distance counts, but
// guard against library bugs by using the shorter length
let result_count = matches.keys.len().min(matches.distances.len());
let mut results = Vec::with_capacity(result_count);
for (key, distance) in matches.keys.iter().zip(matches.distances.iter()) {
if let Some(name) = self.names.get(key) {
results.push(SearchResult {
Expand Down Expand Up @@ -417,8 +420,10 @@ impl VectorSet {
let count = self.elements.len();

// usearch internal: vector storage + HNSW graph edges
let vector_bytes = count * self.dim * self.quantization.bytes_per_element();
let graph_bytes = count * self.connectivity * 2 * 8; // each edge is a u64 key
let vector_bytes = count
.saturating_mul(self.dim)
.saturating_mul(self.quantization.bytes_per_element());
let graph_bytes = count.saturating_mul(self.connectivity).saturating_mul(16); // each edge is a u64 key

// rust-side hashmaps: elements + names
let name_bytes: usize = self
Expand Down Expand Up @@ -476,22 +481,54 @@ impl Clone for VectorSet {
// try_clone rebuilds the index from scratch. if it fails (e.g. OOM
// inside usearch), return an empty set with the same config rather
// than panicking. data loss is preferable to a server crash.
self.try_clone().unwrap_or_else(|_| {
Self::new(
self.dim,
self.metric,
self.quantization,
self.connectivity,
self.expansion_add,
)
.unwrap_or_else(|_| {
// if we can't even create an empty index with the same
// config, fall back to the smallest possible valid set.
// this only happens under extreme memory pressure.
Self::new(self.dim, self.metric, self.quantization, 2, 2)
.expect("failed to allocate even a minimal index — system is out of memory")
self.try_clone()
.or_else(|_| {
Self::new(
self.dim,
self.metric,
self.quantization,
self.connectivity,
self.expansion_add,
)
})
.or_else(|_| Self::new(self.dim, self.metric, self.quantization, 2, 2))
.unwrap_or_else(|e| {
// absolute last resort — log the error and return a 1-dim
// empty set. this loses data but keeps the server alive.
tracing::error!("VectorSet clone failed under extreme memory pressure: {e}");
Self {
index: Index::new(&IndexOptions {
dimensions: self.dim,
metric: self.metric.to_metric_kind(),
quantization: self.quantization.to_scalar_kind(),
connectivity: 2,
expansion_add: 2,
expansion_search: 0,
multi: false,
})
.unwrap_or_else(|_| {
// if even this fails, create the absolute minimum
Index::new(&IndexOptions {
dimensions: 1,
metric: MetricKind::Cos,
quantization: ScalarKind::F32,
connectivity: 2,
expansion_add: 2,
expansion_search: 0,
multi: false,
})
.expect("cannot allocate 1-dim index — system is critically out of memory")
}),
elements: HashMap::new(),
names: HashMap::new(),
next_key: 0,
dim: self.dim,
metric: self.metric,
quantization: self.quantization,
connectivity: 2,
expansion_add: 2,
}
})
})
}
}

Expand Down
8 changes: 7 additions & 1 deletion crates/ember-persistence/src/aof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,13 @@ impl AofRecord {
let key = read_string(&mut cursor, "key")?;
let element = read_string(&mut cursor, "element")?;
let dim = format::read_u32(&mut cursor)?;
let mut vector = Vec::with_capacity(format::capped_capacity(dim));
if dim > format::MAX_PERSISTED_VECTOR_DIMS {
return Err(FormatError::InvalidData(format!(
"AOF VADD dimension {dim} exceeds max {}",
format::MAX_PERSISTED_VECTOR_DIMS
)));
}
let mut vector = Vec::with_capacity(dim as usize);
for _ in 0..dim {
vector.push(format::read_f32(&mut cursor)?);
}
Expand Down
12 changes: 12 additions & 0 deletions crates/ember-persistence/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ pub enum FormatError {
#[error("unknown record tag: {0}")]
UnknownTag(u8),

#[error("invalid data: {0}")]
InvalidData(String),

#[error("file is encrypted but no encryption key was provided")]
EncryptionRequired,

Expand Down Expand Up @@ -247,6 +250,15 @@ pub fn capped_capacity(count: u32) -> usize {
(count as usize).min(65_536)
}

/// Maximum vector dimensions allowed in persistence formats.
/// Matches the protocol-layer cap. Records exceeding this are rejected
/// during deserialization to prevent OOM from corrupt files.
pub const MAX_PERSISTED_VECTOR_DIMS: u32 = 65_536;

/// Maximum element count per vector set in persistence formats.
/// Prevents corrupt count fields from causing unbounded loops.
pub const MAX_PERSISTED_VECTOR_COUNT: u32 = 10_000_000;

/// Verifies that two CRC32 values match.
pub fn verify_crc32_values(computed: u32, stored: u32) -> Result<(), FormatError> {
if computed != stored {
Expand Down
48 changes: 46 additions & 2 deletions crates/ember-persistence/src/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,37 @@ fn parse_snap_value(r: &mut impl io::Read) -> Result<SnapValue, FormatError> {
#[cfg(feature = "vector")]
TYPE_VECTOR => {
let metric = format::read_u8(r)?;
if metric > 2 {
return Err(FormatError::InvalidData(format!(
"unknown vector metric: {metric}"
)));
}
let quantization = format::read_u8(r)?;
if quantization > 2 {
return Err(FormatError::InvalidData(format!(
"unknown vector quantization: {quantization}"
)));
}
let connectivity = format::read_u32(r)?;
let expansion_add = format::read_u32(r)?;
let dim = format::read_u32(r)?;
if dim > format::MAX_PERSISTED_VECTOR_DIMS {
return Err(FormatError::InvalidData(format!(
"vector dimension {dim} exceeds max {}",
format::MAX_PERSISTED_VECTOR_DIMS
)));
}
let count = format::read_u32(r)?;
if count > format::MAX_PERSISTED_VECTOR_COUNT {
return Err(FormatError::InvalidData(format!(
"vector element count {count} exceeds max {}",
format::MAX_PERSISTED_VECTOR_COUNT
)));
}
let mut elements = Vec::with_capacity(format::capped_capacity(count));
for _ in 0..count {
let name = read_snap_string(r, "vector element name")?;
let mut vector = Vec::with_capacity(format::capped_capacity(dim));
let mut vector = Vec::with_capacity(dim as usize);
for _ in 0..dim {
vector.push(format::read_f32(r)?);
}
Expand Down Expand Up @@ -629,16 +651,38 @@ impl SnapshotReader {
#[cfg(feature = "vector")]
TYPE_VECTOR => {
let metric = format::read_u8(&mut self.reader)?;
if metric > 2 {
return Err(FormatError::InvalidData(format!(
"unknown vector metric: {metric}"
)));
}
format::write_u8(&mut buf, metric)?;
let quantization = format::read_u8(&mut self.reader)?;
if quantization > 2 {
return Err(FormatError::InvalidData(format!(
"unknown vector quantization: {quantization}"
)));
}
format::write_u8(&mut buf, quantization)?;
let connectivity = format::read_u32(&mut self.reader)?;
format::write_u32(&mut buf, connectivity)?;
let expansion_add = format::read_u32(&mut self.reader)?;
format::write_u32(&mut buf, expansion_add)?;
let dim = format::read_u32(&mut self.reader)?;
if dim > format::MAX_PERSISTED_VECTOR_DIMS {
return Err(FormatError::InvalidData(format!(
"vector dimension {dim} exceeds max {}",
format::MAX_PERSISTED_VECTOR_DIMS
)));
}
format::write_u32(&mut buf, dim)?;
let count = format::read_u32(&mut self.reader)?;
if count > format::MAX_PERSISTED_VECTOR_COUNT {
return Err(FormatError::InvalidData(format!(
"vector element count {count} exceeds max {}",
format::MAX_PERSISTED_VECTOR_COUNT
)));
}
format::write_u32(&mut buf, count)?;
let mut elements = Vec::with_capacity(format::capped_capacity(count));
for _ in 0..count {
Expand All @@ -650,7 +694,7 @@ impl SnapshotReader {
"vector element name is not valid utf-8",
))
})?;
let mut vector = Vec::with_capacity(format::capped_capacity(dim));
let mut vector = Vec::with_capacity(dim as usize);
for _ in 0..dim {
let v = format::read_f32(&mut self.reader)?;
format::write_f32(&mut buf, v)?;
Expand Down
Loading