Skip to content

Optional LatticeEmbedding provider for ruvector-core (feature: lattice-embeddings)#648

Open
ohdearquant wants to merge 5 commits into
ruvnet:mainfrom
ohdearquant:feat/lattice-embedding-provider
Open

Optional LatticeEmbedding provider for ruvector-core (feature: lattice-embeddings)#648
ohdearquant wants to merge 5 commits into
ruvnet:mainfrom
ohdearquant:feat/lattice-embedding-provider

Conversation

@ohdearquant

Copy link
Copy Markdown
Contributor

Optional LatticeEmbedding provider for ruvector-core

Adds an optional EmbeddingProvider implementation, LatticeEmbedding, that wraps the published lattice-embed crate (a pure-Rust BERT-family text-embedding engine). It sits alongside the existing HashEmbedding / CandleEmbedding / OnnxEmbedding / ApiEmbedding providers, entirely behind a new default-off feature lattice-embeddings. The default build and the default embedder are unchanged.

This is an option, not a default-flip — a correct, measured embedding backend you can opt into. Whether it ever becomes a default .rvf embedder is entirely your call on the evidence below.

Why it might be useful

  • Correct-by-construction asymmetric retrieval. BGE/E5 are asymmetric retrievers: the query needs an instruction prefix, the document does not. LatticeEmbedding exposes an explicit embed() (passage, no prefix) and an inherent embed_query() (applies the BGE query instruction). The EmbeddingProvider trait has a single symmetric embed() with no query/passage distinction (the default MiniLM embedder per ADR-210 is symmetric, so this is invisible there), but a BGE/E5 model plugged into that trait embeds queries without the required prefix. Since .rvf packs ship bge-small-en-v1.5 vectors, this asymmetry is exactly the BGE case. This is the strongest, most honest differentiator.

  • Exact model, zero conversion. .rvf packs ship 384-dim bge-small-en-v1.5 vectors; lattice-embed runs that exact model, so there is no embed-to-a-different-space mismatch.

  • f32 fidelity (real but modest). Against FP32 sentence-transformers/bge-small-en-v1.5 gold over a 100-sentence corpus:

    candidate mean cosine top-1 agreement top-5 overlap
    lattice-native (f32) 0.9999 10/10 1.000
    transformers.js int8 (default) 0.9966 8/10 0.900

    Real, but small. int8 is a good embedder here, not a broken one.

  • Native Rust, no Node/WASM/ONNX runtime — an architectural fit for a Rust-first project. Compute is CPU/Accelerate; no GPU required.

What this is not: not a claim that transformers.js is broken, and not a large retrieval-quality jump. Raw single-text speed is roughly a wash — correctness (the asymmetry split) is the lever, not throughput.

Design

  • Feature-gated, default-off. lattice-embeddings = ["dep:lattice-embed", "dep:tokio"], not in default. lattice-embed pulls only its native (CPU) feature, never metal-gpu. Nothing from the feature touches the default build path.
  • async→sync bridge, safe from async callers. lattice-embed's EmbeddingService is async-only; the EmbeddingProvider trait is sync. Rather than call Runtime::block_on on the caller's thread (which panics if the caller is already inside a Tokio runtime), LatticeEmbedding runs the runtime and the embedding service on a dedicated worker thread; embed/embed_query send a request over a channel and block on the reply. The worker has no ambient async context, so a call from inside an existing runtime is safe. Requests are FIFO-serialized through the single worker (fine for v0; a pool is a later option). A #[tokio::test] embeds from inside a running runtime as the regression guard.
  • Model mapping. from_pretrained(model_id) delegates id parsing to lattice-embed's own FromStr (case-insensitive; accepts display names, short names, HuggingFace ids), then rejects at construction any id that is unrecognized or resolves to a model the native engine can't run locally (e.g. the remote-only OpenAI variant) — no silent default, no deferred failure on first embed().
  • MSRV. The default build keeps the workspace MSRV. Enabling lattice-embeddings raises the effective MSRV to Rust 1.93 (edition 2024), since lattice-embed requires it; Cargo cannot express a per-feature rust-version, so this is documented in the module docs and above the dependency line rather than in rust-version.
  • Dependency. lattice-embed = { version = "0.5.1", optional = true } from crates.io.

