Tokens And Embeddings

Searching by Meaning: Real Semantic Search, in Six Languages

0 of 15 complete

0%

Contents

Back|Tokens And EmbeddingsSearching by Meaning: Real Semantic Search, in Six Languages
1/15
38 min left
Prerequisites
What an Embedding Is: Meaning as a List of Numbersrequired
Related Topics
Chunking: The First Lever on Retrieval QualityRetrieval and RAG in ProductionIs There Really a Best Chunk Size? Measured on This Course's Own LessonsRetrieval and RAG in ProductionVector DatabaseDatabase Types & StorageRetrieval Got Better and the System Got WorseLLM Evaluation and Error AnalysisWho Wrote Your Test Questions?LLM Evaluation and Error Analysis
1 of 15

Asking the Librarian, Not the Card Index

In a library, a card index finds books by the exact words on the card. Ask it for "why my website feels slow" and it finds nothing, because no card says that.

A good librarian works differently. She hears what you mean, and points you to the right shelf, where books on the same subject stand together.

An illustration of a library helper at an information desk pointing a visitor with a question note toward a section of shelves where books on the same subject stand together.

Semantic search is the librarian. It uses , from the last lesson, to find text by meaning. In this lesson I build it over every lesson on this site, ask it real questions in six languages, and measure how often it points to the right shelf.

The Words You Need First

If a word below is new, read its line. Embedding and come from the last lesson.

A hand-drawn word list. Semantic search: finding text by meaning. Keyword search: finding text by shared words. Document: one piece of text you can find. Index: stored embeddings of every document. Top 5: the five highest-scoring documents. Ranked first: the document with the highest score.

Semantic search. Finding text by meaning, using , instead of by matching words.

Keyword search. Finding text by the words it shares with the question. No model involved.

Document. Any piece of text you want to be able to find. Here, one lesson's title and short description.

Index. The stored embeddings of all your documents, made once, before anyone searches.

Top 5. The five documents that scored highest for a question.

Ranked first. The document with the highest score.

Ollama. A free program that runs AI models on your own computer.

bge-m3 and nomic-embed-text. The two embedding models from the last lesson. bge-m3 was trained on many languages; nomic-embed-text mostly on English.

How Semantic Search Works

There are two steps, and they happen at different times.

A flowchart in two parts. Every document is embedded once into the index. A question is embedded, compared with the index, and the highest scores come back. A note says the index here held 878 lessons, embedded once.

Once, ahead of time: embed every document and store the . This is the index. With 878 lessons, that is 878 embeddings.

For every question: embed the question, compare it with every stored embedding using , and return the highest scores.

A sequence diagram: the learner sends a question in any language to the model, bge-m3; its embedding goes to the index of 878 lessons; every lesson is scored; the top 5 go back to the learner. Only the question is embedded at search time.

The expensive part, embedding the documents, happens once. Each search only embeds one short question. That is why semantic search can be fast.

Here is that step in Ollama's own documentation. Only one idea matters on this page: you send text, and you get back a list of numbers. (Their example uses a different model, embeddinggemma; the call is the same for bge-m3.)

A real screenshot of Ollama's documentation for the embed endpoint: you send a model name and text, and get back a list of numbers. The example uses a model called embeddinggemma. A note says only one idea matters: send text, get back a list of numbers.

A Small Search You Can Run

Before the big test, here is a small one. Ten lessons, one question, asked in English, Spanish and Hindi. This is the real file, open in my VS Code.

A real screenshot of search_by_meaning.py open in VS Code: ten real lesson titles with short descriptions, a USE_DESCRIPTIONS switch, and one question in English, Spanish and Hindi, each ranked against the lessons with bge-m3.

And this is what it printed.

A real screenshot of VS Code's terminal after running python search_by_meaning.py. For the question in English, Spanish and Hindi, Rate Limiting is first each time, at 0.749, 0.729, 0.736, with Message Queues and Cache-Aside Pattern next.

Each number is the from the last lesson: closer to 1 means closer in meaning. In all three languages, "Rate Limiting" came first, well ahead of the next lesson.

To run it:

  1. Install Ollama from ollama.com. On Mac or Windows, open the Ollama app; on Linux, run ollama serve.
  2. Run ollama pull bge-m3 in a terminal. It is a download of about 1.2 GB.
  3. Save the file below as search_by_meaning.py, as UTF-8 text.
  4. Run python3 search_by_meaning.py (on Windows: python search_by_meaning.py).

