Skip to main content

mongreldb_core/
embedding.rs

1//! Pluggable embedding generation (optional layer over stored vectors).
2//!
3//! MongrelDB **never hard-codes an external embedding vendor**. Dense ANN
4//! indexes operate only on already-materialized vectors plus model metadata.
5//! Sparse retrieval needs no embedding model at all.
6//!
7//! # Sources
8//!
9//! - [`EmbeddingSource::SuppliedByApplication`] — the default: the client
10//!   writes `Value::Embedding` directly. No generation runs inside the engine.
11//! - [`EmbeddingSource::LocalModel`] — Kit or the server may load a local
12//!   model from disk (bundled or operator-provided). Core only stores the
13//!   path/id; a registered [`EmbeddingProvider`] performs inference.
14//! - [`EmbeddingSource::GeneratedColumn`] — a named provider registered with
15//!   the process (local or remote). The registry is process-local; storage
16//!   remains vendor-independent.
17//!
18//! # Policy
19//!
20//! **Do not invent arbitrary dense vectors** (hashed pseudo-embeddings, random
21//! noise, etc.) merely to claim Dense ANN is in use. A weak hashed vector can
22//! perform worse than MongrelDB's native Sparse index while consuming more
23//! storage and creating misleading "semantic search" expectations. Prefer
24//! sparse retrieval when no real embedding model is available.
25
26use std::collections::BTreeMap;
27use std::path::PathBuf;
28use std::sync::{Arc, RwLock};
29
30use serde::{Deserialize, Serialize};
31
32/// Where embedding values for a column originate.
33///
34/// Catalog metadata only: the storage engine never calls out to a vendor
35/// based on this enum. Resolution goes through [`EmbeddingProviderRegistry`].
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
37#[serde(tag = "kind", rename_all = "snake_case")]
38pub enum EmbeddingSource {
39    /// Application supplies `Value::Embedding` on write. Default for every
40    /// `TypeId::Embedding` column that omits an explicit source.
41    #[default]
42    SuppliedByApplication,
43    /// Local on-disk model (Kit-bundled or operator-installed). Inference
44    /// requires a registered provider that accepts this `model_id`.
45    LocalModel {
46        /// Filesystem path to model weights / tokenizer bundle.
47        model_path: PathBuf,
48        /// Stable model identity recorded with ANN generations.
49        model_id: String,
50    },
51    /// Named provider registered on the server/process
52    /// (`EmbeddingProviderRegistry::register`). May be local or remote; core
53    /// does not interpret the provider string.
54    GeneratedColumn {
55        /// Registry key of the provider.
56        provider: String,
57    },
58}
59
60impl EmbeddingSource {
61    /// Human-readable label for diagnostics and catalog dumps.
62    pub fn label(&self) -> &str {
63        match self {
64            Self::SuppliedByApplication => "supplied_by_application",
65            Self::LocalModel { .. } => "local_model",
66            Self::GeneratedColumn { .. } => "generated_column",
67        }
68    }
69
70    /// Model identity when known (for ANN generation metadata); `None` for
71    /// application-supplied vectors that carry no model stamp.
72    pub fn model_id(&self) -> Option<&str> {
73        match self {
74            Self::SuppliedByApplication => None,
75            Self::LocalModel { model_id, .. } => Some(model_id.as_str()),
76            Self::GeneratedColumn { provider } => Some(provider.as_str()),
77        }
78    }
79
80    /// Whether the engine/provider layer is expected to materialize vectors
81    /// (as opposed to the application writing them directly).
82    pub fn requires_provider(&self) -> bool {
83        !matches!(self, Self::SuppliedByApplication)
84    }
85}
86
87/// Errors from the embedding provider layer (not storage).
88#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
89pub enum EmbeddingError {
90    /// No provider is registered under the requested id.
91    #[error("embedding provider {0:?} is not registered")]
92    ProviderNotFound(String),
93    /// Provider exists but cannot produce vectors (missing model file, wrong
94    /// dimension, remote unavailable, etc.).
95    #[error("embedding provider {provider:?}: {message}")]
96    ProviderFailed {
97        /// Provider id.
98        provider: String,
99        /// Operator-facing detail (no secrets).
100        message: String,
101    },
102    /// Application-supplied source was asked to generate vectors.
103    #[error("embedding source is SuppliedByApplication; pass vectors from the client")]
104    SuppliedByApplication,
105    /// Caller asked for a generation path without configuring a source that
106    /// can produce vectors.
107    #[error("no embedding source configured for generation")]
108    NoSource,
109    /// Dimension mismatch between provider output and column definition.
110    #[error("embedding dimension mismatch: expected {expected}, got {got}")]
111    DimensionMismatch {
112        /// Column / index dimension.
113        expected: u32,
114        /// Provider output dimension.
115        got: u32,
116    },
117}
118
119/// One registered embedding backend (local model runner, remote HTTP adapter,
120/// test double, …). Implementations live outside core storage; core only
121/// holds the trait object in a process-local registry.
122pub trait EmbeddingProvider: Send + Sync {
123    /// Stable registry key (matches [`EmbeddingSource::GeneratedColumn`] /
124    /// local model ids).
125    fn provider_id(&self) -> &str;
126
127    /// Model identity stamped onto ANN generations when this provider runs.
128    fn model_id(&self) -> &str;
129
130    /// Fixed output dimension; must match the column's `TypeId::Embedding { dim }`.
131    fn dimension(&self) -> u32;
132
133    /// Encode one or more texts into dense vectors of [`Self::dimension`].
134    ///
135    /// Implementations must **not** invent weak hashed stand-ins for real
136    /// semantic models. Prefer returning [`EmbeddingError::ProviderFailed`]
137    /// over producing misleading vectors.
138    fn embed(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, EmbeddingError>;
139}
140
141/// Process-local registry of embedding providers.
142///
143/// Empty by default: dense search works when applications supply vectors;
144/// sparse search works with no provider at all.
145#[derive(Clone, Default)]
146pub struct EmbeddingProviderRegistry {
147    inner: Arc<RwLock<BTreeMap<String, Arc<dyn EmbeddingProvider>>>>,
148}
149
150impl std::fmt::Debug for EmbeddingProviderRegistry {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        let guard = self.inner.read().unwrap_or_else(|e| e.into_inner());
153        f.debug_struct("EmbeddingProviderRegistry")
154            .field("providers", &guard.keys().cloned().collect::<Vec<_>>())
155            .finish()
156    }
157}
158
159impl EmbeddingProviderRegistry {
160    /// Empty registry (no vendors pre-registered).
161    pub fn new() -> Self {
162        Self::default()
163    }
164
165    /// Register or replace a provider under its [`EmbeddingProvider::provider_id`].
166    pub fn register(&self, provider: Arc<dyn EmbeddingProvider>) {
167        let id = provider.provider_id().to_owned();
168        let mut guard = self.inner.write().unwrap_or_else(|e| e.into_inner());
169        guard.insert(id, provider);
170    }
171
172    /// Remove a provider by id. Returns whether it was present.
173    pub fn unregister(&self, provider_id: &str) -> bool {
174        let mut guard = self.inner.write().unwrap_or_else(|e| e.into_inner());
175        guard.remove(provider_id).is_some()
176    }
177
178    /// Lookup by registry key.
179    pub fn get(&self, provider_id: &str) -> Option<Arc<dyn EmbeddingProvider>> {
180        let guard = self.inner.read().unwrap_or_else(|e| e.into_inner());
181        guard.get(provider_id).cloned()
182    }
183
184    /// Sorted list of registered provider ids.
185    pub fn list_ids(&self) -> Vec<String> {
186        let guard = self.inner.read().unwrap_or_else(|e| e.into_inner());
187        guard.keys().cloned().collect()
188    }
189
190    /// Resolve a source to a provider, or refuse generation for
191    /// application-supplied columns.
192    pub fn resolve(
193        &self,
194        source: &EmbeddingSource,
195    ) -> Result<Arc<dyn EmbeddingProvider>, EmbeddingError> {
196        match source {
197            EmbeddingSource::SuppliedByApplication => Err(EmbeddingError::SuppliedByApplication),
198            EmbeddingSource::LocalModel { model_id, .. } => self
199                .get(model_id)
200                .ok_or_else(|| EmbeddingError::ProviderNotFound(model_id.clone())),
201            EmbeddingSource::GeneratedColumn { provider } => self
202                .get(provider)
203                .ok_or_else(|| EmbeddingError::ProviderNotFound(provider.clone())),
204        }
205    }
206
207    /// Generate embeddings for `texts` under `source`, checking `expected_dim`.
208    ///
209    /// For [`EmbeddingSource::SuppliedByApplication`] this always errors —
210    /// callers must write vectors themselves.
211    pub fn embed(
212        &self,
213        source: &EmbeddingSource,
214        texts: &[&str],
215        expected_dim: u32,
216    ) -> Result<Vec<Vec<f32>>, EmbeddingError> {
217        let provider = self.resolve(source)?;
218        if provider.dimension() != expected_dim {
219            return Err(EmbeddingError::DimensionMismatch {
220                expected: expected_dim,
221                got: provider.dimension(),
222            });
223        }
224        let vectors = provider.embed(texts)?;
225        for v in &vectors {
226            if v.len() as u32 != expected_dim {
227                return Err(EmbeddingError::DimensionMismatch {
228                    expected: expected_dim,
229                    got: v.len() as u32,
230                });
231            }
232            if v.iter().any(|x| !x.is_finite()) {
233                return Err(EmbeddingError::ProviderFailed {
234                    provider: provider.provider_id().to_owned(),
235                    message: "provider returned non-finite embedding values".into(),
236                });
237            }
238        }
239        Ok(vectors)
240    }
241}
242
243/// Test/demo provider that only echoes a fixed finite vector. **Not** a
244/// semantic model — used solely to exercise the registry plumbing. Production
245/// code should never present this as "semantic search."
246#[derive(Debug, Clone)]
247pub struct FixedVectorProvider {
248    /// Registry key.
249    pub id: String,
250    /// Model stamp.
251    pub model_id: String,
252    /// Output vector (also defines dimension).
253    pub vector: Vec<f32>,
254}
255
256impl EmbeddingProvider for FixedVectorProvider {
257    fn provider_id(&self) -> &str {
258        &self.id
259    }
260
261    fn model_id(&self) -> &str {
262        &self.model_id
263    }
264
265    fn dimension(&self) -> u32 {
266        self.vector.len() as u32
267    }
268
269    fn embed(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, EmbeddingError> {
270        Ok(texts.iter().map(|_| self.vector.clone()).collect())
271    }
272}
273
274/// Metadata recorded alongside an ANN index generation so readers know which
275/// model produced the stored vectors (when known).
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
277pub struct EmbeddingModelMeta {
278    /// Optional model / provider identity.
279    #[serde(default, skip_serializing_if = "Option::is_none")]
280    pub model_id: Option<String>,
281    /// Optional human-readable source label (`supplied_by_application`, …).
282    #[serde(default, skip_serializing_if = "Option::is_none")]
283    pub source_kind: Option<String>,
284    /// Embedding dimension.
285    pub dim: u32,
286}
287
288impl EmbeddingModelMeta {
289    /// Build from a column source and dimension.
290    pub fn from_source(source: &EmbeddingSource, dim: u32) -> Self {
291        Self {
292            model_id: source.model_id().map(str::to_owned),
293            source_kind: Some(source.label().to_owned()),
294            dim,
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn default_source_is_application_supplied() {
305        assert_eq!(
306            EmbeddingSource::default(),
307            EmbeddingSource::SuppliedByApplication
308        );
309        assert!(!EmbeddingSource::SuppliedByApplication.requires_provider());
310    }
311
312    #[test]
313    fn registry_is_empty_by_default_no_vendor() {
314        let reg = EmbeddingProviderRegistry::new();
315        assert!(reg.list_ids().is_empty());
316        assert!(matches!(
317            reg.resolve(&EmbeddingSource::GeneratedColumn {
318                provider: "openai".into()
319            }),
320            Err(EmbeddingError::ProviderNotFound(_))
321        ));
322    }
323
324    #[test]
325    fn supplied_by_application_refuses_generation() {
326        let reg = EmbeddingProviderRegistry::new();
327        let err = reg
328            .embed(&EmbeddingSource::SuppliedByApplication, &["hello"], 4)
329            .unwrap_err();
330        assert!(matches!(err, EmbeddingError::SuppliedByApplication));
331    }
332
333    #[test]
334    fn registered_provider_embeds_with_dim_check() {
335        let reg = EmbeddingProviderRegistry::new();
336        reg.register(Arc::new(FixedVectorProvider {
337            id: "local-test".into(),
338            model_id: "fixed-v1".into(),
339            vector: vec![0.0, 1.0, 0.0, 0.0],
340        }));
341        let source = EmbeddingSource::GeneratedColumn {
342            provider: "local-test".into(),
343        };
344        let out = reg.embed(&source, &["a", "b"], 4).unwrap();
345        assert_eq!(out.len(), 2);
346        assert_eq!(out[0], vec![0.0, 1.0, 0.0, 0.0]);
347        let dim_err = reg.embed(&source, &["a"], 8).unwrap_err();
348        assert!(matches!(
349            dim_err,
350            EmbeddingError::DimensionMismatch {
351                expected: 8,
352                got: 4
353            }
354        ));
355    }
356
357    #[test]
358    fn local_model_source_resolves_by_model_id() {
359        let reg = EmbeddingProviderRegistry::new();
360        reg.register(Arc::new(FixedVectorProvider {
361            id: "kit-mini".into(),
362            model_id: "kit-mini".into(),
363            vector: vec![1.0, 0.0],
364        }));
365        let source = EmbeddingSource::LocalModel {
366            model_path: PathBuf::from("/models/kit-mini"),
367            model_id: "kit-mini".into(),
368        };
369        assert_eq!(source.model_id(), Some("kit-mini"));
370        let out = reg.embed(&source, &["q"], 2).unwrap();
371        assert_eq!(out[0].len(), 2);
372    }
373
374    #[test]
375    fn model_meta_from_source() {
376        let meta = EmbeddingModelMeta::from_source(&EmbeddingSource::SuppliedByApplication, 768);
377        assert_eq!(meta.dim, 768);
378        assert_eq!(meta.model_id, None);
379        assert_eq!(meta.source_kind.as_deref(), Some("supplied_by_application"));
380    }
381
382    #[test]
383    fn sparse_path_needs_no_provider() {
384        // Documentation invariant: sparse retrieval is independent of this
385        // registry. Presence of an empty registry must not block sparse.
386        let reg = EmbeddingProviderRegistry::new();
387        assert!(reg.list_ids().is_empty());
388    }
389}