A thick book has an index at the back. If you know the exact word, the index takes you straight to the page. If you only know the idea, and the book uses other words for it, the index is no help at all.

Search on a computer can work either way. Keyword search matches the words themselves. Meaning search, which this whole chapter has built, matches . Many real systems run both and combine the results. This lesson measures whether combining them actually helps.

Keyword search. Rank texts by the words they share with the question. No model, no .
BM25. The standard recipe for keyword search. "BM" stands for Best Matching, and 25 was its number in a series of recipes tried in the 1990s. It is explained on a later slide.
Meaning search. Rank texts by how close their embeddings are to the question's, as in lesson 4.
Hybrid search. Run both searches and combine their rankings.
Rank fusion. A simple way to combine two rankings into one. It is also explained below.
Place. A text's position in a ranking: 1 is the best. If two texts tie, both get the worse place.
Signal. Useful information. A ranking with no signal puts texts in an order that says nothing about the question.
Baseline. A simple method you always run first, so you can see whether anything fancier is really better.
This file scores four short texts against three questions, by shared words and by meaning, and then combines the two rankings. Here it is, open in my VS Code.

And this is what it printed:


Look at the three questions one by one.

The example counted shared words. Real keyword search uses a better recipe, BM25:

In a large collection the first rule deals with words like "one" and "how": they appear in many texts, so they count for very little. In the tiny four-text example each of them appears in only one text, so even BM25 could not tell them apart from a meaningful word.
The two searches give numbers of very different sizes. In the quiz test below, the top BM25 score for a question was usually between about 13 and 53, while the cosine scores in this chapter sit between 0 and 1. You cannot simply add them. Reciprocal rank fusion avoids the problem by using only each text's place. "Reciprocal" just means 1 divided by a number:

For each text, take 1 divided by (60 plus its keyword place), add 1 divided by (60 plus its meaning place), and sort the texts by that sum. A text near the top of either list rises. A text near the top of both rises most. The 60 comes from the 2009 paper that named the method, by Cormack, Clarke and Buettcher. Because of the 60, being first in one list gives only a small extra push over being second or third. This lesson did not tune it.


Test 1: the quiz questions. The same setup as lessons 6 to 10: 470 quiz questions against 1,823 pieces of 112 lessons, with both models from this chapter, nomic-embed-text and bge-m3. The quiz questions were written by the same author as the lessons, so they often reuse the lessons' own words.
Test 2: other words. Lesson 4's 10 questions. I wrote them before running any test, and they do not use the words in the lesson titles. Each is asked in English, French, Spanish, Hindi, Japanese and Bengali. That is 60 searches against the title and description of 878 lessons, exactly as in lesson 4, with bge-m3, which reads all six languages. The lesson texts are in English. For keyword search, a word is any run of letters in any alphabet. Japanese is written without spaces, so a whole phrase counts as one word there, which makes keyword search weak in Japanese by nature. The two tests search different collections, so compare the three methods within a test, not across tests.
Both tests rank lessons by keyword search (BM25), by meaning search, and by both combined. If the right lesson ties with others, it gets the worst place in that tie.
Here is the lab's report, from the terminal.


On the quiz questions, the simplest search won. Keyword search, with no model at all, found the right lesson in the top 5 for 453 of 470. Meaning search found 441 with nomic-embed-text and 439 with bge-m3.

This is not a failure of . The quiz questions share many words with their lessons, and that is exactly the case keyword search is built for. A later chapter of this course, on search for AI answers, found the same thing with chunks of many sizes.
Combining the two helped meaning search a lot:


Question by question, fusion found the right lesson for 17 questions that bge-m3 alone missed, and lost only 1. But against keyword search alone, the gain was tiny: 455 against 453 in the top 5, and 379 against 378 first.

Now the questions use other words, and five of the six languages are not English. The lesson texts are all in English.
Look at the two groups of languages separately.

This box holds the real ranks from both tests, for bge-m3. In the second test, 878 means keyword search matched no word at all, so every lesson tied and the right one got the last place. Press Run to see the quiz test. Then change TEST to "WORDED" to see the other-words test, and TOP to 1.



Measured: keyword, meaning and fused search on 470 quiz questions and on 60 own-words searches in six languages; two models; one fusion constant, 60.
Not measured: weighted mixes that trust one list more than the other, other keyword recipes, and questions from real users. Only 60 searches in the second test, so its numbers can move by a few with other questions. Real users will likely do both: some use your words and some do not.

keyword_and_meaning.py and read the scores for each question.
5 questions - Score 80% to pass
On the 470 quiz questions, keyword search alone found the right lesson in the top 5 for 453, and bge-m3 meaning search for 439. What is the best explanation?
How does reciprocal rank fusion combine a keyword ranking and a meaning ranking?
In English, French and Spanish, meaning search found 16 and both combined found 11. In Hindi, Japanese and Bengali, both found exactly what meaning found. Why the difference?
In the small example, why did the sharding text share a word with the question about flooding an API?
Your users type exact product codes like "XK-240" that appear in your documents. What does this lesson suggest?
To run it: install Ollama from ollama.com, open it and leave it running. Run ollama pull bge-m3, save the file below as keyword_and_meaning.py, and run python3 keyword_and_meaning.py (on Windows: python keyword_and_meaning.py). If you see "Connection refused", open the Ollama app first.
# Score the same texts by shared words and by meaning, then combine the two rankings.
# Needs Ollama (ollama.com) running, and: ollama pull bge-m3
import json, re, urllib.request
def embed(texts):
body = {"model": "bge-m3", "input": texts}
req = urllib.request.Request("http://localhost:11434/api/embed", data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"})
return json.loads(urllib.request.urlopen(req).read())["embeddings"]
def words(t): return set(re.findall(r"[a-z0-9]+", t.lower()))
def dot(a, b): return sum(x * y for x, y in zip(a, b))
def place(scores): # 1 = best; a tie shares the worse place
return [sum(s >= x for s in scores) for x in scores]
names = ["rate limiting", "HTTP 429", "caching", "sharding"]
texts = ["Rate limiting caps how many requests each user may send.",
"HTTP 429 means Too Many Requests: slow down and retry later.",
"Caching keeps a copy of data close to where it is used.",
"Sharding splits one big table across many machines."]
docs = embed(texts)
for question in ["What does status 429 mean?",
"How can I stop one customer from flooding my API?",
"¿Cómo evito que un cliente sature mi API?"]:
q = embed([question])[0]
shared = [len(words(question) & words(t)) for t in texts]
meaning = [dot(q, d) for d in docs]
kw, mn = place(shared), place(meaning)
both = [1 / (60 + kw[i]) + 1 / (60 + mn[i]) for i in range(len(texts))]
print(question)
for i, n in enumerate(names):
print(f" {n:<14} shared words {shared[i]} meaning {meaning[i]:.3f} combined place {place(both)[i]}")
Rank fusion gives both lists an equal vote. An empty keyword list is harmless. A keyword list that confidently points at the wrong lessons does the damage.