Skip to content

How text becomes numbers

You called embed("the spare set is in the kitchen drawer") and got 1,536 numbers back. This page is about what happened in between.

Fair warning on scope: I’m not going to teach you to train a model. I’m going to give you the amount of understanding that changes decisions you’ll actually make — why long documents behave badly, why your token bill looks like that, why the same word can produce different vectors.

Models don’t read characters, and they don’t quite read words either. They read tokens — chunks that sit somewhere in between, worked out from what’s statistically common in the training data.

"The spare set for the front door is in the kitchen drawer."
["The", " spare", " set", " for", " the", " front", " door",
" is", " in", " the", " kitchen", " drawer", "."]

Common words are single tokens. Rarer ones get split up:

"unbelievable" → ["un", "bel", "iev", "able"]
"antidisestablish" → ["ant", "id", "ise", "stab", "lish"]
"🎉" → often several tokens on its own

Two consequences you’ll meet in practice:

Your bill is in tokens, not words. For ordinary English, roughly 1 token per 0.75 words — so 1,000 words is about 1,300 tokens. Other languages fare worse. The same sentence in Japanese or Hindi can cost two or three times as many tokens, because the tokenizer was fitted mostly on English text. If you’re building for a non-English market, budget accordingly.

There’s a hard input limit. OpenAI’s text-embedding-3-small accepts 8,191 tokens per input — around 6,000 words. Go over and you get an error, not a truncation. This is the single most common reason people need chunking.

Step 2: each token becomes a starting vector

Section titled “Step 2: each token becomes a starting vector”

Every token in the model’s vocabulary has a vector attached to it, learned during training. The model looks each one up in a big table.

At this stage the vectors are context-free. The token "bank" gets the same starting vector whether your sentence is about rivers or mortgages. If we stopped here we’d have 2013-era Word2Vec, and “bank” would be permanently confused.

This is the part that makes modern embeddings work, and it’s where the Transformer architecture earns its reputation.

The model runs the token vectors through a stack of layers. At every layer, each token gets to look at every other token in the input and adjust itself accordingly. That mechanism is called attention, and it means meaning flows between words.

"I sat on the river bank"
"bank" looks around, notices "river",
and shifts towards the geography sense
"I withdrew cash from the bank"
same token, notices "cash" and "withdrew",
shifts towards the finance sense

Do this across a dozen or so layers and each token’s vector has absorbed a great deal about its surroundings. This is why embeddings handle ambiguity, idiom, and negation with any grace at all — the words genuinely inform each other before anything is finalised.

Step 4: many vectors get squashed into one

Section titled “Step 4: many vectors get squashed into one”

After all that, you’ve got one vector per token. But you asked for one vector for the whole sentence. Something has to combine them, and this step is called pooling.

The two common approaches:

  • Mean pooling — average all the token vectors together. Simple, and what most sentence-transformer models do.
  • CLS pooling — many models prepend a special token whose entire job is to accumulate a summary of the sequence. Take that one and discard the rest.

Either way, everything collapses into one fixed-length vector. Which is what makes the output size independent of the input size — 5 tokens or 5,000, you get the same number of dimensions out.

Most providers scale the final vector to length 1 before returning it, which makes cosine similarity a plain dot product. Covered in detail here.

"The spare set is in the kitchen drawer."
┌─────────────────────────────────────────┐
│ 1. Tokenize → 10 tokens │
│ 2. Look up → 10 starting vectors │
│ 3. Attention → 10 context-aware ones │
│ 4. Pool → 1 vector │
│ 5. Normalize → length 1 │
└─────────────────────────────────────────┘
[0.021, -0.014, 0.008, ... ]

Milliseconds on a GPU. And every one of those steps is fixed at training time, which is why the same text always gives you the same vector from the same model — embeddings are deterministic, unlike the chat models you may be more used to.

Three practical takeaways:

Same model, always. Different models produce coordinates in unrelated spaces. Mixing them yields garbage with no error message. Re-embed everything when you upgrade.

Shorter inputs give sharper vectors. Pooling dilutes. Aim for a paragraph or a section, not a document.

Deterministic means cacheable. Same text plus same model equals same vector, forever. Hash your input, cache the result, and stop paying for work you’ve already done.

You’ll mostly pick along three axes:

Smaller / faster Larger / better
Example text-embedding-3-small (1,536d) text-embedding-3-large (3,072d)
Cost Roughly 6× cheaper Higher per token
Storage Half the bytes per vector Double
Quality Good enough for most work Measurably better on hard retrieval

Start small. Genuinely — start small. Most teams never need the large model, and the storage difference gets painful at scale. Upgrade when you have a measurement showing retrieval quality is your bottleneck, not before.

There’s a fuller comparison in choosing an embedding model. For rankings across providers, the MTEB leaderboard is the standard reference. Treat it as a shortlist rather than an answer — benchmark scores on public datasets don’t always survive contact with your particular documents.