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
- Explain why keyword search misses documents that mean the same thing, and why semantic search misses exact identifiers such as order numbers.
- Trace the semantic search pipeline end to end, and say which steps run once per document and which run on every search.
- Estimate how many vectors a corpus needs, the storage they take and the cost of embedding them once.
Builds on 2 lessons
Start with Module 01, lesson 315 minVideo 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.
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.
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 answer
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.
Write path, when a ticket is resolved or an article is saved:
- An indexing worker picks up the document’s text.
- It sends the text to an embedding model, a model that returns a vector instead of writing text.
- 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:
{
"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:
- The app server sends Ngozi’s ticket to the same embedding model, and gets one vector back.
- It asks the vector store for the nearest vectors with Priya’s
shop_id: the top 3 tickets and the top 2 articles. - 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.
At write time, when a ticket is resolved or an article is saved.
Each document is embedded once, seconds after it changes. A panel costs one embedding call, and new documents show up almost at once.
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?”.
Why does Ngozi’s earlier ticket about order 48213 drop out of the semantic results?Show answer
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
- Design: a support assistant over company docs grows this panel into an assistant that drafts a reply from the documents it finds.
- Design: an AI customer support platform uses the same write and query paths for tickets and help articles, across many customers.
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
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.
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.
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
The shop edits its refund policy article. Which path runs, and roughly what does it cost?Show answer
Deskora switches to 512-dimension vectors. What happens to storage for all of Deskora, and to the embedding bill?Show answer
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 answer
A teammate says the index is built once, so the panel is free after that. What does each panel still cost?Show answer
Summary
- 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.
- 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.
- Storage = vectors × dimensions × 4 bytes: about 164 MB for Priya’s shop and 124 GB for all of Deskora at 1,024 dimensions.
- 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.
- Semantic search wins on meaning and loses on exact identifiers such as order 48213, where keyword search wins.