You need only Python 3, no extra packages; the "(venv)" in my screenshot is just my own setup. If you see "Connection refused", Ollama is not running. If you see an 404 error, update Ollama.

# Search by meaning: find the closest lesson title to a question, in any language.
# Needs Ollama (ollama.com) running, and:  ollama pull bge-m3
import json, math, urllib.request

def embed(texts):
    body = json.dumps({"model": "bge-m3", "input": texts}).encode()
    req = urllib.request.Request("http://localhost:11434/api/embed", data=body,
                                 headers={"Content-Type": "application/json"})
    return json.loads(urllib.request.urlopen(req).read())["embeddings"]

def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    return dot / math.sqrt(sum(x * x for x in a) * sum(y * y for y in b))

# each lesson's real title, with a short description (most shortened from the lesson's own; Cache-Aside written for this example)
lessons = {
    "Rate Limiting": "Protect your API from abuse and overload by controlling how many requests each consumer can make",
    "Circuit Breaker Pattern": "Prevent cascading failures in distributed systems by failing fast when a downstream service is unhealthy",
    "Consistent Hashing": "The algorithm that makes adding or removing nodes cheap",
    "Database Sharding": "Splitting a database across multiple servers",
    "Read Replicas": "Scaling reads by replicating data to follower databases",
    "Message Queues": "Decouple producers from consumers with a buffer that holds messages until they're processed",
    "Idempotency": "Why some operations are safe to retry and others charge you twice",
    "Bloom Filters": "A space-efficient data structure that tells you if an element is definitely not in a set",
    "Cache-Aside Pattern": "Read from the cache first, and go to the database only on a miss",
    "Latency": "Time delay in system response",
}
USE_DESCRIPTIONS = True   # try False: search on titles alone

# one question, asked in three languages
questions = {
    "English": "How can I stop one user from sending too many requests to my API?",
    "Spanish": "¿Cómo puedo evitar que un usuario envíe demasiadas peticiones a mi API?",
    "Hindi": "एक उपयोगकर्ता को मेरे API पर बहुत ज़्यादा अनुरोध भेजने से कैसे रोकूँ?",
}

names = list(lessons)
texts = [f"{n}. {d}" if USE_DESCRIPTIONS else n for n, d in lessons.items()]
lesson_vectors = embed(texts)          # embed every lesson once
for lang, qv in zip(questions, embed(list(questions.values()))):
    scores = sorted(((cosine(qv, lv), name) for lv, name in zip(lesson_vectors, names)), reverse=True)
    print("question in", lang)
    for score, name in scores[:3]:
        print(f"   {score:.3f}  {name}")

Titles Alone Are Not Enough

Now change USE_DESCRIPTIONS = True to False and run it again. The script then searches on the two or three words of each title only.

Paired bars of how far the top lesson beat the next one. Titles only: English 0.005, Spanish 0.037 with the wrong lesson first, Hindi 0.014. With descriptions: 0.225, 0.202, 0.202.

The lead is the winner's score minus the second place score. With titles alone, "" beat the next lesson by only 0.005 in English. In Spanish it lost: "Message Queues" came first.

With one line of description added, "Rate Limiting" won in all three languages, by about 0.2.

A title like "Rate Limiting" holds very little meaning for the model to work with. A sentence like "controlling how many requests each consumer can make" holds a lot. The model can only match the meaning you give it.

The Big Test

Now the real test, on the whole site.

Two panels. What was searched: 878 lessons as title and description, with 3 searches, 2 models and 1 keyword. What was asked: 10 questions with answers set first, in 6 languages, translated by the author. A note says right answers were fixed before the run.

  • Documents: all 878 lessons on this site outside this chapter, each as its title and one-line description.
  • Questions: 10 questions I wrote before running anything, each the way a learner would ask it, not copying the lesson's title. For example: "If a payment request is sent twice, how do I make sure the customer is charged only once?"
  • Right answers: chosen before the run. Where the site has several lessons on one topic, like three on , any of them counts.
  • Languages: each question in English, and in my own translations into French, Spanish, Hindi, Japanese and Bengali.
  • Searches: nomic-embed-text and bge-m3 in all six languages, and plain keyword search in English only, to compare against.

