Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 80 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions crates/ruvector-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ tokenizers = { version = "0.20", default-features = false, features = ["onig"],
# HuggingFace Hub for model downloads
hf-hub = { version = "0.4", optional = true }

# Native (pure-Rust) local embeddings via lattice-embed (not available in WASM).
# NOTE: lattice-embed 0.5.1 requires Rust >= 1.93 (edition 2024). Cargo cannot
# express a per-feature `rust-version`, so enabling the `lattice-embeddings`
# feature raises the effective MSRV above this crate's workspace-inherited
# 1.77 for anyone who turns it on. The default build is unaffected.
lattice-embed = { version = "0.5.1", optional = true }
tokio = { workspace = true, optional = true }

[dev-dependencies]
criterion = { workspace = true }
proptest = { workspace = true }
Expand Down Expand Up @@ -94,6 +102,12 @@ harness = false
name = "bench_memory"
harness = false

# Feature-gated: only built when `lattice-embeddings` is enabled, since it uses
# the LatticeEmbedding provider (which is behind that feature).
[[example]]
name = "lattice_embedding_example"
required-features = ["lattice-embeddings"]

[features]
default = ["simd", "simd-avx512", "storage", "hnsw", "api-embeddings", "parallel"]
simd = ["simsimd"] # SIMD acceleration (not available in WASM)
Expand All @@ -110,6 +124,7 @@ uuid-support = [] # Deprecated: uuid is now always included
real-embeddings = [] # Feature flag for embedding provider API (use ApiEmbedding for production)
api-embeddings = ["reqwest"] # API-based embeddings (not available in WASM)
onnx-embeddings = ["ort", "tokenizers", "hf-hub"] # ONNX-based local embeddings (not available in WASM)
lattice-embeddings = ["dep:lattice-embed", "dep:tokio"] # Native pure-Rust local embeddings via lattice-embed (not available in WASM)

[lib]
crate-type = ["rlib"]
Expand Down
76 changes: 76 additions & 0 deletions crates/ruvector-core/examples/lattice_embedding_example.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! Native pure-Rust local embeddings via the `LatticeEmbedding` provider.
//!
//! Demonstrates asymmetric retrieval on a BGE model: a query is embedded
//! differently from a passage, which is what makes retrieval scores correct.
//! The provider exposes two sides:
//! - `embed(text)`: the passage/document side (no query instruction)
//! - `embed_query(text)`: the query side (applies the model's query instruction)
//!
//! Run with (downloads `bge-small-en-v1.5` from HuggingFace into
//! `~/.lattice/models` on first use):
//! ```bash
//! cargo run --example lattice_embedding_example --features lattice-embeddings
//! ```

use ruvector_core::embeddings::{EmbeddingProvider, LatticeEmbedding};

/// Cosine similarity of two equal-length vectors.
fn cosine(a: &[f32], b: &[f32]) -> f32 {
let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
let na = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let nb = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if na == 0.0 || nb == 0.0 {
0.0
} else {
dot / (na * nb)
}
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== LatticeEmbedding (native pure-Rust) Example ===\n");

// BGE is an asymmetric retriever: query and passage are embedded differently.
let provider = LatticeEmbedding::from_pretrained("bge-small-en-v1.5")?;
println!(
"✓ Loaded provider: {} ({} dimensions)\n",
provider.name(),
provider.dimensions()
);

let passage = "The Eiffel Tower is located in Paris, France.";
let query = "Where is the Eiffel Tower?";
println!("Passage: {passage:?}");
println!("Query: {query:?}\n");

// Passage side: no query instruction.
let passage_vec = provider.embed(passage)?;

// The same query text, embedded two ways:
// (a) as a passage: WRONG for a query on an asymmetric model,
// (b) as a query: CORRECT, applies BGE's retrieval instruction.
let query_as_passage = provider.embed(query)?;
let query_as_query = provider.embed_query(query)?;

println!("--- Asymmetry: query embedded as passage vs. as query ---");
println!(
"cosine(passage, query-as-passage) = {:.4}",
cosine(&passage_vec, &query_as_passage)
);
println!(
"cosine(passage, query-as-query) = {:.4} <- embed_query()",
cosine(&passage_vec, &query_as_query)
);

let self_sim = cosine(&query_as_passage, &query_as_query);
println!("\ncosine(query-as-passage, query-as-query) = {self_sim:.4}");
println!(
"A value below 1.0 confirms embed_query() prepended BGE's retrieval\n\
instruction and produced a different vector. That is the whole point:\n\
queries and passages live in the same space but are encoded by\n\
different protocols. (A single pair's absolute cosine is not the\n\
retrieval signal; what matters is ranking across a corpus, where\n\
embedding queries with embed_query() is what BGE was trained for.)"
);

Ok(())
}
Loading
Loading