Skip to content

Day 7 — Embeddings and Vector Search

Today text becomes geometry. AskDocs uses an offline hashing embedder so you can inspect every assumption without a model download or network call.

#[async_trait::async_trait]
pub trait Embedder: Send + Sync {
async fn embed(&self, texts: &[String]) -> Result<Vec<Vec<f32>>>;
fn dim(&self) -> usize;
}

HashingEmbedder lowercases tokens, hashes each token into one of 256 buckets, counts, and L2-normalizes. It captures lexical overlap, not meaning. That limitation is valuable because success and failure are easy to explain.

pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
if a.len() != b.len() { return 0.0; }
let mut dot = 0.0;
let mut norm_a = 0.0;
let mut norm_b = 0.0;
for i in 0..a.len() {
dot += a[i] * b[i];
norm_a += a[i] * a[i];
norm_b += b[i] * b[i];
}
if norm_a == 0.0 || norm_b == 0.0 { return 0.0; }
dot / (norm_a.sqrt() * norm_b.sqrt())
}

Test identical, orthogonal, opposite, zero, mismatched, and non-finite vectors. f32 does not implement total ordering because NaN is incomparable; ranking code must state what happens.

pub struct Scored<'a> {
pub score: f32,
pub chunk: &'a Chunk,
}

top_k(&self, ...) -> Vec<Scored<'_>> avoids cloning every winning chunk. The lifetime says results cannot outlive the store. This is a Rust concept arriving exactly when retrieval would otherwise copy text on every query.

Terminal window
cd rust
cargo test -p askr vector embed

Create two documents with overlapping vocabulary but different meaning. Observe the wrong lexical winner. Then replace the hashing implementation behind Embedder—not the store or RAG pipeline—with a real embedding provider or local model.

Explain why cosine similarity alone cannot establish relevance, factual support, diversity, recency, or authorization. Those become separate ranking and policy layers.

Deep references: Text generation, embeddings, rerankers, and classifiers and Multimodal retrieval and reranking.

Next: Day 8 — Cited RAG →.