An isometric row of four blocks joined by arrows showing the steps of the test: questions, 10 in 6 languages; Ollama, which embeds them; a cylinder for the index of 878 lessons; and the ranks, top 5 or not.

All ten questions, in English, with the answer set before the run:

#Question (English version)Right lesson

The Results

Paired bars per language of questions with the right lesson in the top 5, out of 10. nomic: English 4, French 2, Spanish 1, Hindi 1, Japanese 0, Bengali 1. bge-m3: English 6, French 5, Spanish 5, Hindi 4, Japanese 3, Bengali 4. Keyword search, English only: 4.

In English: keyword search found the right lesson in the top 5 for 4 of 10 questions. So did nomic-embed-text. bge-m3 found 6 of 10. With only 10 questions, a gap of two is too small to call; treat English as a tie.

In other languages: nomic-embed-text fell to 0 to 2 of 10. bge-m3 held at 3 to 5 of 10. Added up over the five languages, that is 5 of 50 against 21 of 50. That gap is the real result.

Two panels for the Japanese questions: nomic found 0 of 10 in the top 5, bge-m3 found 3 of 10. A note says this is the worst language for each model.

The most useful thing is not a single number. It is how steady bge-m3 was. The rate limiting and questions were ranked first or second in all six languages. The grid below shows each question's rank, language by language.

A grid of bge-m3 ranks, one row per question and one column per language, filled when in the top 5. slow website: 49, 43, 47, 38, 34, 50; failing service: 1, 1, 2, 1, 1, 1; new cache server: 24, 37, 37, 23, 47, 16; too many requests: 1, 1, 1, 1, 1, 1; table too big: 12, 14, 12, 15, 16, 17; mostly reads: 4, 16, 9, 31, 9, 71; hand off work: 18, 36, 20, 22, 24, 23; charged twice: 2, 2, 3, 2, 6, 2; username check: 5, 4, 5, 3, 4, 3; cache first: 2, 1, 3, 30, 8, 9.

Why Four Questions Were Never Found

Four questions were never found in the top 5, by any model, in any language: the slow website, the new cache server, the table that is too big, and handing work to another service without waiting. Look at the first two.

Two cards titled two misses, the text did not say it. The slow website question, next to the Latency lesson indexed as Time delay in system response - most basic performance metric. The new cache server question, next to Consistent Hashing indexed as makes adding or removing nodes cheap. A note says the next figure tests whether the text is really the cause.

"Why does my website feel slow even when the server is fast?" The right lesson is "". Its description is "Time delay in system response - most basic performance metric". It never says website, slow or feel.

"When I add a new cache server, how do I avoid moving almost every key?" The right lesson is "". Its description says "adding or removing nodes cheap", but never cache, server or key.

The words describing those two lessons do not say what a learner asks about. The next slide tests whether that is really the cause.

The other two are near misses. For "My table is too big for one machine. How do I split it across several?", in English both models ranked three partitioning lessons above "Database ". For "hand work to another service without waiting", bge-m3's first result in English was "Asynchronous Processing", not "Message Queues". Those are very similar topics, so they are sensible results, but they were not the answers I set before the run, so they count as misses.

A hand-drawn sketch titled similar lessons, not the right one, for the question my table is too big: arrows to Table Partitioning at rank 1, Vertical Partitioning at rank 2 and Data Partitioning at rank 3, and a dashed arrow to Database Sharding at rank 12. A note says bge-m3, English, sensible results but a miss.

Testing the Cause: Rewrite One Description

Was the missing answer the model's fault, or the text's? I tested it. Same 878 lessons, same bge-m3, same question in all six languages. I changed only one thing: the Latency lesson's indexed text.

  • Before: "Latency. Time delay in system response - most basic performance metric"
  • After: "Latency. Why a website or app feels slow: the time a request spends travelling, waiting and being processed before the user sees a response."

Paired bars of the Latency lesson's rank before and after rewriting only its description, for six languages. Before: English 49, French 43, Spanish 47, Hindi 38, Japanese 34, Bengali 50. After: rank 1 in all six. A note says the new text was written knowing the question, so it is the best case.

Before, the right lesson ranked between 34 and 50. After, it ranked first in all six languages.

One honest note: I wrote the new description knowing the question, so this is the best case. It proves the text was the cause. It does not prove any rewrite would work as well. The rule still holds: search is only as good as the text you put in the index. Describe each document in the words people will use to ask for it.

