Vector Embedding & Vector Databases
- An embedding is a vector that captures meaning; similar texts sit close together.
- Use
embed_queryfor the question,embed_documentsfor your chunks β same model. - Rank by cosine similarity; MiniLM = 384-dim (free, local), OpenAI small = 1536.
Embeddings are how RAG turns text into something a computer can compare. This section covers the intuition, the similarity math, and how to actually generate embeddings with HuggingFace and OpenAI models. One submodule per topic, ending with a cheat sheet.
What an embedding isβ
An embedding translates text into numbers β a fixed-length vector that captures meaning. Texts about similar things land close together in this number-space; unrelated texts land far apart.
A tiny 2-D example makes it concrete. Notice the animal words cluster on one side and the vehicle words on the other:
import numpy as np
import matplotlib.pyplot as plt
# Real embeddings have hundreds of dimensions; this is just 2D to visualize.
word_embeddings = {
"cat": [0.8, 0.6],
"kitten": [0.75, 0.65],
"dog": [0.7, 0.3],
"puppy": [0.65, 0.35],
"car": [-0.5, 0.2],
"truck": [-0.45, 0.15],
}
cat/kitten sit together, car/truck sit together, and the two groups are far
apart β that spatial closeness is semantic similarity.
Measuring similarity (cosine)β
To compare two vectors we use cosine similarity β it measures the angle between them, ignoring length:
- close to 1 β very similar
- close to 0 β unrelated
- close to β1 β opposite meaning
def cosine_similarity(vec1, vec2):
dot_product = np.dot(vec1, vec2)
norm_a = np.linalg.norm(vec1)
norm_b = np.linalg.norm(vec2)
return dot_product / (norm_a * norm_b)
cat_vector = [0.8, 0.6, 0.3]
kitten_vector = [0.75, 0.65, 0.35]
car_vector = [-0.5, 0.2, 0.1]
cosine_similarity(cat_vector, kitten_vector) # β 0.99 β very similar
cosine_similarity(cat_vector, car_vector) # β 0.30 β unrelated
The big gap between the two scores is exactly what retrieval relies on to rank chunks.
Your first embeddings β HuggingFaceβ
HuggingFaceEmbeddings runs an open model locally β no API key needed. The classic
starter model is all-MiniLM-L6-v2, which outputs a 384-dimension vector.
from langchain_huggingface import HuggingFaceEmbeddings
# Local, free, no API key
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
text = "Hello, I am learning about embeddings!"
embedding = embeddings.embed_query(text)
print(len(embedding)) # 384 β the model's fixed vector size
Embedding a query vs a batch of documentsβ
Two methods, and the difference matters:
embed_query(text)β one string β one vector. Use for the user's question.embed_documents([...])β a list of strings β a list of vectors. Use to embed all your chunks at once (much faster than looping one at a time).
sentences = [
"The cat sat on the mat",
"The dog played in the yard",
"I love programming in Python",
"Python is my favorite programming language",
]
doc_vectors = embeddings.embed_documents(sentences) # one vector per sentence
print(len(doc_vectors), len(doc_vectors[0])) # 4 vectors, each 384-dim
OpenAI embeddings (the API alternative)β
When you want a hosted, higher-quality model instead of a local one, swap in
OpenAIEmbeddings β same embed_query / embed_documents interface, but it calls the
API (needs a key).
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small") # 1536-dim
vector = embeddings.embed_query("Hello, embeddings!")
The interface is identical, so you can switch providers without rewriting your pipeline β just don't mix models between indexing and querying.
Choosing an embedding modelβ
Different models trade size (quality/storage) against speed. Common open choices:
| Model | Dim | Best for |
|---|---|---|
all-MiniLM-L6-v2 | 384 | fast, general purpose, real-time |
all-MiniLM-L12-v2 | 384 | slightly better, a bit slower |
all-mpnet-base-v2 | 768 | best quality, slower |
multi-qa-MiniLM-L6-cos-v1 | 384 | Q&A / semantic search |
paraphrase-multilingual-MiniLM-L12-v2 | 384 | 50+ languages |
Start with all-MiniLM-L6-v2; move up to mpnet only if your eval set shows it's worth
the extra cost and latency.
Cheat sheetβ
| Task | Code |
|---|---|
| Local model (no key) | HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2") |
| Hosted model | OpenAIEmbeddings(model="text-embedding-3-small") |
| Embed one query | embeddings.embed_query(text) |
| Embed many chunks | embeddings.embed_documents([...]) |
| Compare two vectors | cosine_similarity(a, b) β 1 = same, 0 = unrelated |
| MiniLM size | 384 dimensions Β· OpenAI small = 1536 |
- Embedding queries and documents with different models β incomparable vectors.
- Re-embedding only new docs after switching models β you must re-embed everything.
- Confusing
embed_query(one string) withembed_documents(a list); using the wrong one breaks shapes or wastes calls. - Comparing raw distances without knowing the metric (cosine: higher = closer; L2: lower = closer).
Embeddings = meaning as a vector; cosine similarity ranks them. Use embed_query for the question and embed_documents for your chunks, with the same model on both sides. Start with all-MiniLM-L6-v2 (384-dim, free, local).
Quick self-check
What does an embedding represent?
The meaning of a piece of text as a fixed-length vector β similar meanings land close together.
embed_query vs embed_documents?
embed_query embeds one string (the question); embed_documents embeds a list (your chunks) in one batch.
How many dimensions does all-MiniLM-L6-v2 output?
384. (OpenAI text-embedding-3-small = 1536.)
What does cosine similarity of ~0.9 vs ~0.1 mean?
~0.9 = very similar meaning; ~0.1 = basically unrelated. That gap is what retrieval uses to rank chunks.
Related: Cosine similarity (Glossary) Β· Vector Stores β
Next: Vector Stores & Vector Databases β coming soon (studying next).