Vector embeddings: meaning as geometry 🧭

An embedding turns text into a list of numbers so that meaning becomes distance. Similar ideas sit close, unrelated ones sit far. Every section starts with a real problem, gives you a picture, then the plain words, then a demo you can actually poke.

This is the layer under your rag-docs-chatbot project and under the managed retrieval your advisor-portal got from AWS Kendra. Once you can explain why cosine beats keyword search and how you chose a chunk size, the RAG interview questions stop being scary.
Opened 0/9 · tap any section to expand
1

What an embedding actually istext becomes a point in space

🤔 Problem: a computer cannot compare the meaning of "car" and "automobile." As raw strings they share almost no letters, and "car" vs "cat" share two of three. String matching thinks car and cat are close and car and automobile are total strangers. That is backwards.
🌍 Think of it like
A map of a country. Every town gets a latitude and longitude. Now "how related are two towns" is just "how close are their coordinates." An embedding does the same thing for words and sentences: it hands each one a set of coordinates so related meanings land near each other.

An embedding is a fixed-length list of numbers (a vector) that a model assigns to a piece of text so that semantic similarity shows up as geometric closeness. "car" might become [0.02, -0.41, 0.88, ...] and "automobile" a nearly identical list, while "banana" points somewhere else entirely. You never read the numbers directly. You only ever compare them.

"car" embedding modellearns meaning → numbers [0.02, -0.41,0.88, ... ]a point in space
🚧 Gotcha: an embedding is not a hash and not a keyword index. Two texts with zero words in common ("my paycheck cleared" and "the deposit went through") can have nearly identical vectors. That is the whole point, and it is exactly what keyword search cannot do.

Now the words: the numeric list is a dense vector (dense = most entries are non-zero, unlike a sparse bag-of-words). The space it lives in is the embedding space or latent space. The property that similar meaning gives nearby vectors is the model being semantically meaningful. Each position in the vector is a dimension.

2

The semantic map, livepick a word, watch its neighbors light up

🤔 Problem: "similar meanings sit close" sounds nice, but you need to see it and see the actual similarity numbers, otherwise it is just a claim.
🌍 Think of it like
A star chart. Fifteen words are plotted as stars. Choose one and the three nearest stars connect with lines. The real embeddings are hundreds of dimensions; this is a flattened 2D view, but the neighbor math below runs on the actual vectors.

Each word carries a small hand-built vector. Click a star (or use the dropdown). The demo computes cosine similarity from that word to all fourteen others, connects the top three, and prints the numbers. Watch bank pull toward both money and river, and java pull toward both python and coffee. That split is polysemy, one string with two meanings, and it is a real failure mode in retrieval.

2D semantic map · cosine computed live in JS
query word tip: click any star
select a word to see its 3 nearest neighbors by cosine similarity
Read the map like an engineer: king and queen sit close even though one is male and one is female, because "royalty" dominates their vectors. That is the intuition behind the famous king − man + woman ≈ queen vector arithmetic. Directions in the space carry meaning, not just positions.

Now the words: the flattened picture is a projection (real tools use PCA, t-SNE, or UMAP to squeeze 768 dims into 2). The lines connect nearest neighbors. A word with two meaning-clusters pulling on it is exhibiting polysemy, and a single averaged vector for it is a known weakness of static embeddings.

3

Cosine vs Euclidean distancewhy angle beats straight-line for text

🤔 Problem: you have two vectors. To rank "how similar" you need one number. Straight-line (Euclidean) distance is the obvious pick, but a long document and a short query about the same topic have very different vector lengths, and Euclidean would call them far apart even though they mean the same thing.
🌍 Think of it like
Two people pointing. If they both point at the same mountain, they agree, even if one arm is long and one is short. Cosine similarity measures the angle between the arms and ignores their length. Euclidean measures the gap between the fingertips, which changes the moment one arm gets longer.

Move the sliders. The angle slider rotates the blue vector; the length slider stretches it. Notice: cosine only changes when the angle changes. Stretch the vector all you want and cosine holds steady, while Euclidean distance jumps. For text embeddings we care about direction (topic), not magnitude, so cosine is the default.

rotate and stretch vector B · math computed live
A (fixed) B
angle: 50°
length: 90
Cosine similarityEuclidean distance
MeasuresAngle between vectorsStraight-line gap between tips
Range-1 to 1 (1 = identical direction)0 to ∞ (0 = identical)
Length sensitive?No, ignores magnitudeYes, grows with length
Text default?YesOnly after normalizing
🚧 Gotcha: if you L2-normalize every vector to length 1 first, cosine and Euclidean rank results in the exact same order (they become monotonic in each other). That is why many vector databases store normalized vectors and let you use either metric. Cosine distance is just 1 - cosine similarity.

