Skip to main content

zeph_memory/
embed_probe.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Shared embedding-dimension probe helper.
5//!
6//! Several ingest and memory subsystems need to learn an embedding provider's output
7//! dimension before creating a Qdrant collection: they call the provider once with a
8//! throwaway probe string and use the resulting vector's length as the collection's
9//! vector size. [`probe_vector_size`] centralizes that sequence (including an optional
10//! timeout) so callers stop re-implementing it inline.
11
12use std::future::Future;
13use std::time::Duration;
14
15/// Error surfaced by [`probe_vector_size`].
16#[derive(Debug, thiserror::Error)]
17pub enum ProbeError<E> {
18    /// The probe embedding call exceeded `timeout`.
19    #[error("embedding probe timed out after {0:?}")]
20    Timeout(Duration),
21    /// The probe embedding call itself returned an error.
22    #[error("{0}")]
23    Embed(E),
24}
25
26/// Learn an embedding provider's output dimension.
27///
28/// Awaits `embed_fut` — the caller's already-invoked embed call for a throwaway probe string —
29/// and returns the length of the resulting vector. When `timeout` is `Some`, the await is
30/// bounded by it; pass `None` to await unconditionally.
31///
32/// Taking the future itself (rather than a `Fn(&str) -> Fut` closure) sidesteps the higher-rank
33/// lifetime that a borrowing `embed(&self, text: &str)` call would otherwise require, and lets
34/// each caller keep its own probe string literal.
35///
36/// This function only performs the probe — pair the returned size with the caller's own
37/// `ensure_collection` (or `ensure_collection_with_quantization`) call, since collection setup
38/// differs across call sites (plain vs. quantized, fixed vs. caller-supplied collection name).
39///
40/// # Errors
41///
42/// Returns [`ProbeError::Timeout`] if `timeout` elapses, or [`ProbeError::Embed`] if the probe
43/// call fails.
44///
45/// # Examples
46///
47/// ```
48/// use std::time::Duration;
49/// use zeph_memory::embed_probe::probe_vector_size;
50///
51/// # async fn probe() -> Result<(), Box<dyn std::error::Error>> {
52/// let embed_fut = async { Ok::<_, std::convert::Infallible>(vec![0.0_f32; 384]) };
53/// let size = probe_vector_size(embed_fut, Some(Duration::from_secs(15))).await?;
54/// assert_eq!(size, 384);
55/// # Ok(())
56/// # }
57/// ```
58#[tracing::instrument(
59    name = "memory.embed_probe.probe_vector_size",
60    skip_all,
61    err,
62    level = "debug"
63)]
64pub async fn probe_vector_size<Fut, E>(
65    embed_fut: Fut,
66    timeout: Option<Duration>,
67) -> Result<u64, ProbeError<E>>
68where
69    Fut: Future<Output = Result<Vec<f32>, E>>,
70    E: std::fmt::Display,
71{
72    let vector = match timeout {
73        Some(d) => tokio::time::timeout(d, embed_fut)
74            .await
75            .map_err(|_| ProbeError::Timeout(d))?
76            .map_err(ProbeError::Embed)?,
77        None => embed_fut.await.map_err(ProbeError::Embed)?,
78    };
79    // Safe: a Vec<f32> with 4B+ elements is impossible in practice on any 64-bit platform.
80    Ok(vector.len() as u64)
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use std::convert::Infallible;
87
88    #[tokio::test]
89    async fn probe_vector_size_returns_length() {
90        let size = probe_vector_size(async { Ok::<_, Infallible>(vec![0.0_f32; 128]) }, None)
91            .await
92            .unwrap();
93        assert_eq!(size, 128);
94    }
95
96    #[tokio::test]
97    async fn probe_vector_size_propagates_embed_error() {
98        let result = probe_vector_size(async { Err::<Vec<f32>, _>("boom") }, None).await;
99        assert!(matches!(result, Err(ProbeError::Embed("boom"))));
100    }
101
102    #[tokio::test]
103    async fn probe_vector_size_times_out() {
104        let result = probe_vector_size(
105            async {
106                tokio::time::sleep(Duration::from_millis(50)).await;
107                Ok::<_, Infallible>(vec![0.0_f32; 4])
108            },
109            Some(Duration::from_millis(1)),
110        )
111        .await;
112        assert!(matches!(result, Err(ProbeError::Timeout(_))));
113    }
114
115    #[tokio::test]
116    async fn probe_vector_size_no_timeout_awaits_unconditionally() {
117        let size = probe_vector_size(
118            async {
119                tokio::time::sleep(Duration::from_millis(20)).await;
120                Ok::<_, Infallible>(vec![0.0_f32; 7])
121            },
122            None,
123        )
124        .await
125        .unwrap();
126        assert_eq!(size, 7);
127    }
128}