lumo

Module 02 · Lesson 2 of 12Free

Embeddings: turning words into numbers with meaning

Level
Beginner
Topic
How LLMs work: what builders need to know
Reading time
13 min

When you finish this page you can

  1. Explain what an embedding is and why texts with similar meaning get similar lists of numbers.
  2. Work out the cosine similarity of two short vectors by hand and say what the score means.
  3. Explain where embeddings come from, an embedding model with its own API, and what builders use them for.
  4. Choose between keyword search, a language model and embeddings for finding similar tickets, and say what would change the choice.
  1. Calling a model API: request, response and cost15 min

Video lesson · coming soon

Two tickets with no words in common

Deskora is a help desk app. A shop’s support team uses it to answer customer emails from one shared inbox. Each customer problem becomes a ticket: one customer’s whole conversation with the support team.

Priya is a support agent at a kitchenware shop that uses Deskora. A new ticket arrives from Daniela: “My parcel never came.” The shop has 40,000 past tickets, and dozens of them solved this exact problem. Priya would like to see how.

Tomás, an engineer at Deskora, is building a “Similar past tickets” panel for her. His first version uses keyword search. It ranks past tickets by the words they share with the new one, ignoring small words such as “my” and “I”. It looks words up in an , so it is fast.

Diagram not drawn yet: embeddings-keyword-search-miss.svg

On the left, the new ticket: “My parcel never came”, with its words parcel, never and came highlighted. On the right, three past tickets ranked by shared words. First: “My parcel came damaged”, 2 shared words (parcel, came), marked wrong problem. Second: “I never got my refund”, 1 shared word (never), marked wrong problem. Last: “Package not delivered”, 0 shared words, marked the ticket Priya needs.

Keyword search ranks the damaged-parcel ticket first. The ticket Priya needs shares no words with Daniela’s, so it comes last.

The ticket Priya needs says “Package not delivered.” It means the same thing and shares no words.

Check yourselfAnswer in your head first, then reveal
Tomás adds a synonym list that treats parcel and package as the same word. Does “Package not delivered” now rank first?Show answerHide answer
No. It now shares one word, the same as the refund ticket, while “My parcel came damaged” still shares two. Not “more synonyms will fix it”: “never came” and “not delivered” say the same thing with different words and grammar, and no list covers every way customers write.

This lesson answers one question: how can software tell that two pieces of text mean the same thing when they share no words?

The 40,000 past tickets, and the other numbers in this lesson except Deskora’s 1,500 shops and 120,000 tickets a day, are estimates for this lesson’s scenario.

An embedding is a list of numbers

An is a list of numbers that stands for a piece of text. A model produces it. It places the text so that texts with similar meaning get similar numbers. A list of numbers like this is called a vector.

Here is the start of a real one, from Anthropic’s embeddings guide. Voyage AI’s voyage-4 model turned the text “Sample text 1” into 1,024 numbers 1:

json
{
  "object": "embedding",
  "embedding": [-0.013131560757756233, 0.019828535616397858 /* ... 1,022 more */],
  "index": 0
}

No single number has a meaning you can name. The meaning is in how the whole list compares with other lists.

To see how this works, take a hand-made version with two numbers per ticket. Pretend the first number means “about a delivery that didn’t arrive” and the second means “about a damaged item”:

Ticket Hand-made vector
New: “My parcel never came” [0.9, 0.1]
Past: “Package not delivered” [0.8, 0.2]
Past: “My parcel came damaged” [0.4, 0.9]

Diagram not drawn yet: embeddings-ticket-vectors-2d.svg

A graph with two axes: the horizontal axis is “delivery didn’t arrive”, the vertical axis is “item damaged”, both from 0 to 1. Three arrows start at the origin. “My parcel never came” points to (0.9, 0.1). “Package not delivered” points to (0.8, 0.2), 7.7 degrees away from it. “My parcel came damaged” points to (0.4, 0.9), 59.7 degrees away from the new ticket.

Hand-made two-number vectors drawn as arrows. The two tickets about a missing parcel point almost the same way; the damaged-parcel ticket points elsewhere.

The two missing-parcel tickets point almost the same way, with no shared words. The damaged-parcel ticket, keyword search’s top match, points far away.

To rank 40,000 tickets, Tomás needs “almost the same way” as a single number.

Closeness is one number: cosine similarity

measures how closely two vectors point the same way. It ignores how long they are:

cosine similarity=(A · B)÷(length of A×length of B)
A · B, the dot product, multiplies matching numbers and adds the results. Length = the square root of the sum of each number squared.

The score runs from 1 (same direction) through 0 (unrelated) to −1 (opposite).

Dot product, new and “Package not delivered”

0.9 × 0.8 + 0.1 × 0.2 = 0.72 + 0.02 = 0.74

Multiply the first numbers, multiply the second numbers, add.

Lengths

√(0.9² + 0.1²) = √0.82 = 0.906 √(0.8² + 0.2²) = √0.68 = 0.825

The length of each arrow.

Cosine

0.74 ÷ (0.906 × 0.825) = 0.74 ÷ 0.747 = 0.99

