Skip to main content

entelix_memory/
vector.rs

1//! Shared vector validation helpers for embedder and vector-store boundaries.
2
3use entelix_core::{Error, Result};
4
5/// Return the first non-finite vector element, if any.
6///
7/// Provider-backed embedders use this to classify malformed provider
8/// responses, while vector stores use [`validate_vector_shape`] to
9/// reject invalid caller-supplied vectors before they reach an index.
10#[must_use]
11pub fn first_non_finite_vector_value(vector: &[f32]) -> Option<(usize, f32)> {
12    vector
13        .iter()
14        .copied()
15        .enumerate()
16        .find(|(_, value)| !value.is_finite())
17}
18
19/// Validate vector dimension and finite values for vector-store calls.
20///
21/// Reach for this on the caller-input boundary of every
22/// `VectorStore` impl (`add` / `search` / `update`) so all
23/// backends reject the same two malformations — wrong dimension
24/// and non-finite element — with identical error wording.
25/// Returns [`Error::InvalidRequest`] because vectors passed into a
26/// [`crate::VectorStore`] are caller input at the store boundary.
27pub fn validate_vector_shape(
28    surface: &str,
29    label: &str,
30    vector: &[f32],
31    expected_dimension: usize,
32) -> Result<()> {
33    if vector.len() != expected_dimension {
34        return Err(Error::invalid_request(format!(
35            "{surface}: {label} dimension {} does not match index dimension {expected_dimension}",
36            vector.len()
37        )));
38    }
39    if let Some((index, value)) = first_non_finite_vector_value(vector) {
40        return Err(Error::invalid_request(format!(
41            "{surface}: {label} contains non-finite value at index {index}: {value}"
42        )));
43    }
44    Ok(())
45}
46
47#[cfg(test)]
48mod tests {
49    use super::{first_non_finite_vector_value, validate_vector_shape};
50    use entelix_core::Error;
51
52    #[test]
53    fn first_non_finite_vector_value_reports_position_and_value() {
54        assert_eq!(first_non_finite_vector_value(&[1.0, 2.0]), None);
55        let Some((index, value)) = first_non_finite_vector_value(&[1.0, f32::INFINITY, f32::NAN])
56        else {
57            panic!("expected non-finite vector value");
58        };
59        assert_eq!(index, 1);
60        assert!(value.is_infinite());
61    }
62
63    #[test]
64    fn validate_vector_shape_rejects_dimension_mismatch_and_non_finite_values() {
65        assert!(validate_vector_shape("Store::add", "vector", &[1.0, 2.0], 2).is_ok());
66
67        let Err(err) = validate_vector_shape("Store::add", "vector", &[1.0], 2) else {
68            panic!("dimension mismatch should be rejected");
69        };
70        assert!(matches!(err, Error::InvalidRequest(msg) if msg.contains("dimension")));
71
72        let Err(err) = validate_vector_shape("Store::add", "vector", &[f32::NAN, 2.0], 2) else {
73            panic!("non-finite vector should be rejected");
74        };
75        assert!(matches!(err, Error::InvalidRequest(msg) if msg.contains("non-finite")));
76    }
77}