From 10835ca788bb46f6d6e5097a4692d500c414fecd Mon Sep 17 00:00:00 2001 From: aaltshuler Date: Sun, 5 Jul 2026 05:06:44 +0300 Subject: [PATCH] fix(engine): reject non-finite and zero-norm embeddings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validate every component is_finite() (naming the offending index) and reject zero-norm vectors before normalizing, via the existing Err(String) channel — callers already map it through EmbedCallError {retryable:false} into OmniError::manifest_internal, so no new error plumbing. The red tests turn green; docs state the finite/non-zero contract in embeddings.md. Closes iss-embedding-nan-validation. --- crates/omnigraph/src/embedding.rs | 19 +++++++++++++++++++ docs/user/search/embeddings.md | 5 ++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/omnigraph/src/embedding.rs b/crates/omnigraph/src/embedding.rs index 53f6584d..9c8aba3f 100644 --- a/crates/omnigraph/src/embedding.rs +++ b/crates/omnigraph/src/embedding.rs @@ -542,6 +542,25 @@ fn validate_and_normalize_embedding( values.len() )); } + // Reject poisoned vectors BEFORE normalizing: a NaN component makes the + // norm NaN (whose `> EPSILON` comparison is false, silently skipping + // normalization), an Inf component normalizes the rest to 0s and itself + // to NaN, and a zero vector has no direction — all three would persist + // as "successful" embeddings and degrade ANN/IVF for unrelated rows. + if let Some(idx) = values.iter().position(|v| !v.is_finite()) { + return Err(format!( + "embedding component {} is not finite ({})", + idx, values[idx] + )); + } + let norm = values + .iter() + .map(|v| (*v as f64) * (*v as f64)) + .sum::() + .sqrt() as f32; + if norm <= f32::EPSILON { + return Err("embedding has zero norm (no direction)".to_string()); + } Ok(normalize_vector(values)) } diff --git a/docs/user/search/embeddings.md b/docs/user/search/embeddings.md index 11f35406..2f5a55e5 100644 --- a/docs/user/search/embeddings.md +++ b/docs/user/search/embeddings.md @@ -13,7 +13,10 @@ query vectors and document vectors share one model and one vector space. | `gemini` | `POST {base}/models/{model}:embedContent`, `x-goog-api-key`, with `RETRIEVAL_QUERY` / `RETRIEVAL_DOCUMENT` task types | Reaching Google's `generativelanguage` API directly | | `mock` | none — deterministic offline vectors | Tests and local dev without a key | -Vectors are stored L2-normalized as `FixedSizeList(Float32, dim)`; the requested output dimension is driven by +Vectors are stored L2-normalized as `FixedSizeList(Float32, dim)`. Embedding +values must be finite — a vector containing NaN/Inf, or an all-zero vector +(no direction under cosine distance), is rejected at the client boundary +rather than persisted; the requested output dimension is driven by the target column width and sent as Gemini `outputDimensionality` / OpenAI `dimensions`. ## Configuration (cluster)