DS DevShelfHub Projects · AI tools
Cheatsheets / Embeddings
Cheatsheet · AI frameworks

Embeddings Cheatsheet: Models, Metrics, Chunking and Cost

By DevShelfHub

Models, dimensions, distance metrics, normalization, batching, cost — the practical surface for choosing and using text embeddings.

96 items 7 min Vectors Metrics Models

Start hereQuick start · 6 you’ll reach for daily

OpenAI singleclient.embeddings.create(model, input)
Batch encodemodel.encode(texts, batch_size=64)
Normalizev / np.linalg.norm(v)
Cosinenp.dot(a, b) # both normalized
Top-knp.argpartition(-scores, k)[:k]
Shrink dimdimensions=512 # matryoshka

Target versions · paceVersions

Targets: openai ≥ 1.40 sentence-transformers ≥ 3.0 cohere ≥ 5.0 voyageai ≥ 0.2 python ≥ 3.10

Embedding model names change fast. OpenAI’s text-embedding-3-* family supersedes text-embedding-ada-002. Cohere’s embed-v4.0 is multimodal & multilingual. Voyage’s voyage-3-large tops MTEB English for retrieval. Check MTEB leaderboard before committing to a model. This sheet pins to the names current as of May 2026.

Install · envSetup

bash
# Hosted providers
pip install openai cohere voyageai

# Local / OSS
pip install sentence-transformers      # CPU/GPU, huggingface
pip install fastembed                  # ONNX runtime, no torch
pip install "ollama"                   # client for local Ollama server

# Plumbing
pip install numpy tiktoken

# Env
export OPENAI_API_KEY=sk-...
export COHERE_API_KEY=...
export VOYAGE_API_KEY=...

Where things liveCommon imports

from openai import OpenAIHosted embeddings via OpenAI SDK.
from cohere import ClientV2Cohere v2 client; embed-v4.0 is multilingual + multimodal.
import voyageaiVoyage AI — strong English retrieval.
from sentence_transformers import SentenceTransformerLocal HuggingFace models.
from fastembed import TextEmbeddingONNX runtime, no torch dependency. Tiny install.
import ollamaLocal Ollama embed endpoint.
import numpy as npVector ops, norms, similarity.
import tiktokenCount tokens before billing.

What to pickModels · pick by task

Hosted

text-embedding-3-smallOpenAI. 1536 dim. Default Cheap, fast, good enough for most RAG.
text-embedding-3-largeOpenAI. 3072 dim. Higher recall; supports matryoshka shrink.
text-embedding-ada-002Legacy Older OpenAI; use the 3-series unless pinned by spend.
embed-v4.0Cohere. Multilingual, multimodal, supports image+text in one space.
voyage-3-largeVoyage. Tops MTEB retrieval English; uses input_type prefixes.
voyage-code-3Voyage. Tuned for code search.

Local · OSS

BAAI/bge-small-en-v1.5384 dim. Tiny, fast, great English baseline.
BAAI/bge-large-en-v1.51024 dim. Slower but strong on benchmarks.
BAAI/bge-m3Multilingual (100+ langs), dense + sparse + ColBERT in one model.
intfloat/e5-large-v2Use query: / passage: prefixes — asymmetric.
nomic-embed-text-v1.58k context window. Matryoshka-truncatable.
jina-embeddings-v3Multilingual, task-LoRA (retrieval / classification / clustering).
Cardinal rule: the model that embeds your documents must be the same one that embeds your queries. Switching = re-index everything.

SDK callsHosted API

python
from openai import OpenAI

client = OpenAI()

# Single
resp = client.embeddings.create(
    model="text-embedding-3-small",
    input="The quick brown fox",
)
vec = resp.data[0].embedding   # list[float], dim=1536

# Batched — much cheaper than a loop
resp = client.embeddings.create(
    model="text-embedding-3-small",
    input=["doc one", "doc two", "doc three"],
)
vecs = [d.embedding for d in resp.data]

# Matryoshka — truncate to a smaller dim
short = client.embeddings.create(
    model="text-embedding-3-large",
    input="hello",
    dimensions=512,         # 3072 → 512, preserves quality
).data[0].embedding

Cohere · input_type matters

co.embed(texts=[…], model="embed-v4.0", input_type="search_document")For corpus / docs.
co.embed(texts=[…], model="embed-v4.0", input_type="search_query")For queries. Cohere uses different projections per type.
input_type="classification"For classifier features.
input_type="clustering"For k-means etc.
embedding_types=["float","int8"]Get quantized + float in one call.

Voyage · same idea

vo.embed([…], model="voyage-3-large", input_type="document")For corpus.
vo.embed([q], model="voyage-3-large", input_type="query")For query.
output_dimension=512Matryoshka shrink.

Run on your hardwareLocal · OSS

python
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("BAAI/bge-small-en-v1.5")

# Encode — returns np.ndarray of shape (n, dim)
vecs = model.encode(
    ["query text", "another doc"],
    batch_size=64,
    normalize_embeddings=True,   # cosine ≈ dot product
    show_progress_bar=False,
)

