Skip to main content

lean_ctx/core/embeddings/
mod.rs

1//! Embedding engine for semantic code search.
2//!
3//! Provides dense vector embeddings for code chunks using a local ONNX model.
4//! Supports multiple models via `EmbeddingModel` registry — selected via
5//! `LEAN_CTX_EMBEDDING_MODEL` env var (default: all-MiniLM-L6-v2).
6//!
7//! Feature-gated under `embeddings` — falls back gracefully to BM25-only
8//! search when the feature or model is not available.
9//!
10//! Architecture:
11//!   Tokenizer → ONNX Model (rten) → Mean Pooling → L2 Normalize → `Vec<f32>`
12
13pub mod download;
14pub mod model_registry;
15pub mod pooling;
16pub mod tokenizer;
17
18use std::path::{Path, PathBuf};
19
20use model_registry::{EmbeddingModel, ModelConfig, VocabSource};
21use tokenizer::{TokenizedInput, WordPieceTokenizer};
22
23#[cfg(feature = "embeddings")]
24use std::sync::Arc;
25
26#[cfg(feature = "embeddings")]
27use rten::Model;
28
29pub struct EmbeddingEngine {
30    #[cfg(feature = "embeddings")]
31    model: Arc<Model>,
32    tokenizer: TokenizerKind,
33    dimensions: usize,
34    max_seq_len: usize,
35    model_id: EmbeddingModel,
36    model_config: ModelConfig,
37    #[cfg(feature = "embeddings")]
38    graph_inputs: GraphInputs,
39    #[cfg(feature = "embeddings")]
40    output_id: rten::NodeId,
41}
42
43/// Abstraction over different tokenizer backends.
44enum TokenizerKind {
45    WordPiece(WordPieceTokenizer),
46    HfTokenizer(tokenizer::HfTokenizerWrapper),
47}
48
49/// The two ONNX graph topologies we can drive (GL #452).
50///
51/// Transformers take `[1, seq]` id/mask tensors and emit per-token hidden
52/// states `[1, seq, dim]` that we mean-pool. model2vec exports are
53/// EmbeddingBag graphs: flat `input_ids: [n_tokens]` plus `offsets: [batch]`,
54/// already pooled to `[batch, dim]` — ~500x faster, no attention pass.
55#[cfg(feature = "embeddings")]
56enum GraphInputs {
57    Transformer {
58        input_ids: rten::NodeId,
59        attention_mask: rten::NodeId,
60        token_type_ids: Option<rten::NodeId>,
61    },
62    EmbeddingBag {
63        input_ids: rten::NodeId,
64        offsets: rten::NodeId,
65    },
66}
67
68/// Classify the graph topology from its input names (pure, unit-testable).
69/// The model2vec signature is exactly two inputs whose second is `offsets`;
70/// everything else is treated as a transformer.
71#[cfg(feature = "embeddings")]
72fn is_embedding_bag_signature(input_names: &[Option<&str>]) -> bool {
73    input_names.len() == 2 && input_names[1] == Some("offsets")
74}
75
76impl EmbeddingEngine {
77    /// Load embedding model and vocabulary from a directory.
78    /// Downloads model automatically from HuggingFace if not present.
79    #[cfg(feature = "embeddings")]
80    pub fn load(model_dir: &Path) -> anyhow::Result<Self> {
81        let selected = model_registry::resolve_model();
82        Self::load_model(model_dir, selected)
83    }
84
85    /// Load a specific embedding model from a directory.
86    #[cfg(feature = "embeddings")]
87    pub fn load_model(base_dir: &Path, model_id: EmbeddingModel) -> anyhow::Result<Self> {
88        let config = model_id.config();
89        let model_dir = base_dir.join(model_id.storage_dir_name());
90
91        download::ensure_model(&model_dir, &config)?;
92
93        let tokenizer = load_tokenizer(&model_dir, &config)?;
94        let model_path = model_dir.join("model.onnx");
95        let model = Model::load_file(&model_path)?;
96
97        let model_inputs = model.input_ids();
98        if model_inputs.len() < 2 {
99            anyhow::bail!(
100                "Expected model with at least 2 inputs (input_ids, attention_mask), got {}",
101                model_inputs.len()
102            );
103        }
104
105        // Topology detection (GL #452): model2vec EmbeddingBag graphs expose
106        // exactly (input_ids, offsets) — structurally incompatible with the
107        // transformer path, so they get their own input adapter.
108        let names: Vec<Option<&str>> = model_inputs
109            .iter()
110            .map(|id| model.node_info(*id).and_then(|n| n.name()))
111            .collect();
112        let graph_inputs = if is_embedding_bag_signature(&names) {
113            GraphInputs::EmbeddingBag {
114                input_ids: model_inputs[0],
115                offsets: model_inputs[1],
116            }
117        } else {
118            let token_type_ids = if config.needs_token_type_ids {
119                if model_inputs.len() < 3 {
120                    anyhow::bail!(
121                        "Model {} requires token_type_ids but only has {} inputs",
122                        config.name,
123                        model_inputs.len()
124                    );
125                }
126                Some(model_inputs[2])
127            } else if model_inputs.len() >= 3 {
128                Some(model_inputs[2])
129            } else {
130                None
131            };
132            GraphInputs::Transformer {
133                input_ids: model_inputs[0],
134                attention_mask: model_inputs[1],
135                token_type_ids,
136            }
137        };
138
139        let output_id = *model
140            .output_ids()
141            .first()
142            .ok_or_else(|| anyhow::anyhow!("Model has no outputs"))?;
143
144        let dimensions = detect_dimensions(
145            &model,
146            &tokenizer,
147            &graph_inputs,
148            output_id,
149            config.max_seq_len,
150        )
151        .unwrap_or(config.dimensions);
152
153        tracing::info!(
154            "Embedding engine loaded: model={}, {}d, max_seq_len={}, topology={}",
155            config.name,
156            dimensions,
157            config.max_seq_len,
158            match graph_inputs {
159                GraphInputs::Transformer { .. } => "transformer",
160                GraphInputs::EmbeddingBag { .. } => "embedding-bag (model2vec)",
161            },
162        );
163
164        Ok(Self {
165            model: Arc::new(model),
166            tokenizer,
167            dimensions,
168            max_seq_len: config.max_seq_len,
169            model_id,
170            model_config: config,
171            graph_inputs,
172            output_id,
173        })
174    }
175
176    #[cfg(not(feature = "embeddings"))]
177    pub fn load(_model_dir: &Path) -> anyhow::Result<Self> {
178        anyhow::bail!("Embeddings feature not enabled. Compile with --features embeddings")
179    }
180
181    /// Load from default model directory (~/.lean-ctx/models/).
182    pub fn load_default() -> anyhow::Result<Self> {
183        Self::load(&Self::model_directory())
184    }
185
186    /// Generate an embedding vector for a single text (document/code).
187    pub fn embed(&self, text: &str) -> anyhow::Result<Vec<f32>> {
188        let prefixed;
189        let input_text = if let Some(prefix) = &self.model_config.document_prefix {
190            prefixed = format!("{prefix}{text}");
191            &prefixed
192        } else {
193            text
194        };
195        let input = tokenize(&self.tokenizer, input_text, self.max_seq_len);
196        self.run_inference(&input)
197    }
198
199    /// Generate an embedding vector for a query string.
200    /// Applies query-specific prefix if the model requires one.
201    pub fn embed_query(&self, query: &str) -> anyhow::Result<Vec<f32>> {
202        let prefixed;
203        let input_text = if let Some(prefix) = &self.model_config.query_prefix {
204            prefixed = format!("{prefix}{query}");
205            &prefixed
206        } else {
207            query
208        };
209        let input = tokenize(&self.tokenizer, input_text, self.max_seq_len);
210        self.run_inference(&input)
211    }
212
213    /// Generate embedding vectors for multiple texts (documents/code).
214    pub fn embed_batch(&self, texts: &[&str]) -> anyhow::Result<Vec<Vec<f32>>> {
215        texts.iter().map(|t| self.embed(t)).collect()
216    }
217
218    pub fn dimensions(&self) -> usize {
219        self.dimensions
220    }
221
222    pub fn model_id(&self) -> &EmbeddingModel {
223        &self.model_id
224    }
225
226    pub fn model_name(&self) -> &str {
227        &self.model_config.name
228    }
229
230    /// Resolve the model directory (respects LEAN_CTX_MODELS_DIR env).
231    pub fn model_directory() -> PathBuf {
232        if let Ok(dir) = std::env::var("LEAN_CTX_MODELS_DIR") {
233            return PathBuf::from(dir);
234        }
235        if let Ok(d) = crate::core::paths::cache_dir() {
236            return d.join("models");
237        }
238        PathBuf::from("models")
239    }
240
241    /// Check if the model files are present and loadable.
242    pub fn is_available() -> bool {
243        let base_dir = Self::model_directory();
244        let selected = model_registry::resolve_model();
245        let config = selected.config();
246        let model_dir = base_dir.join(selected.storage_dir_name());
247        model_dir.join("model.onnx").exists()
248            && model_dir.join(config.vocab_file.filename()).exists()
249    }
250
251    #[cfg(feature = "embeddings")]
252    fn run_inference(&self, input: &TokenizedInput) -> anyhow::Result<Vec<f32>> {
253        use rten_tensor::NdTensor;
254
255        let seq_len = input.input_ids.len();
256
257        let mut embedding = match &self.graph_inputs {
258            GraphInputs::Transformer {
259                input_ids,
260                attention_mask,
261                token_type_ids,
262            } => {
263                let ids_tensor = NdTensor::from_data([1, seq_len], input.input_ids.clone());
264                let mask_tensor = NdTensor::from_data([1, seq_len], input.attention_mask.clone());
265
266                let mut inputs = vec![
267                    (*input_ids, ids_tensor.into()),
268                    (*attention_mask, mask_tensor.into()),
269                ];
270
271                if let Some(type_id) = token_type_ids {
272                    let type_tensor =
273                        NdTensor::from_data([1, seq_len], input.token_type_ids.clone());
274                    inputs.push((*type_id, type_tensor.into()));
275                }
276
277                let hidden = self.run_to_vec(inputs)?;
278                pooling::mean_pool(&hidden, &input.attention_mask, seq_len, self.dimensions)
279            }
280            GraphInputs::EmbeddingBag { input_ids, offsets } => {
281                // Empty bag (e.g. empty string): skip inference, a zero
282                // vector is the only honest answer and normalize_l2 keeps it.
283                if seq_len == 0 {
284                    return Ok(vec![0.0; self.dimensions]);
285                }
286                // Flat ids + one offset per batch row; the graph pools
287                // internally, so the output is already [1, dim].
288                let ids_tensor = NdTensor::from_data([seq_len], input.input_ids.clone());
289                let offsets_tensor = NdTensor::from_data([1], vec![0i32]);
290                self.run_to_vec(vec![
291                    (*input_ids, ids_tensor.into()),
292                    (*offsets, offsets_tensor.into()),
293                ])?
294            }
295        };
296
297        pooling::normalize_l2(&mut embedding);
298        Ok(embedding)
299    }
300
301    /// Run the graph and flatten its first output into a `Vec<f32>`.
302    #[cfg(feature = "embeddings")]
303    fn run_to_vec(
304        &self,
305        inputs: Vec<(rten::NodeId, rten::ValueOrView)>,
306    ) -> anyhow::Result<Vec<f32>> {
307        use rten_tensor::AsView;
308
309        let outputs = self.model.run(inputs, &[self.output_id], None)?;
310        Ok(outputs
311            .into_iter()
312            .next()
313            .ok_or_else(|| anyhow::anyhow!("No output from model"))?
314            .into_tensor::<f32>()
315            .ok_or_else(|| anyhow::anyhow!("Model output is not float32"))?
316            .to_vec())
317    }
318
319    #[cfg(not(feature = "embeddings"))]
320    fn run_inference(&self, _input: &TokenizedInput) -> anyhow::Result<Vec<f32>> {
321        anyhow::bail!("Embeddings feature not enabled")
322    }
323}
324
325/// Load the appropriate tokenizer for the model config.
326fn load_tokenizer(model_dir: &Path, config: &ModelConfig) -> anyhow::Result<TokenizerKind> {
327    match &config.vocab_file {
328        VocabSource::VocabTxt(filename) => {
329            let path = model_dir.join(filename);
330            let tok = WordPieceTokenizer::from_file(&path)?;
331            Ok(TokenizerKind::WordPiece(tok))
332        }
333        VocabSource::TokenizerJson(filename) => {
334            let path = model_dir.join(filename);
335            let tok = tokenizer::HfTokenizerWrapper::from_file(&path).map_err(|e| {
336                anyhow::anyhow!(
337                    "Failed to load tokenizer.json for {}: {e}. Custom models must ship a \
338                     HuggingFace tokenizer.json with a supported model type (WordPiece/BPE).",
339                    config.name
340                )
341            })?;
342            Ok(TokenizerKind::HfTokenizer(tok))
343        }
344    }
345}
346
347/// Tokenize text using whatever tokenizer backend is loaded.
348fn tokenize(tokenizer: &TokenizerKind, text: &str, max_len: usize) -> TokenizedInput {
349    match tokenizer {
350        TokenizerKind::WordPiece(wp) => wp.encode(text, max_len),
351        TokenizerKind::HfTokenizer(hf) => hf.encode(text, max_len),
352    }
353}
354
355/// Detect embedding dimensions by running a dummy inference.
356#[cfg(feature = "embeddings")]
357fn detect_dimensions(
358    model: &Model,
359    tokenizer: &TokenizerKind,
360    graph_inputs: &GraphInputs,
361    output_id: rten::NodeId,
362    max_seq_len: usize,
363) -> Option<usize> {
364    use rten_tensor::{Layout, NdTensor};
365
366    let dummy = tokenize(tokenizer, "test", max_seq_len.min(8));
367    let seq_len = dummy.input_ids.len();
368
369    let inputs: Vec<(rten::NodeId, rten::ValueOrView)> = match graph_inputs {
370        GraphInputs::Transformer {
371            input_ids,
372            attention_mask,
373            token_type_ids,
374        } => {
375            let ids = NdTensor::from_data([1, seq_len], dummy.input_ids);
376            let mask = NdTensor::from_data([1, seq_len], dummy.attention_mask);
377            let mut inputs = vec![(*input_ids, ids.into()), (*attention_mask, mask.into())];
378            if let Some(type_id) = token_type_ids {
379                let types = NdTensor::from_data([1, seq_len], dummy.token_type_ids);
380                inputs.push((*type_id, types.into()));
381            }
382            inputs
383        }
384        GraphInputs::EmbeddingBag { input_ids, offsets } => {
385            if seq_len == 0 {
386                return None;
387            }
388            let ids = NdTensor::from_data([seq_len], dummy.input_ids);
389            let offs = NdTensor::from_data([1], vec![0i32]);
390            vec![(*input_ids, ids.into()), (*offsets, offs.into())]
391        }
392    };
393
394    let outputs = model.run(inputs, &[output_id], None).ok()?;
395    let tensor = outputs.into_iter().next()?.into_tensor::<f32>()?;
396    let shape = tensor.shape();
397
398    match graph_inputs {
399        // Shape is [batch=1, seq_len, dim].
400        GraphInputs::Transformer { .. } => shape.last().copied(),
401        // Already pooled: [batch=1, dim] — the last axis IS the dim, but be
402        // explicit about the rank so a surprising graph fails loudly into
403        // the config fallback instead of mis-probing.
404        GraphInputs::EmbeddingBag { .. } => {
405            if shape.len() == 2 {
406                shape.last().copied()
407            } else {
408                None
409            }
410        }
411    }
412}
413
414/// Compute cosine similarity between two L2-normalized vectors.
415/// Both vectors must be pre-normalized for correct results.
416///
417/// Uses the chunked, autovectorizable dot product from [`crate::core::embedding_quant`]
418/// (turbovec-derived) so every semantic-search hot path gets SIMD throughput.
419pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
420    debug_assert_eq!(a.len(), b.len(), "vectors must have equal dimensions");
421    crate::core::embedding_quant::dot_f32(a, b)
422}
423
424/// Compute cosine similarity without requiring pre-normalization.
425pub fn cosine_similarity_raw(a: &[f32], b: &[f32]) -> f32 {
426    debug_assert_eq!(a.len(), b.len());
427    use crate::core::embedding_quant::dot_f32;
428    let dot = dot_f32(a, b);
429    let norm_a = dot_f32(a, a).sqrt();
430    let norm_b = dot_f32(b, b).sqrt();
431    if norm_a == 0.0 || norm_b == 0.0 {
432        return 0.0;
433    }
434    dot / (norm_a * norm_b)
435}
436
437#[cfg(feature = "embeddings")]
438static SHARED_ENGINE: std::sync::OnceLock<anyhow::Result<EmbeddingEngine>> =
439    std::sync::OnceLock::new();
440
441/// Global singleton embedding engine. Loaded once, shared across all consumers.
442/// Returns None if the embeddings feature is disabled or the model fails to load.
443/// NOTE: This function BLOCKS on first call while loading the ONNX model.
444/// For non-blocking access, use `try_shared_engine()` instead.
445#[cfg(feature = "embeddings")]
446pub fn shared_engine() -> Option<&'static EmbeddingEngine> {
447    SHARED_ENGINE
448        .get_or_init(EmbeddingEngine::load_default)
449        .as_ref()
450        .ok()
451}
452
453/// Non-blocking variant: returns the engine ONLY if already loaded.
454/// Never triggers model loading or download. Safe to call on hot paths.
455#[cfg(feature = "embeddings")]
456pub fn try_shared_engine() -> Option<&'static EmbeddingEngine> {
457    SHARED_ENGINE.get()?.as_ref().ok()
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    #[test]
465    fn cosine_similarity_identical() {
466        let a = vec![1.0, 0.0, 0.0];
467        let b = vec![1.0, 0.0, 0.0];
468        assert!((cosine_similarity(&a, &b) - 1.0).abs() < 1e-6);
469    }
470
471    #[test]
472    fn cosine_similarity_orthogonal() {
473        let a = vec![1.0, 0.0, 0.0];
474        let b = vec![0.0, 1.0, 0.0];
475        assert!(cosine_similarity(&a, &b).abs() < 1e-6);
476    }
477
478    #[test]
479    fn cosine_similarity_opposite() {
480        let a = vec![1.0, 0.0, 0.0];
481        let b = vec![-1.0, 0.0, 0.0];
482        assert!((cosine_similarity(&a, &b) + 1.0).abs() < 1e-6);
483    }
484
485    #[test]
486    fn cosine_similarity_raw_unnormalized() {
487        let a = vec![3.0, 4.0];
488        let b = vec![3.0, 4.0];
489        assert!((cosine_similarity_raw(&a, &b) - 1.0).abs() < 1e-6);
490    }
491
492    #[test]
493    fn cosine_similarity_raw_zero_vector() {
494        let a = vec![0.0, 0.0];
495        let b = vec![1.0, 2.0];
496        assert_eq!(cosine_similarity_raw(&a, &b), 0.0);
497    }
498
499    #[test]
500    fn model_directory_env_override_and_availability() {
501        let unique = "/tmp/lean_ctx_test_embed_42xyz";
502        std::env::set_var("LEAN_CTX_MODELS_DIR", unique);
503        let dir = EmbeddingEngine::model_directory();
504        assert_eq!(dir.to_string_lossy(), unique);
505        assert!(!EmbeddingEngine::is_available());
506        std::env::remove_var("LEAN_CTX_MODELS_DIR");
507    }
508
509    /// GL #452: the EmbeddingBag detection is purely name-based — exactly two
510    /// inputs with the second named `offsets`. Everything else (classic 2-/
511    /// 3-input transformers, unnamed graphs) must stay on the transformer path.
512    #[test]
513    #[cfg(feature = "embeddings")]
514    fn embedding_bag_signature_detection() {
515        // model2vec / potion export.
516        assert!(is_embedding_bag_signature(&[
517            Some("input_ids"),
518            Some("offsets")
519        ]));
520        // Transformers: mask second, optional token types third.
521        assert!(!is_embedding_bag_signature(&[
522            Some("input_ids"),
523            Some("attention_mask")
524        ]));
525        assert!(!is_embedding_bag_signature(&[
526            Some("input_ids"),
527            Some("attention_mask"),
528            Some("token_type_ids")
529        ]));
530        // Unnamed inputs or wrong arity never flip the topology.
531        assert!(!is_embedding_bag_signature(&[Some("input_ids"), None]));
532        assert!(!is_embedding_bag_signature(&[Some("offsets")]));
533        assert!(!is_embedding_bag_signature(&[
534            Some("input_ids"),
535            Some("offsets"),
536            Some("extra")
537        ]));
538    }
539
540    // NOTE: `try_shared_engine_returns_none_when_not_initialized` lives in
541    // `tests/embeddings_shared_engine.rs` (own process). SHARED_ENGINE is a
542    // process-wide OnceLock: in the unit-test suite any sibling test that
543    // legitimately loads the engine (or #551 background activation) would
544    // initialize it first and make the assertion order-dependent/flaky.
545
546    /// Live proof for GL #397: loads a real HuggingFace repo through the
547    /// `hf:org/repo@rev` scheme (download → SHA-256 lockfile → tokenizer.json →
548    /// ONNX inference → dimension probe). Ignored by default (network + ~91MB);
549    /// run explicitly:
550    /// `cargo test --lib --features embeddings -- --ignored custom_hf_model_end_to_end`
551    #[test]
552    #[ignore = "downloads a real model from HuggingFace (~91MB)"]
553    #[cfg(feature = "embeddings")]
554    fn custom_hf_model_end_to_end() {
555        let model = model_registry::EmbeddingModel::from_str_name(
556            "hf:sentence-transformers/all-MiniLM-L6-v2@main",
557        )
558        .expect("valid hf: spec");
559
560        let base = std::env::temp_dir().join("lean_ctx_test_custom_hf_e2e");
561        let engine = EmbeddingEngine::load_model(&base, model.clone()).expect("load custom model");
562
563        assert_eq!(engine.dimensions(), 384, "probed dims from ONNX graph");
564        assert_eq!(
565            engine.model_name(),
566            "hf:sentence-transformers/all-MiniLM-L6-v2@main"
567        );
568
569        let v = engine.embed("fn main() { println!(\"hello\"); }").unwrap();
570        assert_eq!(v.len(), 384);
571        let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
572        assert!((norm - 1.0).abs() < 1e-3, "L2-normalized, got {norm}");
573
574        // Lockfile must exist and pin both artifacts.
575        let lock_path = base.join(model.storage_dir_name()).join("model.lock.json");
576        let lock: std::collections::BTreeMap<String, String> =
577            serde_json::from_str(&std::fs::read_to_string(&lock_path).unwrap()).unwrap();
578        assert!(lock.contains_key("model.onnx"));
579        assert!(lock.contains_key("tokenizer.json"));
580
581        // Semantic sanity: similar code closer than unrelated text.
582        let a = engine.embed("read a file from disk").unwrap();
583        let b = engine.embed("load file contents from filesystem").unwrap();
584        let c = engine.embed("the weather in Zurich is sunny").unwrap();
585        assert!(
586            cosine_similarity(&a, &b) > cosine_similarity(&a, &c),
587            "related texts must be closer"
588        );
589    }
590
591    /// Live proof for GL #452: a model2vec EmbeddingBag graph end-to-end
592    /// through the same `hf:` scheme (potion-base-8M, ~30MB). Ignored by
593    /// default (network); run explicitly:
594    /// `cargo test --lib --features embeddings -- --ignored model2vec_potion_end_to_end`
595    #[test]
596    #[ignore = "downloads a real model from HuggingFace (~30MB)"]
597    #[cfg(feature = "embeddings")]
598    fn model2vec_potion_end_to_end() {
599        let model =
600            model_registry::EmbeddingModel::from_str_name("hf:minishlab/potion-base-8M@main")
601                .expect("valid hf: spec");
602
603        let base = std::env::temp_dir().join("lean_ctx_test_model2vec_e2e");
604        let engine =
605            EmbeddingEngine::load_model(&base, model.clone()).expect("load model2vec model");
606
607        // potion-base-8M is 256d; the probe must read it off the rank-2
608        // output, not assume a [1, seq, dim] transformer shape.
609        assert_eq!(engine.dimensions(), 256, "probed dims from EmbeddingBag");
610
611        let code_vec = engine.embed("fn main() { println!(\"hello\"); }").unwrap();
612        assert_eq!(code_vec.len(), 256);
613        let norm: f32 = code_vec.iter().map(|x| x * x).sum::<f32>().sqrt();
614        assert!((norm - 1.0).abs() < 1e-3, "L2-normalized, got {norm}");
615
616        // Distinct inputs must not collapse to one vector.
617        let sql_vec = engine.embed("SELECT * FROM users WHERE id = 1").unwrap();
618        assert!(cosine_similarity(&code_vec, &sql_vec) < 0.999);
619
620        // Semantic sanity survives the static-embedding quality trade-off.
621        let read_vec = engine.embed("read a file from disk").unwrap();
622        let load_vec = engine.embed("load file contents from filesystem").unwrap();
623        let weather_vec = engine.embed("the weather in Zurich is sunny").unwrap();
624        assert!(
625            cosine_similarity(&read_vec, &load_vec) > cosine_similarity(&read_vec, &weather_vec),
626            "related texts must be closer"
627        );
628
629        // Empty input: zero vector, no inference panic.
630        let empty_vec = engine.embed("").unwrap();
631        assert_eq!(empty_vec.len(), 256);
632    }
633}