feature-extraction Β· text Β· WASM Β· ~34 MB
E5 (from Microsoft) turns a sentence into 384 numbers β but with a twist no other small embedder
shares: you must prefix every input. A search gets query:, a document
gets passage:. E5 was contrastively trained on queryβpassage pairs, so the two sides
play different roles and the prefix tells the model which one this is. Skip it and retrieval gets
worse. Type some lines to build a live similarity matrix, run asymmetric search, watch the prefix
move the geometry, and see four embedders disagree on the same pairs. Nothing leaves your device.
| Model | Xenova/e5-small-v2 (ONNX of intfloat/e5-small-v2) |
|---|---|
| Task / pipeline | feature-extraction |
| Architecture | BERT-style encoder (~33M params) β 384-dimensional sentence embedding |
| Pooling | mask-aware mean pooling, then L2-normalize |
| Prefixes | required: query: for searches, passage: for documents (contrast GTE = none, BGE = one query instruction) |
| Quantization | q8 (8-bit) ONNX |
| Backend | WebAssembly (runs anywhere; no WebGPU needed) |
| Download | ~34 MB q8, cached after first load |
| License | MIT (intfloat E5) |
| Web APIs | WebAssembly, Web Workers β all Baseline widely available |
Put one sentence per line, then embed. For a symmetric similarity matrix E5 recommends
the query: prefix on every line. The matrix shows cosine similarity between every pair
(1.00 = identical direction); the scatter is a 2D PCA projection where near = similar. Then run a
search below, where the query and documents get different prefixes.
The query is embedded with query:; the lines above are embedded with passage: β exactly as E5 is meant to be used for retrieval.
The encoder emits one 384-number vector per token. To get a single vector per sentence E5
mean-pools those and L2-normalizes, so every sentence becomes a
unit-length arrow and cosine similarity is a plain dot product. Pick a sentence to see its real
vector and pre-normalization magnitude (embedded here with query:).
First 96 of 384 dimensions (blue = positive, orange = negative, brighter = larger magnitude):
Same query, same document β embedded three ways. Correct uses query:
on the question and passage: on the document (how E5 is meant to run). No
prefix embeds both raw. Swapped deliberately mislabels them. Watch how
the cosine to the right document changes when you get the prefix wrong.
Four embedders, four training recipes: E5 (mean pool, query/passage prefix),
MiniLM (mean pool, no prefix), BGE (CLS pool, query instruction)
and GTE (mean pool, no prefix). They do not agree on how similar two sentences are
β that disagreement is exactly why the model you pick changes your search results. Loads MiniLM
(~25 MB), BGE (~34 MB) and GTE (~34 MB) on demand, embeds each pair with all four (E5 with its
query: prefix), and shows the real cosines.
Embeddings are the quiet workhorse behind search, recommendations, deduplication, clustering, and retrieval-augmented generation. E5's pitch is retrieval quality from the prefix trick: telling the model whether a string is a question or an answer lets one small 33M-param encoder handle both sides of a search well β and it runs in a tab, private, because the text never leaves the device.
Do two lines mean the same thing? One cosine β and a toggle showing the prefix change the answer.
Asymmetric search and classify-by-nearest-label over your own text, prefixes applied correctly.
Cluster a mixed pile of text into themes with k-means over E5 vectors, then see them on a 2D map.
E5 retrieves a shortlist by meaning, then a cross-encoder reranker re-scores the top hits.
Feature-extraction plus the prefix discipline is the whole recipe β the prefix is the part people forget:
import { pipeline } from "@huggingface/transformers";
const extract = await pipeline(
"feature-extraction",
"Xenova/e5-small-v2",
); // downloads + caches ~34 MB once
// E5 REQUIRES prefixes: "query: " on searches, "passage: " on documents.
const out = await extract(
["query: how do I sign in?", "passage: the login page rejects my password"],
{ pooling: "mean", normalize: true },
);
const [q, d] = out.tolist();
// unit-length vectors β cosine similarity is just the dot product:
const sim = q.reduce((s, x, i) => s + x * d[i], 0); // high β same meaning, correct prefixes
This page asks the worker for the un-normalized pooled vectors so "See inside" can show the real pre-normalization magnitude, then normalizes them in JS. Inference runs in a Web Worker, so the matrix, projection and comparison never jank the page.