Now the words: cosine similarity is the dot product divided by the product of the two L2 norms (magnitudes). Normalizing means scaling a vector to unit length. The inner-product metric IP equals cosine when vectors are normalized. FAISS and pgvector expose cosine, L2, and inner product as choices.

4

Dimensions and the "curse"why 384, 768, 1536 numbers per vector

🤔 Problem: two dimensions was easy to draw, but you cannot fit the meaning of all of English into a 2D map without everything colliding. So how many numbers do you actually need, and does piling on more dimensions only help?
🌍 Think of it like
Describing a person. Height alone collides millions of people. Add weight, age, city, job, hobbies, and each new axis gives more room to keep distinct people apart. More dimensions means more independent "axes of meaning" the model can use before two different ideas are forced to overlap.

Real models output 384 (all-MiniLM-L6-v2), 768 (many BERT-family models), 1536 (OpenAI text-embedding-3-small), or 3072 dims (text-embedding-3-large). More dimensions buys capacity to separate meanings, but it costs storage, memory, and query time. There is a real tradeoff called the curse of dimensionality: in very high dimensions, random points drift toward being equally far apart, so distances get less discriminating and you lean harder on a good metric and a good model.

how "orthogonal" are two random vectors as dimensions grow?
click to run. watch mean |cosine| collapse toward 0 as dimensions climb.
The payoff, stated for an interview: in high dimensions two random vectors are almost always nearly orthogonal (cosine near 0). That is a feature. It means the model has huge room to place unrelated concepts far apart, and only the pairs it deliberately learned to make similar end up with high cosine. Capacity without forced collisions.
🚧 Gotcha: bigger is not free and not always better. 3072-dim vectors cost roughly double the storage and RAM of 1536, and for many retrieval tasks the accuracy gain is small. text-embedding-3 supports Matryoshka truncation, you can shorten the vector (say 1536 to 512) and keep most of the quality. Benchmark before you pay for more dims.

Now the words: the number of entries is the dimensionality. The distance-flattening problem is the curse of dimensionality / concentration of distances. Nearly-perpendicular vectors are near-orthogonal. Truncating a vector while keeping quality is Matryoshka Representation Learning (MRL).

5

How embeddings get madethe models you would actually call

🤔 Problem: you are not going to hand-write vectors like the demo did. You need a model that has read enough text to know that "paycheck" and "salary" belong together. Which one, and how do you call it?
🌍 Think of it like
Hiring a translator who never sleeps. A model trained on billions of sentences has internalized which words and phrases keep appearing in the same contexts. Ask it for the vector of any text and it returns coordinates consistent with everything it has seen.

Two paths. Hosted APIs: OpenAI text-embedding-3-small (1536 dims, cheap) and text-embedding-3-large (3072 dims), or Cohere embed-english-v3.0 / embed-multilingual-v3.0. Open-source local: sentence-transformers models like all-MiniLM-L6-v2 (384 dims, fast, runs on a laptop) or BAAI/bge-* for higher quality. In your rag-docs-chatbot you can start local with MiniLM, then swap to a hosted model by changing one function.

local, open-source · sentence-transformers · Python
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("all-MiniLM-L6-v2")   # 384-dim, ~80MB

docs = ["The bank raised interest rates.",
        "A puppy chased its tail."]
vecs = model.encode(docs, normalize_embeddings=True)   # L2-normalized
print(vecs.shape)          # (2, 384)
hosted API · OpenAI client · Python
from openai import OpenAI
client = OpenAI()

resp = client.embeddings.create(
    model="text-embedding-3-small",    # 1536 dims
    input=["The bank raised interest rates."],
    dimensions=512                        # optional Matryoshka truncation
)
vec = resp.data[0].embedding          # list[float], length 512
ModelDimsWhereGood for
all-MiniLM-L6-v2384Local (sentence-transformers)Fast, free, prototypes, on-prem/private data
text-embedding-3-small1536OpenAI APICheap hosted default, strong quality
text-embedding-3-large3072OpenAI APITop accuracy when it earns its cost
embed-v3.01024Cohere APIMultilingual, doc/query input types

Now the words: these are sentence / text embedding models, usually a transformer encoder with mean pooling over token vectors and a contrastive fine-tune. The public scoreboard is MTEB (Massive Text Embedding Benchmark). Managed services like AWS Kendra (used in your advisor-portal) and Bedrock Knowledge Bases hide the model choice and do the embedding for you.

6

Chunking documents for RAGwhy you embed pieces, not whole files