Testing

cargo check / cargo test -p ruvector-core --features lattice-embeddings (library + doctests) and cargo clippy --all-targets pass against the published lattice-embed 0.5.1. Provider-specific tests cover the async-context regression, construction-time rejection of remote-only model ids, and acceptance of a valid native id. The default build (feature off) is untouched.

ohdearquant and others added 4 commits July 6, 2026 13:18
… (feature: lattice-embeddings)

Adds a pure-Rust native EmbeddingProvider backed by lattice-embed 0.5
(CPU-only, default 'native' feature, no metal-gpu). Entirely behind
the new lattice-embeddings feature (not in default).

- LatticeEmbedding::from_pretrained delegates model-id parsing to
  lattice_embed::EmbeddingModel's own FromStr impl instead of
  reimplementing the mapping (bge/e5/minilm/qwen3, display names +
  HF ids), so it stays in sync with lattice-embed's canonical table.
- embed() is passage/document-side (no query instruction) via
  EmbeddingService::embed_passage; the inherent embed_query() method
  is query-side (applies E5/Qwen3 query instructions) via
  EmbeddingService::embed_passage/embed_query, making asymmetric
  retrieval correct.
- Bridges lattice-embed's async EmbeddingService trait onto the sync
  EmbeddingProvider trait via a dedicated single-threaded tokio
  Runtime + block_on (documented re-entrancy constraint: do not call
  from within another running Tokio runtime).
- Tests: pure model-id mapping tests (no network) + a #[ignore]'d
  real end-to-end embed test (needs ~130MB model download).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bumps the optional lattice-embed dependency from 0.5 to the published 0.5.1,
which includes the BGE query-instruction prefix for asymmetric retrieval.
Verified: cargo check + test -p ruvector-core --features lattice-embeddings
pass against lattice-embed 0.5.1 from crates.io.
…models, correct BGE docs

Run lattice-embed's runtime and embedding service on a dedicated worker thread
instead of calling block_on on the caller's thread. embed/embed_query now send a
request over a channel and block on the reply, so they are safe to call from
inside an existing Tokio runtime; the previous stored-runtime approach panicked
on nested block_on.

Reject non-local models (e.g. the remote-only OpenAI variant) at construction in
with_model rather than deferring the failure to the first embed() call.

Correct the module- and method-level rustdoc: BGE v1.5 applies a query-side
instruction prefix for asymmetric retrieval (only MiniLM is symmetric), and
disclose that enabling the lattice-embeddings feature raises the effective MSRV
to Rust 1.93 while the default build is unchanged.

Verified: cargo check + test (lib + doc) + clippy --all-targets pass under the
lattice-embeddings feature against lattice-embed 0.5.1 from crates.io. New tests
cover the async-context regression and construction-time rejection of
remote-only model ids.
…osure

Rustfmt reflows the outcome.map_err(...).and_then(...) chain in the worker
thread's reply mapping. No behavior change.
…edding

Adds documentation and a dedicated, runnable example demonstrating the
asymmetric-retrieval differentiator of the native LatticeEmbedding provider.

- examples/lattice_embedding_example.rs: constructs the provider via
  from_pretrained("bge-small-en-v1.5"), then embeds a passage with embed()
  and a query with embed_query(), printing the cosine similarities to show
  concretely that the query instruction changes the vector. Matches the
  crate's existing example convention (per-crate examples/, //! header with a
  "Run with:" block, ===/--- section prints). Gated by a [[example]] block
  with required-features = ["lattice-embeddings"] so the default build skips it.
  Runnable: cargo run --example lattice_embedding_example --features lattice-embeddings

- embeddings.rs: adds a "# Examples" section to the LatticeEmbedding struct
  rustdoc with a no_run doctest of the same passage/query split.

Verified (feature on): cargo fmt --check clean, clippy clean on the new code,
example compiles and runs (bge-small-en-v1.5, 384-dim), doctests compile.
Docs/example only; no provider behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant