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 OpenAI
Hosted embeddings via OpenAI SDK.
from cohere import ClientV2
Cohere v2 client; embed-v4.0 is multilingual + multimodal.
import voyageai
Voyage AI — strong English retrieval.
from sentence_transformers import SentenceTransformer
Local HuggingFace models.
from fastembed import TextEmbedding
ONNX runtime, no torch dependency. Tiny install.
import ollama
Local Ollama embed endpoint.
import numpy as np
Vector ops, norms, similarity.
import tiktoken
Count tokens before billing.
What to pickModels · pick by task
Hosted
text-embedding-3-small
OpenAI. 1536 dim. Default Cheap, fast, good enough for most RAG.
Direction only. Magnitude-invariant. Default for sentence embeddings.
dot(a, b) = a·b
Equals 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 param
Truncates + re-normalizes server-side.
output_dimension=256 # Voyage
Same idea. Trained for it — quality holds.
vec[:512] # local matryoshka model
For 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.
Default for English prose. Tune for code / tables.
RecursiveCharacterTextSplitter
Splits on ¶ → sent → word. Use from langchain-text-splitters.
SemanticChunker(embeddings)
Splits at embedding-distance breakpoints. Slower; sometimes worth it.
model.max_seq_length
Hard 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.
32× smaller; ~5–10% recall loss; pair with Hamming + re-ranking.
PCA / re-projection
Dimensionality 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.
Lexical match in a dense vector format. Pair with dense for hybrid search.
ColBERT · late-interaction
One vector per token; maxsim at query time. Higher recall, ~100× storage.
CLIP · image+text shared space
openai/clip-vit-base-patch32. Search images by text and vice-versa.
CodeBERT / voyage-code-3
Trained on (NL, code) pairs. Use for code search.
Multilingual · bge-m3 / embed-v4
One 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–256
Sweet 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.
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.