Skip to content

ruvllm: embed()/similarity produce non-discriminative embeddings (random-init char-level embedder, no pretrained weights) #655

Description

@pacphi

Summary

@ruvector/ruvllm@2.6.0's embed()/similarity produce non-discriminative embeddings: unrelated text pairs frequently score a higher cosine similarity than genuine paraphrase pairs. This makes the package unusable for its stated purpose (semantic similarity / vector memory).

Environment

  • @ruvector/ruvllm@2.6.0 (current npm latest, confirmed no newer version published)
  • Native binary confirmed loaded (@ruvector/ruvllm-darwin-arm64, not the JS/WASM fallback)
  • macOS, arm64 (Apple Silicon), Node v26.4.0

How to reproduce

npm install @ruvector/ruvllm
npx ruvllm similarity "how to connect gmail to claude for email automation" \
  "recipe for chocolate lava cake with raspberry coulis"
npx ruvllm similarity "how to connect gmail to claude for email automation" \
  "steps for linking your gmail account with claude to send emails"
npx ruvllm similarity "connect gmail to claude" "connect gmail to claude"

Additional runs with bin/cli.js similarity against the native darwin-arm64 module (not the JS fallback) confirm this is not environment-specific:

Pair type Similarity
Paraphrase 97.85%
Paraphrase 97.89%
Paraphrase 99.53%
Identical 100.00%
Unrelated 91.24%
Unrelated 97.78%
Unrelated 98.75%
Unrelated 99.02%

Two of four unrelated pairs score higher than two of four paraphrase pairs. All eight values fall in a 91–100% band with no separation by actual semantic content.

Expected behavior

A trained sentence embedder should clearly separate unrelated text (low similarity, roughly 0–0.4 cosine on a typical MiniLM-class model) from paraphrases (high similarity, roughly 0.7–0.95), with unrelated pairs always scoring below genuine paraphrases.

Root cause (confirmed by reading source, examples/ruvLLM/src/embedding.rs, current main as of 2026-07-10)

There is no pretrained-weight-loading code path in this file at all — this is architecturally a random-projection character-bag embedder, not a case of pretrained weights failing to load:

  • Tokenizer::new() (~line 54) builds a character-level vocabulary (individual letters/digits/punctuation), not word- or subword-level tokens.
  • EmbeddingService::new() (~lines 159–166) initializes embedding_matrix via rand::thread_rng().gen_range(-0.1..0.1) per token — freshly randomized on every process start, never loaded from any checkpoint. grep-ing the file for pretrained|load_weight returns no matches.
  • mean_pooling() (~lines 327–352) averages random per-character token vectors plus sinusoidal position embeddings across the whole sequence, then L2-normalizes. Averaging many random unit-ish vectors drives all output embeddings toward a similar central direction (law of large numbers) — this is the mechanism that collapses cosine similarity toward 1.0 regardless of semantic content, for any sufficiently long input.

Sample use cases affected

  • Semantic search / RAG retrieval ranking by cosine similarity
  • Vector-memory deduplication or nearest-neighbor clustering
  • Any downstream code that thresholds on similarity score to decide relatedness

Workaround

Use a real pretrained sentence embedder instead (e.g. @xenova/transformers/@huggingface/transformers with a MiniLM-class model) — confirmed to correctly order paraphrase pairs above unrelated pairs on the same test set.

Proposed fix

  • Load actual pretrained weights (or drop the "embedding" framing until a trained model backs it), and/or switch tokenization to word/subword level.
  • Add a regression test in CI asserting sim(text, paraphrase(text)) > sim(text, unrelated) across a small fixed corpus, run on every release.
  • Expose a debug/verbose flag reporting whether weights are pretrained vs. randomly initialized, so this failure mode is visible rather than silent.

Related issues

ruvnet/ruvector#316 ("Bug: embed() always returns hash vectors — breaks cosine similarity when ONNX is active") is related but distinct — it's scoped to the core ruvector package's ONNX embedder silently falling back to a hash vector. @ruvector/ruvllm's embedder has no ONNX/pretrained path to fall back from; it's random by construction. Also loosely related for context: #523, #402. None of these cover this specific defect.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions