lumo

Module 04 · Lesson 1 of 10Free

Semantic search: finding by meaning, not keywords

Level
Beginner
Topic
Embeddings and vector search
Reading time
12 min

When you finish this page you can

  1. Explain why keyword search misses documents that mean the same thing, and why semantic search misses exact identifiers such as order numbers.
  2. Trace the semantic search pipeline end to end, and say which steps run once per document and which run on every search.
  3. Estimate how many vectors a corpus needs, the storage they take and the cost of embedding them once.
  1. Calling a model API: request, response and cost15 min
  2. Embeddings: turning words into numbers with meaning13 min

Video lesson · coming soon

The panel suggests the wrong tickets

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.

Tomás, an engineer at Deskora, is building a new panel called “Suggested answers”. When a support agent opens a ticket, the panel shows the 3 most similar resolved tickets and the 2 most relevant help-center articles from the same shop. The agent types nothing. The ticket itself is the search.

Priya is a support agent at a kitchenware shop that uses Deskora. She opens a new ticket from Ngozi: “Hi, my parcel never came. It’s order 48213 and I paid for express.”

Tomás’s first version uses keyword search. It ranks documents by the words they share with the ticket, using an that maps each word to the documents containing it 4.

Diagram not drawn yet: suggested-answers-keyword-panel.svg

Ngozi’s ticket, “Hi, my parcel never came. It’s order 48213 and I paid for express”, beside the Suggested answers panel that keyword search fills. Tickets: first, “Parcel came damaged, order 51190”, sharing parcel, came and order, marked wrong problem; second, Ngozi’s earlier ticket “Order 48213: can I change the address?”, sharing order and 48213, marked useful; third, “Paid twice for my order”, sharing paid and order, marked wrong problem. Articles: “Express shipping prices”, sharing express, and “How to place an order”, sharing order, both marked wrong problem. Greyed out below, not shown: the article “Delivery not received: what to do” and the ticket “Package not delivered”, sharing no words.

Keyword search fills the panel with documents that share words, not meaning. It does find Ngozi’s earlier ticket about order 48213, because that number is an exact match.

The article Priya needs is called “Delivery not received: what to do”. It means exactly what Ngozi wrote and shares none of her words. So Priya ignores the panel and searches the help center by hand, and Ngozi waits longer.

This lesson answers one question: how can the panel find documents that mean the same thing as the ticket, and what does that cost across Deskora?

Deskora’s 1,500 shops and 120,000 new tickets a day are facts used across this course. The other numbers in this lesson are estimates for Deskora’s retrieval features, or assumptions labelled where they appear.

Semantic search ranks by meaning

Semantic search finds the documents whose meaning is closest to the query. It works on embeddings. An is a list of numbers, a vector, that a model produces for a piece of text. Texts with similar meaning get vectors that point in similar directions. Embeddings shows how they work.

So “parcel never came” and “delivery not received” get vectors that sit close together, with no shared words. Closeness is one number, such as . Similarity measures compares the choices.

Semantic search is that comparison, run against every document a shop has.

Check yourselfAnswer in your head first, then reveal
Priya’s shop has 40,000 resolved tickets and 150 help-center articles. When she opens Ngozi’s ticket, how many texts does Deskora send to the embedding model?Show answerHide answer
One: Ngozi’s ticket. The 40,150 documents were embedded earlier, once each, and their vectors are stored. Not 40,151: embedding every document on every search would repeat the same work for every ticket every agent opens.

Two paths: write time and query time

The work splits into two paths. The write path runs once per document, in the background. The query path runs every time an agent opens a ticket.

Diagram not drawn yet: semantic-search-write-and-query-paths.svg

Top row, the write path, marked “once per document, in the background”: W1, the ticket database tells the indexing worker a ticket was resolved or an article saved; W2, the worker sends the text to the embedding model; W3, a vector of 1,024 numbers comes back; W4, the worker saves the document id, shop id and vector in the vector store. Bottom row, the query path, marked “every time an agent opens a ticket”: Q1, Priya’s browser opens Ngozi’s ticket on the Deskora app server; Q2, the app server sends the ticket text to the same embedding model; Q3, one vector comes back; Q4, the app server asks the vector store for the nearest 3 tickets and 2 articles from Priya’s shop only; Q5, ids and scores come back; Q6, the app server loads those documents from the ticket database; Q7, the panel appears.

The write path embeds each document once and stores its vector. The query path embeds only the new ticket, finds the nearest stored vectors and loads their documents.

Write path, when a ticket is resolved or an article is saved:

  1. An indexing worker picks up the document’s text.
  2. It sends the text to an embedding model, a model that returns a vector instead of writing text.
  3. It stores the vector in a vector store, next to the document’s id. Here a vector store is a table of vectors that can be searched by closeness; Vector databases covers what dedicated ones add.

One stored record looks like this:

json
{
  "doc_id": "article-0093",
  "shop_id": "shop-0412",
  "kind": "article",
  "title": "Delivery not received: what to do",
  "model": "voyage-4",
  "vector": [0.0213, -0.0087, 0.0441 /* ... 1,021 more numbers */]
}

The shop_id is there because a shop must never see another shop’s tickets, so every search is filtered by shop (Filtering vector search by metadata). The model is there because vectors from two different models can’t be compared.

Query path, when Priya opens a ticket:

  1. The app server sends Ngozi’s ticket to the same embedding model, and gets one vector back.
  2. It asks the vector store for the nearest vectors with Priya’s shop_id: the top 3 tickets and the top 2 articles.
  3. It loads those documents by id and shows them in the panel.

The simplest way to find the nearest vectors is to compare the query with every stored vector and keep the best five. For Priya’s shop that is 40,150 vectors of 1,024 numbers each, about 41 million multiplications per panel. One server core does that in well under a second, by our estimate.

Write path Query path
Runs Once per document, in the background Every time an agent opens a ticket
Embedding calls One per document One per panel
Grows with Documents added or changed Panels opened
If it fails A new document is missing from results until a retry Priya sees an empty panel

When should Deskora embed a document?

At query time, together with the ticket.

Every panel re-embeds all 40,150 documents. It costs about 60 million tokens per panel and takes far too long to show when Priya opens a ticket.

In a nightly batch.

A nightly batch job embeds the day’s documents at a lower price. Until it runs, a new or edited article is missing or out of date.

Why this choice

Reading the text is the expensive part. Doing it once per document turns every later search into cheap arithmetic.

Embedding at write time keeps the index close to current. That matters when the refund policy article changes and Priya must see the new one. A nightly batch saves money, but the savings are small at Deskora’s volume, as the next section shows.

When the choice changes. A first-time index of millions of old tickets has no deadline, so it suits a batch job. Keeping an index current after edits and deletes is a topic of its own, covered in Keeping a RAG index fresh.

The numbers for Deskora

How big is this index, and what does it cost to build? Three numbers decide it: how many vectors, the storage they take, and the embedding bill.

Two of the inputs are assumptions. Deskora uses voyage-4, which returns 1,024 numbers per text by default, each a 32-bit number of 4 bytes 1. Its price, as of September 2026, is $0.06 per million input tokens 2. Treat both as assumed: Embedding models covers how to choose a model.

Vectors, Priya’s shop

40,000 tickets + 150 articles = 40,150 vectors

One vector per document.

Vectors, all of Deskora

30,000,000 tickets + 1,500 shops × 120 articles = 30,180,000 vectors

Deskora keeps 12 months of resolved tickets, about 30 million.

Storage per vector

1,024 dimensions × 4 bytes = 4,096 bytes

Storage = vectors × dimensions × 4 bytes. This counts the raw numbers only.

Storage, Priya’s shop and all of Deskora

40,150 × 4,096 bytes ≈ 164 MB 30,180,000 × 4,096 bytes ≈ 124 GB

Priya’s shop fits in memory on one server. All of Deskora is a database-sized problem.

Embedding cost, all of Deskora, once

30,000,000 × 1,500 tokens × $0.06 ÷ 1,000,000 = $2,700 180,000 × 800 tokens × $0.06 ÷ 1,000,000 ≈ $9

Assumed: a resolved ticket averages 1,500 tokens; articles are about 800 tokens. Long case: if tickets averaged 4,000 tokens, the tickets would cost $7,200.

Embedding cost, Priya’s shop, once

(40,000 × 1,500 + 150 × 800) tokens × $0.06 ÷ 1,000,000 ≈ $3.61

The same assumptions, for one shop.

Ticket counts and token sizes are estimates for this scenario. The price is Voyage AI’s list price for voyage-4 as of September 2026, used as an assumption.

After the first build, the costs are small. Keeping 12 months means about 82,000 tickets join the index each day (30 million ÷ 365). At 1,500 tokens each, that is about $7.40 a day.

The unit of cost is one completed task: one panel shown. A panel needs one embedding call for the ticket. Assume 500 tokens when the ticket is first opened: 500 × $0.06 ÷ 1,000,000 = $0.00003. For 120,000 new tickets a day, that is about $3.60 a day. For comparison, Deskora’s ticket summaries cost about $38 a day on Claude Haiku 5.5, as of October 2026 (an estimate from Calling a model API). Retries and panels opened more than once are left out.

So the embedding bill is not the hard part. The 124 GB of vectors and the search over them are.

Where semantic search misses: order 48213

Tomás switches the panel to semantic search. The article “Delivery not received: what to do” now ranks first. But something else has gone.

Diagram not drawn yet: suggested-answers-semantic-panel.svg

The same ticket from Ngozi beside the Suggested answers panel that semantic search fills. Tickets: first, “Package not delivered”, marked useful; second, “Courier says delivered but nothing arrived”, marked useful; third, “Express parcel still not here after 5 days”, marked useful. Articles: “Delivery not received: what to do” and “Delivery times by region”, both marked useful. Greyed out below, not shown: Ngozi’s earlier ticket “Order 48213: can I change the address?”.

Semantic search finds the documents that mean the same thing as Ngozi’s ticket. It loses her earlier ticket about the same order, which keyword search found.
Check yourselfAnswer in your head first, then reveal
Why does Ngozi’s earlier ticket about order 48213 drop out of the semantic results?Show answerHide answer
Its meaning is about changing an address, not a missing parcel, and the order number is a tiny part of either text. An embedding places 48213 close to 48231 or any other order number, so the exact match earns almost nothing. Not a bug in the vector store: the closest meaning won, as designed.

The two searches fail in opposite places:

Ticket says Keyword search Semantic search
“parcel never came” Misses “delivery not received” Finds it
“order 48213” Finds the exact order Ranks any order number about the same
An error code, a product SKU Finds the exact string Often misses it

A common belief is that semantic search replaces keyword search. It is half right. On question answering, a dense retriever found the right passage in its top 20 for 9 to 19 percentage points more questions than a strong keyword ranker 3. But order numbers, codes and names are exact strings, and only keyword search matches exact strings. Many systems run both and merge the results, covered in Hybrid search.

What changes at Deskora’s scale

Checking every vector works for Priya’s 40,150. Two things break as Deskora grows:

  • Search work grows with every document. A shop with 2 million tickets needs about 2 billion multiplications per panel. Approximate nearest neighbours explains how indexes skip most of them, at the price of sometimes missing a match.
  • Storage grows with dimensions. Halving the dimensions halves the 124 GB. Fewer dimensions can cost some search quality, so test the change on real tickets first.

Where this shows up

Tomás assumed 1,024 numbers from one model, and every number above depends on that choice. Which model should he pick, and how would he know?

Common mistakes

Embedding the documents on every search

The documents don’t change between searches, so their vectors don’t either. Embed each one once on the write path, and embed only the query on the query path.

Searching every shop’s vectors

A vector store returns the nearest vectors it has, whoever owns them. Without a shop filter on every search, Priya could see another shop’s customer’s ticket.

Dropping keyword search entirely

Agents and customers write order numbers, codes and names. Semantic search treats them as noise. Keep a keyword path for exact identifiers.

Check your understanding

Check yourselfAnswer in your head first, then reveal
The shop edits its refund policy article. Which path runs, and roughly what does it cost?Show answerHide answer
The write path, for that one article: one embedding call of about 800 tokens, about $0.00005 at the assumed price, then its stored vector is replaced. Not a rebuild of the whole index: the other 40,149 vectors haven’t changed.
Check yourselfAnswer in your head first, then reveal
Deskora switches to 512-dimension vectors. What happens to storage for all of Deskora, and to the embedding bill?Show answerHide answer
Storage halves, to about 62 GB, because it is vectors × dimensions × 4 bytes. The bill does not halve: the price is per input token, not per number returned, and every document must be embedded again, about $2,700 at the assumed price.
Check yourselfAnswer in your head first, then reveal
A customer writes “Where is order 51190?” The panel shows three tickets about late parcels but not the earlier ticket about that order. What should Tomás add?Show answerHide answer
An exact keyword match on the order number, merged with the semantic results. The embedding treats one order number much like another. Not a better embedding model: exact strings are what keyword search is for.
Check yourselfAnswer in your head first, then reveal
A teammate says the index is built once, so the panel is free after that. What does each panel still cost?Show answerHide answer
One embedding call for the ticket, about $0.00003 at the assumed price and 500 tokens, plus a search over the shop’s vectors. Across 120,000 tickets a day that is about $3.60 a day: small, but not free.

Summary

  1. The panel finds documents that mean the same as the ticket by embedding both and ranking stored documents by how close their vectors are to the ticket’s.
  2. The write path embeds each document once and stores its vector with its id, shop and model. The query path embeds only the new ticket, finds the nearest vectors and loads their documents.
  3. Storage = vectors × dimensions × 4 bytes: about 164 MB for Priya’s shop and 124 GB for all of Deskora at 1,024 dimensions.
  4. Embedding all of Deskora once costs about $2,700 at the assumed price; each panel costs about $0.00003. The vectors and the search, not the embedding bill, are the hard part.
  5. Semantic search wins on meaning and loses on exact identifiers such as order 48213, where keyword search wins.

Report a mistake

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