Hold a large colour photo of a mountain lake in one hand. In the other, hold a tiny black-and-white print of the same lake. The small one has far less detail, but you would never mistake it for a different place.
![]()
The last lesson made shorter by keeping fewer numbers. This lesson keeps every number, but stores each one with less detail. It is a different way to save space, and it loses far fewer right answers.

Bit. The smallest piece of computer memory. It is either 0 or 1.
Byte. 8 bits together. One byte can hold a whole number from -128 to 127 (when it keeps a sign, as here).
float32. The usual way a computer stores a number with decimals, like 0.0473. It takes 4 bytes. This is how Ollama gives you each number.
int8. A whole number stored in 1 byte. The "8" means 8 bits.
Binary. Keeping only 1 bit per number: is it above zero or not. That is the number's sign: plus if above zero, minus if below.
Scale factor. The number you multiply by to make other numbers bigger or smaller.
Rescore. Check a short list of results again, this time with the full numbers.
Ranked first. The right lesson was the very top result, a stricter test than top 5.
Noise. Small differences that happen by chance. With 470 questions, one question either way can change from rounding alone.
This file takes the five short texts from the last lesson and stores their three ways: 4 bytes for each number, 1 byte for each number and 1 bit for each number. Then it scores each one against the question. Here it is, open in my VS Code.

And this is what it printed:

All three picked the right text. To run it: install Ollama from ollama.com, open it and leave it running. Run ollama pull nomic-embed-text, save the file below as smaller_numbers.py, and run python3 smaller_numbers.py (on Windows: python smaller_numbers.py). If you see "Connection refused", open the Ollama app first.

1 byte (int8). numbers are small. For nomic here, they were all between about -0.25 and 0.25. To fit them into the whole numbers -127 to 127, multiply every number by one shared scale factor, then round. When you compare, divide by the same factor again, so the numbers are back on their old scale.

The factor is 127 divided by the largest size found in the stored pieces, ignoring the minus sign. That way the biggest number becomes exactly 127 or -127. For nomic here it was about 501. Rounding throws away only a tiny bit of each number.
1 bit (binary). Keep only whether each number is above zero. To compare a question with a piece, turn the question into signs too, and count the positions where the two agree. More agreement means a closer match.


The same test as lessons 6 and 8:
The question itself is always embedded at full detail. Only the stored pieces are made smaller. With 1 bit, the question also becomes signs, so both sides look the same.
Here is the lab's report, from the terminal.

As a check, 4 bytes should repeat lesson 8's full-size result, and it did: 441 and 439 in the top 5.


Counting only the answers that came first shows the same thing.

One bit alone lost answers. But bits are small, so comparing them takes very little arithmetic. That suggests a trick: use them for a rough first pass, then check only a short list carefully.

The 50 is a choice, not a rule. A longer list catches more but reads more full numbers. This lesson tried only 50.

It worked. Ranked first, it matched 4 bytes exactly: 339 and 356. In the top 5, nomic went from 405 to 435, and bge-m3 from 415 to 438.

There is a catch, and it matters. The second pass needs the full 4-byte numbers, so you still have to keep them somewhere. Rescoring does not save storage. What it saves is work: the first pass runs over small bits, and the full numbers are read for only 50 pieces per question. A system can keep the bits in memory (fast, but costly) and the full numbers on disk (slower, but cheap). This lesson did not measure the speed.

This is arithmetic, not a measurement: 1 million documents, times 768 numbers, times the size of each number.
How much faster each one makes a real database, this lesson did not measure.
This box holds the real ranks from the test for nomic-embed-text: for each of the 470 questions, which place the right lesson got in the list, for all four ways. Press Run. Then change WAY to "int8" or "binary_rescore", and TOP to 1.

Many vector databases (databases built to store and search them) can store 1-byte or 1-bit embeddings for you. How each one does it varies, so check its documentation, and measure again after you switch.


Measured: two models, four ways to store the numbers, 470 quiz questions over 1,823 pieces.
Not measured: speed inside a real database; other shortlist sizes than 50; other ways of making 1-byte numbers (this lesson used the simplest, one shared factor); and questions from real users. The quiz questions were written by the same author as the lessons, which probably makes them easier to match than real questions.

smaller_numbers.py and compare the three columns.
5 questions - Score 80% to pass
Storing each embedding number in 1 byte instead of 4 gave nomic 442 in the top 5, against 441. What does that tell you?
How does the 1-byte method in this lesson turn a number like 0.05 into one byte?
With 1 bit per number, how are a question and a piece compared?
Ranking by bits, then re-scoring the best 50 with the full numbers, gave nomic 435 in the top 5, close to 441. What is the catch?
Memory is tight and you want to store embeddings smaller. Based on this lesson, what should you try first?
# Store each embedding number in 4 bytes, 1 byte or 1 bit, and see whether search still finds the right text.
# Needs Ollama (ollama.com) running, and: ollama pull nomic-embed-text
import json, urllib.request
def embed(texts):
body = {"model": "nomic-embed-text", "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"]
texts = ["Rate limiting: cap how many requests each user may send per minute.",
"Caching: keep a copy of data close to where it is used.",
"Load balancing: spread incoming traffic across several servers.",
"Sharding: split one big table across many machines.",
"Message queues: let one service hand work to another later."]
question = "How can I stop one user from sending too many requests?"
docs = embed(["search_document: " + t for t in texts])
q = embed(["search_query: " + question])[0]
# 1 byte a number: scale every number into -127..127 with one shared factor, then round
scale = 127 / max(abs(x) for d in docs for x in d)
docs_int8 = [[round(x * scale) for x in d] for d in docs]
# 1 bit a number: keep only whether each number is above zero
bits = lambda v: [x > 0 for x in v]
q_bits = bits(q)
for t, d, d8 in zip(texts, docs, docs_int8):
full = sum(a * b for a, b in zip(q, d)) # length 1, so this is cosine
one_byte = sum(a * b / scale for a, b in zip(q, d8))
agree = sum(a == b for a, b in zip(q_bits, bits(d))) # positions with the same sign
print(f"{t.split(':')[0]:<15} 4 bytes {full:.3f} 1 byte {one_byte:.3f} 1 bit {agree}/{len(q)} agree")
Five texts prove nothing on their own. The real test comes next.