Keyword Search Still Works

In English, plain keyword search matched nomic-embed-text: 4 of 10 in the top 5. It found the and idempotency questions first, because the questions used the same words as those lessons.

Hand-drawn bars for English only, right lesson in the top 5: keyword 4 of 10, nomic 4 of 10, bge-m3 6 of 10. A note says keyword search can barely cross languages, and was not measured outside English.

But keyword search can barely cross languages. A question in Hindi shares almost no words with an English lesson: at most a borrowed word like "API". I did not measure keyword search outside English. That is where a multilingual model is needed.

Many real systems run both searches and merge the two lists. This is called hybrid search, and a later chapter of this course covers it.

Rank It Yourself

This box holds the real ranks from the big test, for bge-m3. Each list has one number per question, in the order of the table below. The number is the rank of the first right lesson. Press Run to count how many landed in the top 5.

Then change TOP = 5 to TOP = 1, or TOP = 10, and run it again.

When to Use It

A decision flowchart. Exact names, codes or IDs? If yes, keyword search, maybe both. If no: more than one language? If yes, multilingual semantic search; if no, semantic search. A note says then test with your own questions and known answers.

  • People ask in their own words, not your document's words. Use semantic search.
  • People ask in several languages. Use a multilingual model, and test it in each language.
  • People search for exact names, codes or IDs. Keyword search is often better. Consider both.
  • Before you trust any of it: write your own test questions with known answers, as this lesson did, and measure.

Three cards with logos: Ollama for both embedding models, Python for scoring and ranking, and VS Code where the example ran. A note says it is free, local and repeatable.

What This Lesson Measured, and What It Did Not

Two columns. Measured: two models and keyword search, 878 titles and descriptions, 10 questions in 6 languages. Not measured: searching full lesson text, other models, many more questions.

Measured: two models and keyword search, 878 lesson titles and descriptions, 10 questions in six languages.

Not measured: searching the full lesson text, other models, or many more questions. Ten questions is a small test, so a difference of one or two is not meaningful. The translations are mine.

What to Do Next

A hand-drawn list of four things to do: run the example then try USE_DESCRIPTIONS set to False, write rich text saying what each document answers, build a test set of questions with known answers, and test every language your users write.

  1. Run the small search, then set USE_DESCRIPTIONS = False and see it get worse.
  2. Write rich text for each document. Say what it answers, in the words people use.
  3. Build a small test set: questions with known right answers, before you tune anything.
  4. Test every language your users write in.

Numbers to keep: in the five non-English languages, bge-m3 found 21 of 50 in the top 5 and nomic 5 of 50. Rewriting one description moved Latency from rank 34 to 50 up to rank 1 in every language.

Knowledge Check

Knowledge Check

4 questions - Score 80% to pass

Q1

In semantic search, what happens once, ahead of time, and what happens for every question?

Q2

With titles alone, Rate Limiting won by only 0.005 in English and lost in Spanish. With a one-line description it won everywhere by about 0.2. Why?

Q3

The question about a slow website never found the Latency lesson. What was the cause?

Q4

In English, keyword search, nomic-embed-text and bge-m3 were close (4, 4 and 6 of 10). Why still use a multilingual embedding model?

1
Why does my website feel slow even when the server is fast?
2How do I stop one failing service from bringing down all the others?
3When I add a new cache server, how do I avoid moving almost every key?
4How can I stop one user from sending too many requests to my API?Rate Limiting
5My table is too big for one machine. How do I split it across several?Database
6Most of my database traffic is reads. How do I spread that load?Read Replicas
7How can one service hand work to another without waiting for it to finish?Message Queues
8If a payment request is sent twice, how do I make sure the customer is charged only once?
9How can I quickly check whether a username was probably never used, without storing every name?Bloom Filters
10My app should read from the cache first and only go to the database when the data is missing. What is this called?Cache-Aside Pattern

Here are the results, from the big test's own script, meaning_search.py. You do not need to run it; the small script above is the one for you.

A real terminal recording of meaning_search.py replaying its stored results: keyword search in English found 2 first and 4 in the top 5. In the top 5, nomic-embed-text found 4, 2, 1, 1, 0, 1 of 10 and bge-m3 6, 5, 5, 4, 3, 4 of 10, for English, French, Spanish, Hindi, Japanese and Bengali.