# Asymmetric retrieval: prefix queries (model-specific)
q_vec = model.encode(
    "Represent this sentence for searching: what is RAG?",
    normalize_embeddings=True,
)

# Move to GPU
model = SentenceTransformer("BAAI/bge-large-en-v1.5", device="cuda")

fastembed · ONNX, no torch

TextEmbedding("BAAI/bge-small-en-v1.5")Lazy-downloads ONNX weights. ~80MB install vs. ~2GB for torch.
list(model.embed(texts))Returns generator of np arrays. Streaming-friendly.
model.passage_embed(docs)Passage-side; prefixes added for you.
model.query_embed("q")Query-side. Prefer this over embed() for retrieval.

Ollama · local server

ollama pull nomic-embed-textShell: download the model.
ollama.embed(model="nomic-embed-text", input="text")Single call; returns {"embeddings":[[…]]}.
ollama.embed(model=…, input=["a","b"])Batched; one HTTP round trip.

Cosine · dot · L2Distance metrics

cosine(a, b) = a·b / (|a||b|)Direction only. Magnitude-invariant. Default for sentence embeddings.
dot(a, b) = a·bEquals cosine when both vectors are unit-normalized. Faster.
euclidean(a, b) = |a - b|L2 distance. Cares about magnitude. Rare for text.
manhattan(a, b) = Σ|a−b|L1. Niche; sometimes used in word2vec follow-ups.
hamming(a, b)For binary-quantized embeddings (int8 / 1-bit).
python
import numpy as np

def cosine(a, b):
    a, b = np.asarray(a), np.asarray(b)
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))

def dot(a, b):
    return float(np.dot(np.asarray(a), np.asarray(b)))

def euclidean(a, b):
    a, b = np.asarray(a), np.asarray(b)
    return float(np.linalg.norm(a - b))

# Top-k over a corpus matrix (rows = normalized vectors)
def top_k(query, matrix, k=5):
    q = np.asarray(query)
    scores = matrix @ q              # dot == cosine when normalized
    idx = np.argpartition(-scores, k)[:k]
    return idx[np.argsort(-scores[idx])], scores[idx]
Most hosted APIs return already-normalized vectors (OpenAI, Cohere). Most local models do not — pass normalize_embeddings=True or divide by the norm yourself.

Shrink without re-trainingDimensions & matryoshka

dimensions=512 # OpenAI paramTruncates + re-normalizes server-side.
output_dimension=256 # VoyageSame idea. Trained for it — quality holds.
vec[:512] # local matryoshka modelFor nomic / bge-m3 — truncate then re-normalize.
vec / np.linalg.norm(vec)Re-normalize after truncation.

Matryoshka models are trained so that the first n dimensions are a valid lower-dim embedding. Cuts index size and ANN cost roughly linearly. Don’t truncate a non-matryoshka model — quality collapses.

Fit inside the contextTokens & chunking

enc = tiktoken.encoding_for_model("text-embedding-3-small")Get the right BPE.
len(enc.encode(text))Pre-billing token count.
chunk_size=800, chunk_overlap=150Default for English prose. Tune for code / tables.
RecursiveCharacterTextSplitterSplits on ¶ → sent → word. Use from langchain-text-splitters.
SemanticChunker(embeddings)Splits at embedding-distance breakpoints. Slower; sometimes worth it.
model.max_seq_lengthHard limit per chunk; over => silent truncation in most SDKs.
Embed lower-cased text only if the model expects it. Modern BPE models (OpenAI, BGE, Voyage) are case-sensitive and trained on mixed case — don’t pre-lowercase.

Shrink the indexQuantization & storage

np.float32Default. 4 bytes / dim. 1536×4 = 6.1 KB / vector.
np.float16Half memory; ~0% recall loss for retrieval.
int8 quantization4× smaller; ~1–3% recall loss. Supported natively by Cohere, Qdrant, Milvus.
binary (1-bit) quantization32× smaller; ~5–10% recall loss; pair with Hamming + re-ranking.
PCA / re-projectionDimensionality reduction on a frozen corpus. Avoid — matryoshka is strictly better.

Two-stage retrievalRerankers

Bi-encoders (what this sheet covers) embed query & docs independently — fast but loose. Cross-encoders score (query, doc) pairs jointly — slow but accurate. Use a bi-encoder for top-100 recall, a cross-encoder to re-rank to top-10.

CrossEncoder("BAAI/bge-reranker-large")Local SOTA reranker; .predict([(q, d), …]) → scores.
co.rerank(query, documents, model="rerank-v3.5")Cohere hosted reranker. Drop-in API.
voyageai.rerank(query, documents, model="rerank-2")Voyage hosted reranker.
jina-reranker-v2-base-multilingualOpen weights; multilingual.

Beyond plain denseSpecial embeddings

Sparse (SPLADE / BM25)Lexical match in a dense vector format. Pair with dense for hybrid search.
ColBERT · late-interactionOne vector per token; maxsim at query time. Higher recall, ~100× storage.
CLIP · image+text shared spaceopenai/clip-vit-base-patch32. Search images by text and vice-versa.
CodeBERT / voyage-code-3Trained on (NL, code) pairs. Use for code search.
Multilingual · bge-m3 / embed-v4One space across 100+ languages. Cross-lingual retrieval works without translation.

$ per 1M tokensCost & batching

text-embedding-3-small$0.02 / 1M tokens. Cheapest hosted.
text-embedding-3-large$0.13 / 1M tokens.
voyage-3-large$0.18 / 1M tokens.
embed-v4.0 (Cohere)$0.12 / 1M tokens.
Local (bge-small on CPU)~200 docs/sec on a laptop. $0.
Batch size 64–256Sweet spot for hosted APIs. Bigger => same TPS but fewer round trips.
Always batch. Embedding 10k docs one-at-a-time over HTTP costs the same in tokens but ~50× in wall-time vs. batching at 256.

Embed · index · query in ~25 linesEnd-to-end · minimal retrieval

No vector DB, no framework — just NumPy. Useful baseline before reaching for Chroma / Qdrant.

python
from openai import OpenAI
import numpy as np

client = OpenAI()
MODEL = "text-embedding-3-small"   # 1536 dim, $0.02 / 1M tokens

def embed(texts: list[str]) -> np.ndarray:
    resp = client.embeddings.create(model=MODEL, input=texts)
    arr = np.array([d.embedding for d in resp.data], dtype=np.float32)
    return arr / np.linalg.norm(arr, axis=1, keepdims=True)   # normalize

# 1. Build the corpus
corpus = [
    "FastAPI is a Python web framework for building APIs.",
    "Ollama runs LLMs locally on your machine.",
    "Embeddings map text to dense vectors.",
]
M = embed(corpus)

# 2. Query → vector
q = embed(["how do I run a model locally?"])[0]

# 3. Top-k by cosine (== dot when normalized)
scores = M @ q
top = np.argsort(-scores)[:2]
for i in top:
    print(f"{scores[i]:.3f}  {corpus[i]}")

Best practiceGood to know

Normalize once at write time, never at query time. Store unit vectors in your index; queries normalize once. Then cosine collapses to a single dot product — an order of magnitude faster on large indexes.
Asymmetric models need prefixes or input_type. E5, BGE, Cohere, Voyage all distinguish “query” vs “passage”. Skipping the prefix silently halves recall — no error, just bad results.
Re-rank cheap; re-embed expensive. A 100→10 cross-encoder pass on top results lifts NDCG more than swapping to a bigger embedder, at a fraction of the index cost.

Common trapsWatch out for

Don’t mix models. Embedding documents with text-embedding-3-small and queries with 3-large gives nonsense — they live in different spaces. Pin the model in config and re-index when you change it.
Silent truncation past max_seq_length. Sentence-Transformers + most hosted APIs truncate over-long inputs without warning. A 50-page PDF becomes its first ~512 tokens. Always pre-chunk.
Cosine on un-normalized local vectors is broken. If normalize_embeddings=False (the default for raw .encode()), np.dot is not cosine. Normalize, or use the explicit formula.

Go deeperSee also

Embeddings FAQ

What is a text embedding?

A text embedding is a fixed-length vector of floating-point numbers that represents the semantic meaning of a piece of text. Similar texts produce vectors that are close together in the vector space, enabling similarity search, clustering, and retrieval-augmented generation (RAG). Embeddings are generated by encoder models, not generative LLMs.

How do I choose an embedding model?

Start with the MTEB leaderboard to compare models by task (retrieval, clustering, classification). For English-only text, OpenAI text-embedding-3-small or Cohere embed-v4 are strong hosted choices. For multilingual or on-premise workloads, sentence-transformers/paraphrase-multilingual-mpnet-base-v2 is a solid open-source baseline.

What distance metrics work best for embeddings?

Cosine similarity is the default for most text embedding tasks because it measures angle rather than magnitude and is not affected by document length. Dot product (inner product) is faster and equivalent to cosine when vectors are L2-normalised. Euclidean (L2) distance works but is sensitive to magnitude, so normalise first.

What is chunking for embeddings?

Chunking splits long documents into smaller segments before embedding because models have a fixed token limit (typically 512 or 8 192 tokens) and embedding a full page in one call loses fine-grained semantics. Common strategies include fixed-size chunks with overlap, sentence splitting, and recursive paragraph splitting. Chunk size directly impacts retrieval precision.

How much do text embeddings cost?

OpenAI text-embedding-3-small costs $0.02 per million tokens and text-embedding-3-large costs $0.13 per million. Cohere is similarly priced. Self-hosted models with sentence-transformers on a CPU cost only electricity. For large corpora, batch the API calls and cache embeddings in a vector store to avoid re-embedding unchanged documents.