Almost the same direction.

New and “My parcel came damaged”

(0.9 × 0.4 + 0.1 × 0.9) ÷ (0.906 × 0.985) = 0.45 ÷ 0.892 = 0.50

About 60 degrees apart: related, but a different problem.

The ticket Priya needs now ranks first, with 0.99 against 0.50.

Voyage AI returns vectors already scaled to length 1 1. So does OpenAI 2. For such vectors the plain dot product gives the same score, faster. Similarity measures compares cosine, dot product and distance in full.

Here is the same arithmetic in plain Python, with a third ticket added. Run it with any Python 3.9 or newer; it needs no libraries and no API key.

python
import math


def cosine(a: list[float], b: list[float]) -> float:
    dot = sum(x * y for x, y in zip(a, b))
    length_a = math.sqrt(sum(x * x for x in a))
    length_b = math.sqrt(sum(y * y for y in b))
    return dot / (length_a * length_b)


# Hand-made vectors: (delivery didn't arrive, item damaged, account problem)
new_ticket = [0.9, 0.1, 0.0]  # "My parcel never came"
past_tickets = {
    "Package not delivered": [0.8, 0.2, 0.0],
    "My parcel came damaged": [0.4, 0.9, 0.0],
    "How do I reset my password?": [0.0, 0.1, 0.9],
}

ranked = sorted(
    past_tickets.items(),
    key=lambda item: cosine(new_ticket, item[1]),
    reverse=True,
)
for text, vector in ranked:
    print(f"{cosine(new_ticket, vector):.2f}  {text}")

It prints 0.99, 0.50 and 0.01, in that order.

Two numbers can’t hold every meaning

Look at the password ticket in the code. With only the two numbers, “delivery” and “damage”, it has nowhere to go: it is about neither. Tomás had to add a third number, “account problem”, to place it. Its score of 0.01 against Daniela’s ticket says the two are unrelated.

Real tickets differ in hundreds of ways: refunds, sizes, discount codes, tone. Each needs room in the list, so real embeddings are long. As of October 2026, voyage-4 returns 1,024 numbers by default 1. OpenAI’s text-embedding-3-small returns 1,536 and text-embedding-3-large returns 3,072 2. The length of the list is the embedding’s dimension.

In a real embedding nobody chooses what each number means; the model learns where to place texts during training. Cosine similarity works the same way on 1,024 numbers as on two. But where do the numbers come from?

An embedding model makes the numbers

An embedding model is a model that reads text and returns a vector. It does not write text. You call it through its own API, separate from a text-generating API such as the Claude Messages API covered in Calling a model API. The request carries a list of texts and a model name; the response carries one vector per text and a count of the tokens it read 1.

Anthropic doesn’t offer its own embedding model. Its documentation points to Voyage AI as one provider 1. OpenAI also offers embedding models 2. Embedding models covers how to choose one.

Diagram not drawn yet: embeddings-similar-tickets-flow.svg

Left to right. Top row, dashed and marked “once per ticket, in the background”: 1, a closed past ticket goes from the Deskora app server to the embedding model; 2, a vector of 1,024 numbers comes back; 3, the app server saves it next to the ticket in the ticket database. Bottom row, when Daniela’s ticket arrives: 4, the app server sends “My parcel never came” to the embedding model; 5, one vector comes back; 6, the app server compares it with the shop’s 40,000 saved vectors using cosine similarity; 7, the top three past tickets appear in Priya’s “Similar past tickets” panel.

Each past ticket is embedded once and saved. A new ticket needs one embedding call, then the comparison runs on Deskora’s own server.

For the panel, the work splits in two:

  1. Once per past ticket, in the background: send its text to the embedding model and save the vector next to the ticket.
  2. When a new ticket arrives: embed it with the same model, compare its vector with the shop’s saved vectors, and show the top three.

The same vectors serve other features. Builders use embeddings for search, grouping similar items, recommendations, spotting unusual items and sorting items into categories 2. For Deskora, that could mean grouping this week’s tickets to find that 300 are about one courier, or suggesting help-centre articles to Priya. Finding the right documents to hand a model before it answers is RAG, built on the same search.

Three ways to find similar tickets

A language model also understands that “never came” means “not delivered”. Why not ask Claude?

The unit of cost is one completed task: one “Similar past tickets” panel. Asking Claude means one call per past ticket, 40,000 calls a panel. As of October 2026, Claude Haiku 5.5 costs $0.10 per million input tokens and $0.50 per million output tokens 4. It thinks by default, and thinking is billed as output.

The formula

cost = input tokens × input price + (thinking + text tokens) × output price, per call, × calls per panel

Prices are per million tokens, so divide by 1,000,000.

One comparison call

300 × $0.10 ÷ 1,000,000 + (100 + 5) × $0.50 ÷ 1,000,000 = $0.00003 + $0.0000525 = $0.0000825

Assumed: an instruction plus the first message of both tickets, about 300 input tokens; 100 thinking tokens; a 5-token answer. The response’s usage.output_tokens_details.thinking_tokens reports the real thinking count.

One panel

40,000 calls × $0.0000825 = $3.30

First messages only. Whole threads would cost several times more.

One day, Priya’s shop

80 new tickets × $3.30 = $132

80 is Deskora’s average: 120,000 tickets a day across 1,500 shops. Retries are left out.

Twenty thousand calls are also far too slow for a panel Priya sees on opening the ticket. Research on an older model found the same pattern. Finding the most similar pair among 10,000 sentences by comparing every pair took about 65 hours; with embeddings it took about 5 seconds 3.

How should Deskora find past tickets that are similar to a new one?

Keyword search over an inverted index.

Fast and nearly free, and exact on order numbers and error codes. But it ranked “My parcel came damaged” first and missed “Package not delivered” entirely.

Ask Claude to compare the new ticket with every past ticket.

Understands meaning well, but needs 40,000 calls per panel: about $3.30 each, and far too slow to show when Priya opens the ticket.

Why this choice

Embeddings move the expensive part, reading the text, to once per ticket. After that, each search is cheap arithmetic. A language model reads every pair again for every search.

When the choice changes. If agents mostly search by order number or error code, keyword search is the better tool. Many systems run both and merge the results, covered in Hybrid search. A language model also becomes affordable for re-checking a short list: the top 10 from embeddings, not all 40,000.

Where embeddings fall short

  • Exact identifiers. “Order 48213” and “order 48231” differ by two digits. Their vectors can be nearly the same, and keyword search tells them apart better.
  • Small words that flip meaning. “Parcel delivered” and “parcel not delivered” share almost all their meaning, and can score close together. Priya might see a ticket about the opposite problem.
  • Scale. At the default 32-bit size, 1,024 numbers take 4,096 bytes 1. Deskora’s 120,000 tickets a day add about 490 MB of vectors a day. Comparing one new ticket with all of them, one by one, gets slow. Vector databases store vectors and search them without checking every one.
How a model learns where to place texts

An embedding model is a neural network trained on very large amounts of text. Training adjusts it until texts used in similar ways, or pairs known to match, such as a question and its answer, get vectors that point the same way. Early word embeddings learned one vector per word from the words around it 5. Today’s models embed whole sentences and passages at once.

Where this shows up

Inside the language models later lessons open up, each token also starts as a list of numbers. Its numbers must then change with the words around it, so that “charge” in “charge my card” and “charge my phone” mean different things. How the model does that is the subject of Attention.

Common mistakes

Comparing vectors from two different models

Each embedding model places texts in its own way, so a vector from one model means nothing next to a vector from another. Embed the new ticket with the same model as the saved ones. When Deskora switches models, it must embed every past ticket again.

Reusing a similarity cut-off from somewhere else

A score of 0.8 can mean “very similar” for one model and “loosely related” for another. Choose any cut-off, such as “only show tickets above this score”, by testing it on your own tickets with the model you use.

Check your understanding

Check yourselfAnswer in your head first, then reveal
Tomás doubles every number in the vector for “Package not delivered”, from [0.8, 0.2] to [1.6, 0.4]. What happens to its cosine similarity with Daniela’s ticket?Show answerHide answer
Nothing: it stays 0.99. Doubling makes the arrow longer without turning it, and cosine similarity measures direction only. Not “it doubles”: the dot product doubles, but so does the length it is divided by.
Check yourselfAnswer in your head first, then reveal
Deskora upgrades to a newer embedding model and embeds new tickets with it. The saved vectors stay from the old model. What does Priya see?Show answerHide answer
Similar-ticket results that look random, because the two models place texts differently and their numbers can’t be compared. Not an error message: cosine similarity still returns a number, so the failure is silent. Every past ticket must be embedded again with the new model.
Check yourselfAnswer in your head first, then reveal
A customer writes “Where is order 58214?” and Priya wants the earlier tickets about that exact order. Which search finds them best?Show answerHide answer
Keyword search, because the order number is an exact string it can match. An embedding may place 58214 and 58241 almost in the same spot, so similar-looking orders could rank just as high.
Check yourselfAnswer in your head first, then reveal
Why does an embedding of 1,024 numbers find similar tickets better than Tomás’s hand-made three numbers?Show answerHide answer
Three numbers can only separate three kinds of meaning, so a refund ticket or a sizing question has nowhere distinct to go. A thousand numbers give room for the many ways tickets differ, and the model learns where to place each one.

Summary

  1. Software tells that two texts mean the same thing by turning each into an embedding, a list of numbers placed so that similar meanings point the same way, and comparing the lists.
  2. Cosine similarity scores how closely two vectors point the same way: 1 for the same direction, 0 for unrelated. It ignores length.
  3. Real embeddings have about 1,000 to 3,000 numbers, as of October 2026, and no single number has a name.
  4. An embedding model, called through its own API, makes the vectors. Anthropic doesn’t offer one; Voyage AI and OpenAI do.
  5. Embed each item once, embed each query with the same model, and compare: cheap and fast, but weak on exact identifiers and on small words like “not”.

Report a mistake

Tell us what’s wrong in this lesson and we’ll fix it.