Each lesson in this chapter made one choice and measured it on its own. A real search system makes all of them at once. Do they still help when they are stacked? Does one undo another?

This lesson builds one search system, one choice at a time, and measures what each step adds and what it costs.

This file runs the chapter's steps on one short document: split it into pieces, embed them, store each number in 1 byte, search by meaning, then add keywords and combine the two rankings. With only four pieces there is nothing to gain from clusters, so that step is left out here; the real test below includes it. Here is the file, open in my VS Code.

And this is what it printed:

The meaning score is the cosine from earlier lessons: higher means closer in meaning. The combined place uses lesson 11's rule, adding 1 divided by (60 plus the place) from each ranking. Pieces 1, 3 and 4 share no words with the question, so they tie in the word ranking and all get the worse place, 4.
Piece 2, about requests per minute, came first by meaning and by shared words, so it came first combined too.

Notice piece 4: just "by card.", two words. Cutting every 12 words left a scrap at the end of the document, and it still scored 0.527, just ahead of piece 3. Lesson 6 showed that a fixed word count also splits sentences in the middle. Cutting at sentence breaks is a common fix, but neither lesson measured it.
To run it: install Ollama from ollama.com, open it and leave it running. Run ollama pull bge-m3, save the file below as whole_pipeline.py, and run python3 whole_pipeline.py (on Windows: python whole_pipeline.py). If you see "Connection refused", open the Ollama app first.
# Every step of this chapter in one small search: split, embed, store in 1 byte, search, add keywords.
# 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 dot(a, b): return sum(x * y for x, y in zip(a, b))
def words(t): return set(re.findall(r"\w+", t.lower()))
def place(scores): return [sum(s >= x for s in scores) for x in scores]
document = ("Our API returns JSON. Errors use standard HTTP codes. "
"Each key may send 100 requests per minute; going over returns HTTP 429. "
"Invoices are sent on the first day of each month and can be paid by card.")
question = "How many requests per minute am I allowed?"
# 1. split into pieces of 12 words
pieces = [" ".join(document.split()[i:i + 12]) for i in range(0, len(document.split()), 12)]
# 2. embed each piece
vectors = embed(pieces)
# 3. store each number in 1 byte
scale = 127 / max(abs(x) for v in vectors for x in v)
stored = [[round(x * scale) for x in v] for v in vectors]
# 4. search by meaning
q = embed([question])[0]
meaning = [dot(q, v) / scale for v in stored]
# 5. add keywords, then combine the two rankings
shared = [len(words(question) & words(p)) for p in pieces]
m, k = place(meaning), place(shared)
both = [1 / (60 + m[i]) + 1 / (60 + k[i]) for i in range(len(pieces))]
for i, p in enumerate(pieces):
print(f"piece {i + 1}: meaning {meaning[i]:.3f} shared words {shared[i]} combined place {place(both)[i]} {p[:22]}...")

Each step keeps everything before it. Step 5 is all five choices together.



Here is the lab's report, from the terminal.

As a check, the first three steps repeat their own lessons exactly:



Ranked first tells the same story: 286, 356, 356, 357, 376. Clusters even gained one here. We search only some clusters, so a lesson with no piece in them drops out, and with fewer lessons left the right one can move up. One question more or less does not matter.

Storage. Pieces multiplied it: 0.46 MB to 7.47 MB, because there are 1,823 pieces instead of 112 lessons. One byte per number brought it back down to 1.87 MB. The 32 cluster centres add a little, to 2.00 MB. Keyword search stores no . It needs its word index instead, and this lesson did not measure the size of that index.

Work. Pieces also multiplied the comparisons, 112 to 1,823. Searching 16 of 32 clusters brought it down to about 1,018. Keyword search adds its own work, reading the word index, which is not counted here.

That is the pattern of the whole chapter. Two choices bought right answers: pieces and keywords. Two choices saved space or work and lost almost no right answers: 1 byte and clusters.
Two honest footnotes, from comparing with lesson 11:
This box holds the real ranks from every step, for all 470 questions. A rank of 112, the last place, means the right lesson was not found at all, for example because none of its pieces was in the searched clusters. This counting is also how to measure your own system: for each question with a known answer, record the place of the right document, then count how many places are 5 or better. Press Run, then change TOP to 1.



Measured: one model, bge-m3; one order of steps; 470 quiz questions; storage of the and comparisons per question, counted.
Not measured: other orders of the steps, other settings for each step, time on a real database, the size of the keyword index, and questions from real users. Our quiz questions use the same words as their lessons, so keyword search does well here. Lesson 11 showed it can do worse when users choose different words.

whole_pipeline.py and change the piece size from 12 words.
This chapter started with a single token. It ends with a search system you can build on a laptop, and a way to measure every choice in it.
5 questions - Score 80% to pass
Which two steps raised the number of right lessons in the top 5 the most?
Storing each number in 1 byte kept 439 right lessons in the top 5. What did it buy?
Searching 16 of 32 clusters cut comparisons from 1,823 to about 1,018. What did it cost in the top 5?
Why did the first three steps give exactly the same numbers as lessons 6 and 9?
You are building your own search system. What should you do before adding any of these steps?