🤔 Problem: a 40-page policy PDF has one vector if you embed it whole. That single vector is a blurry average of everything in the document, so a specific question ("what is the wire cutoff time?") retrieves the whole blob and buries the one relevant sentence. And the whole thing will not even fit the model's input limit.
🌍 Think of it like
Index cards, not the whole book. You do not embed the encyclopedia as one card. You cut it into passages, each about one idea, and embed each card. Then a question retrieves the two or three cards that actually answer it. Overlap between cards keeps a sentence from being cut in half at a boundary.

Split each document into chunks (commonly 200 to 800 tokens) and embed each chunk separately. Add overlap (say 10 to 20 percent) so ideas straddling a boundary appear whole in at least one chunk. Small chunks are precise but lose surrounding context; big chunks keep context but dilute the vector and retrieve noise. Drag the sliders and watch the chunk layout change.

40-token document · adjust chunk size and overlap
chunk size: 10
overlap: 2
🚧 Gotcha: overlap must be smaller than chunk size or the window never advances. And do not split blindly by character count through the middle of a table or code block. Prefer structure-aware splitting (by heading, paragraph, or sentence) then pack to a token budget. Store the source doc id and offsets on each chunk so you can cite it.

Now the words: the split unit is a chunk or passage; the shared region is overlap / stride. Splitting by structure is recursive / semantic chunking. Metadata carried with each chunk (source, page, section) enables metadata filtering at query time. A parent-document retriever embeds small chunks but returns the larger surrounding passage.

7

Semantic search, end to endtype a query, watch the corpus reorder

🤔 Problem: a user searches "hot drink" but no document contains those words. Keyword search returns nothing. You want the coffee sentence to come back anyway.
🌍 Think of it like
Sorting index cards by relevance. Every card already has a vector. The query gets a vector too. You rank the cards by cosine similarity to the query and hand back the top few. Words never have to match, meaning does.

Six sentences, each pre-embedded with a tiny real bag-of-concepts model. Type a query (try hot drink, money and loans, kitten by the window, or coding in java) and hit search. The demo embeds your query the same way, scores every sentence by cosine, and animates them into ranked order with a similarity bar. This is exactly the retrieve step of your rag-docs-chatbot before the chunks go into the prompt.

tiny corpus · cosine ranking · animated reorder
type a query and hit search to rank all six by meaning
Why this beats keyword search: "hot drink" scores the coffee sentence highest with zero shared words, and "money and loans" surfaces both the interest-rate and the paycheck sentences. Keyword search (BM25) would miss both. In production you often run hybrid: BM25 for exact terms (names, IDs, error codes) plus vectors for meaning, then merge.

Now the words: this flow is dense retrieval / vector search. At scale you do not compare against every vector; an ANN (approximate nearest neighbor) index like HNSW or IVF-PQ in FAISS, pgvector, Pinecone, or Weaviate finds the top-k fast. Combining dense + sparse and re-ranking is hybrid search with a cross-encoder reranker.

8

Gotchas that bite in productionthe failures interviewers probe for

🤔 Problem: the demo always works. Real pipelines silently return garbage for reasons that never show up as a crash. Knowing these is what separates "I read about embeddings" from "I have shipped RAG."
🚧 Same model for query and documents. Vectors are only comparable if they came from the same model and version. Embed your corpus with MiniLM and your query with OpenAI and every similarity is noise. Pin the model id and store it alongside the vectors.
🚧 Normalization. Some models return unit-length vectors, some do not. Mixing normalized and unnormalized vectors, or picking cosine when your index is set to raw inner product, quietly corrupts ranking. Decide once: normalize everything, use cosine.
🚧 Dimension mismatch. A 1536-dim query cannot be searched against a 384-dim index. Sounds obvious, happens constantly after a model swap. The index dimension is fixed at creation, so changing models means a full re-embed and rebuild.
🚧 Individual dimensions are not interpretable. There is no "dimension 400 = formality." Only the whole vector, compared to another whole vector, carries meaning. Do not try to read or hand-edit single numbers.
🚧 Model / version drift. Upgrading the embedding model changes the geometry of the entire space. Old vectors and new vectors are incompatible, so you must re-embed the whole corpus, ideally behind a version flag with a shadow index during migration.
🚧 Cost and latency. Embedding millions of chunks and re-embedding on every model change costs API money and time. Cache embeddings keyed by content hash, batch requests, and only re-embed changed documents.
One-line defense: "Same model and version for query and docs, everything L2-normalized, cosine metric, embeddings cached by content hash, and re-embed behind a version flag on model upgrades." Say that and the interviewer knows you have run this for real.

Now the words: incompatible query/doc models is a model mismatch. Space geometry shifting on upgrade is embedding drift. Rebuilding vectors is re-indexing / backfill. Serving old and new indexes during migration is a shadow / blue-green index.

9

Interview rapid-firetap a card to flip

The eight questions a senior gets asked about embeddings and RAG retrieval. Tap to reveal a crisp answer.