Skip to main content

riffdb_types/
vector.rs

1//! Native vector search types.
2
3use std::fmt;
4use std::num::NonZeroU32;
5
6/// The closed set of supported vector distance metrics.
7///
8/// Each metric defines how similarity is computed between vectors.
9#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10pub enum DistanceMetric {
11    /// Cosine similarity (1 - cosine_similarity as distance).
12    Cosine,
13    /// Euclidean (L2) distance.
14    Euclidean,
15    /// Negative inner (dot) product distance.
16    DotProduct,
17}
18
19impl DistanceMetric {
20    /// Durable format tag for versioned encoding.
21    #[must_use]
22    pub const fn tag(self) -> u8 {
23        match self {
24            Self::Cosine => 0x01,
25            Self::Euclidean => 0x02,
26            Self::DotProduct => 0x03,
27        }
28    }
29
30    /// Decodes from a durable format tag.
31    #[must_use]
32    pub const fn from_tag(tag: u8) -> Option<Self> {
33        match tag {
34            0x01 => Some(Self::Cosine),
35            0x02 => Some(Self::Euclidean),
36            0x03 => Some(Self::DotProduct),
37            _ => None,
38        }
39    }
40
41    /// The source-language keyword for this metric.
42    #[must_use]
43    pub const fn keyword(self) -> &'static str {
44        match self {
45            Self::Cosine => "cosine",
46            Self::Euclidean => "euclidean",
47            Self::DotProduct => "dot_product",
48        }
49    }
50}
51
52impl fmt::Display for DistanceMetric {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        f.write_str(self.keyword())
55    }
56}
57
58/// Maximum supported vector dimension (4,096 covers all common embedding models).
59pub const MAX_VECTOR_DIMENSION: u32 = 4_096;
60
61/// A validated positive vector dimension.
62#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
63pub struct VectorDimension(NonZeroU32);
64
65impl VectorDimension {
66    /// Creates a validated dimension. Rejects zero and values above `MAX_VECTOR_DIMENSION`.
67    #[must_use]
68    pub const fn new(value: u32) -> Option<Self> {
69        if value == 0 || value > MAX_VECTOR_DIMENSION {
70            return None;
71        }
72        match NonZeroU32::new(value) {
73            Some(inner) => Some(Self(inner)),
74            None => None,
75        }
76    }
77
78    /// Returns the raw dimension value.
79    #[must_use]
80    pub const fn get(self) -> u32 {
81        self.0.get()
82    }
83}
84
85impl fmt::Display for VectorDimension {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(f, "{}", self.0)
88    }
89}
90
91/// A validated positive stale-entity count threshold.
92///
93/// V1 compares this threshold directly with the authoritative count of stale
94/// entities. Duration-based staleness is reserved for a future amendment.
95#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
96pub struct StaleEntityCountThreshold(NonZeroU32);
97
98impl StaleEntityCountThreshold {
99    /// Creates a positive bounded count threshold. Zero is rejected.
100    #[must_use]
101    pub const fn new(value: u32) -> Option<Self> {
102        match NonZeroU32::new(value) {
103            Some(inner) => Some(Self(inner)),
104            None => None,
105        }
106    }
107
108    /// Returns the declared stale-entity count threshold.
109    #[must_use]
110    pub const fn get(self) -> u32 {
111        self.0.get()
112    }
113
114    /// Returns whether an authoritative stale count breaches this threshold.
115    #[must_use]
116    pub const fn is_breached_by(self, stale_count: u64) -> bool {
117        stale_count > self.0.get() as u64
118    }
119}
120
121impl fmt::Display for StaleEntityCountThreshold {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        write!(f, "{} stale entities", self.0)
124    }
125}
126
127/// Typed rejection of an invalid canonical vector construction.
128#[derive(Clone, Copy, Debug, Eq, PartialEq)]
129pub enum CanonicalVectorError {
130    /// The component count is zero or exceeds `MAX_VECTOR_DIMENSION`.
131    DimensionOutOfRange {
132        /// Observed component count.
133        actual: usize,
134        /// Maximum supported dimension.
135        maximum: u32,
136    },
137    /// A component is NaN or infinite. Vectors carrying non-finite
138    /// components are garbage inputs and never become canonical values.
139    NonFiniteComponent {
140        /// Zero-based index of the first offending component.
141        index: usize,
142    },
143}
144
145impl fmt::Display for CanonicalVectorError {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        match self {
148            Self::DimensionOutOfRange { actual, maximum } => write!(
149                f,
150                "vector has {actual} components; the dimension must be in 1..={maximum}"
151            ),
152            Self::NonFiniteComponent { index } => {
153                write!(f, "vector component {index} is not a finite number")
154            }
155        }
156    }
157}
158
159impl std::error::Error for CanonicalVectorError {}
160
161/// A validated, dimension-checked vector of f32 values in canonical form.
162///
163/// Stored as authoritative entity state. Dimension is fixed at construction
164/// and matches the contract-declared `VectorDimension`.
165///
166/// # Canonical form
167///
168/// Every construction path (constructor and canonical decoder) enforces one
169/// canonical form: all components are finite, and negative zero is
170/// canonicalized to positive zero. Within that domain IEEE `f32` equality,
171/// bitwise comparison, and bitwise hashing all agree, so the derived
172/// `PartialEq`, the bitwise `Hash`/`Ord` below, and the durable canonical
173/// digest are mutually consistent (`a == b` implies `digest(a) == digest(b)`).
174/// This discipline is what makes vectors a safe exception to the
175/// no-business-floats rule in this crate.
176#[derive(Clone, PartialEq)]
177pub struct CanonicalVector {
178    /// The f32 components in declaration order, in canonical form.
179    components: Vec<f32>,
180}
181crate::inspect_owned_capacity_struct_v1!(CanonicalVector {
182    owned: [components],
183    inline: []
184});
185
186impl CanonicalVector {
187    /// Creates a vector from components, enforcing canonical form.
188    ///
189    /// Rejects an empty or over-`MAX_VECTOR_DIMENSION` component list and any
190    /// NaN or infinite component as typed errors; canonicalizes `-0.0` to
191    /// `+0.0`.
192    pub fn new(mut components: Vec<f32>) -> Result<Self, CanonicalVectorError> {
193        let len = components.len();
194        if len == 0 || len > MAX_VECTOR_DIMENSION as usize {
195            return Err(CanonicalVectorError::DimensionOutOfRange {
196                actual: len,
197                maximum: MAX_VECTOR_DIMENSION,
198            });
199        }
200        for (index, component) in components.iter_mut().enumerate() {
201            if !component.is_finite() {
202                return Err(CanonicalVectorError::NonFiniteComponent { index });
203            }
204            if *component == 0.0 {
205                // Canonicalize -0.0 to +0.0 so IEEE equality and the bitwise
206                // Hash/Ord/digest agree on the one canonical representation.
207                *component = 0.0;
208            }
209        }
210        Ok(Self { components })
211    }
212
213    /// The number of components (the dimension).
214    #[must_use]
215    pub fn dimension(&self) -> u32 {
216        self.components.len() as u32
217    }
218
219    /// The raw f32 components.
220    #[must_use]
221    pub fn components(&self) -> &[f32] {
222        &self.components
223    }
224
225    /// Consumes self and returns the owned components.
226    #[must_use]
227    pub fn into_components(self) -> Vec<f32> {
228        self.components
229    }
230
231    /// Byte size for budget accounting (4 bytes per f32 component).
232    #[must_use]
233    pub fn byte_size(&self) -> usize {
234        self.components.len() * 4
235    }
236}
237
238impl fmt::Debug for CanonicalVector {
239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        write!(f, "CanonicalVector(dim={},[REDACTED])", self.dimension())
241    }
242}
243
244// True because construction enforces canonical form: components are finite
245// and -0.0 is canonicalized, so IEEE equality is reflexive here and identical
246// to bitwise equality.
247impl Eq for CanonicalVector {}
248
249// Hash and Ord are bitwise over the canonical form. Within the canonical
250// domain bitwise equality coincides with the derived IEEE `PartialEq`, so
251// `Hash`/`Ord` are consistent with `Eq`. Note `Ord` is a total order for
252// container use (length, then component bits); it is NOT a numeric order —
253// vectors have no meaningful numeric order.
254impl std::hash::Hash for CanonicalVector {
255    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
256        for component in &self.components {
257            state.write_u32(component.to_bits());
258        }
259    }
260}
261
262impl PartialOrd for CanonicalVector {
263    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
264        Some(self.cmp(other))
265    }
266}
267
268impl Ord for CanonicalVector {
269    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
270        self.components
271            .len()
272            .cmp(&other.components.len())
273            .then_with(|| {
274                for (a, b) in self.components.iter().zip(other.components.iter()) {
275                    let ordering = a.to_bits().cmp(&b.to_bits());
276                    if ordering != std::cmp::Ordering::Equal {
277                        return ordering;
278                    }
279                }
280                std::cmp::Ordering::Equal
281            })
282    }
283}
284
285/// Metadata about an embedding write for staleness and model-version tracking.
286#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
287pub struct EmbeddingMetadata {
288    /// The model identity string (e.g. "text-embedding-3-small").
289    model_identity: String,
290    /// The model version string (e.g. "2024-01-25").
291    model_version: String,
292}
293
294impl EmbeddingMetadata {
295    /// Maximum length for model identity and version strings.
296    pub const MAX_MODEL_STRING_LEN: usize = 256;
297
298    /// Creates validated embedding metadata.
299    #[must_use]
300    pub fn new(
301        model_identity: impl Into<String>,
302        model_version: impl Into<String>,
303    ) -> Option<Self> {
304        let model_identity = model_identity.into();
305        let model_version = model_version.into();
306        if model_identity.is_empty()
307            || model_identity.len() > Self::MAX_MODEL_STRING_LEN
308            || model_version.is_empty()
309            || model_version.len() > Self::MAX_MODEL_STRING_LEN
310        {
311            return None;
312        }
313        Some(Self {
314            model_identity,
315            model_version,
316        })
317    }
318
319    /// The model identity.
320    #[must_use]
321    pub fn model_identity(&self) -> &str {
322        &self.model_identity
323    }
324
325    /// The model version.
326    #[must_use]
327    pub fn model_version(&self) -> &str {
328        &self.model_version
329    }
330}
331
332// Exhaustive non-durable ownership inventories for exact-provider admission.
333crate::inspect_owned_capacity_copy_v1!(VectorDimension);
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    #[test]
340    fn stale_entity_count_threshold_is_positive_and_strict() {
341        assert!(StaleEntityCountThreshold::new(0).is_none());
342        let threshold = StaleEntityCountThreshold::new(3).expect("positive threshold");
343        assert_eq!(threshold.get(), 3);
344        assert!(!threshold.is_breached_by(3));
345        assert!(threshold.is_breached_by(4));
346    }
347
348    #[test]
349    fn construction_rejects_non_finite_components_as_typed_errors() {
350        for (components, index) in [
351            (vec![f32::NAN], 0),
352            (vec![1.0, f32::INFINITY], 1),
353            (vec![1.0, 2.0, f32::NEG_INFINITY], 2),
354        ] {
355            assert_eq!(
356                CanonicalVector::new(components),
357                Err(CanonicalVectorError::NonFiniteComponent { index })
358            );
359        }
360    }
361
362    #[test]
363    fn construction_rejects_out_of_range_dimensions() {
364        assert_eq!(
365            CanonicalVector::new(Vec::new()),
366            Err(CanonicalVectorError::DimensionOutOfRange {
367                actual: 0,
368                maximum: MAX_VECTOR_DIMENSION,
369            })
370        );
371        let oversized = vec![0.5; MAX_VECTOR_DIMENSION as usize + 1];
372        assert_eq!(
373            CanonicalVector::new(oversized),
374            Err(CanonicalVectorError::DimensionOutOfRange {
375                actual: MAX_VECTOR_DIMENSION as usize + 1,
376                maximum: MAX_VECTOR_DIMENSION,
377            })
378        );
379        assert!(CanonicalVector::new(vec![0.5; MAX_VECTOR_DIMENSION as usize]).is_ok());
380    }
381
382    #[test]
383    fn negative_zero_is_canonicalized_at_construction() {
384        let negative = CanonicalVector::new(vec![-0.0_f32]).expect("finite");
385        let positive = CanonicalVector::new(vec![0.0_f32]).expect("finite");
386        // One canonical representation: bit-identical components.
387        assert_eq!(
388            negative.components()[0].to_bits(),
389            positive.components()[0].to_bits()
390        );
391        assert_eq!(negative, positive);
392        assert_eq!(negative.cmp(&positive), std::cmp::Ordering::Equal);
393        let hash_of = |vector: &CanonicalVector| {
394            use std::hash::{Hash, Hasher};
395            let mut hasher = std::collections::hash_map::DefaultHasher::new();
396            vector.hash(&mut hasher);
397            hasher.finish()
398        };
399        assert_eq!(hash_of(&negative), hash_of(&positive));
400    }
401
402    #[test]
403    fn equality_is_reflexive_and_agrees_with_ordering() {
404        let vector = CanonicalVector::new(vec![1.5, -2.25, 0.0]).expect("finite");
405        // Reflexivity: with only finite canonical components, IEEE equality
406        // is reflexive (a NaN component previously made `v == v` false).
407        assert_eq!(vector, vector.clone());
408        assert_eq!(vector.cmp(&vector.clone()), std::cmp::Ordering::Equal);
409        let other = CanonicalVector::new(vec![1.5, -2.25, 0.5]).expect("finite");
410        assert_ne!(vector, other);
411        assert_ne!(vector.cmp(&other), std::cmp::Ordering::Equal);
412    }
413}
414
415crate::inspect_owned_capacity_copy_v1!(DistanceMetric);
416
417crate::inspect_owned_capacity_struct_v1!(EmbeddingMetadata {
418    owned: [model_identity, model_version],
419    inline: []
420});