Tokens And Embeddings

Splitting Text Into Pieces: Why Search Works Better on Small Chunks

0 of 16 complete

0%

Contents

Back|Tokens And EmbeddingsSplitting Text Into Pieces: Why Search Works Better on Small Chunks
1/16
34 min left
Prerequisites
The Silent Cut: How Much of Your Text an Embedding Model Really Readsrequired
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 & Storage
1 of 16

A Report, or a Box of Cards

Imagine a 40-page report. Someone asks you one small question about it. You can hand them the whole report, or you can hand them the one card from a card box that holds exactly that answer.

An illustration of an engineer at a desk cutting a long report into blank index cards with a paper trimmer and filing them into a card box with dividers. A line says a small question is easier to match against a small card than against the whole report.

Search systems face the same choice. The last lesson showed two problems with long text: a model may cut it off, and even when it reads everything, one sentence counts for very little in one long .

The usual answer is to cut each document into small pieces before embedding, one card per idea. In this lesson I measure whether that really helps, on this course's own lessons.

The Words You Need First

If a word below is new, read its line. Embedding, and search come from earlier lessons in this chapter.

A hand-drawn word list. Chunk or piece: a short part of a document. Chunking: cutting documents into pieces. Chunk size: how long each piece is, 200 words here. Best piece: a document scores by its closest piece. Index: the stored embeddings a search compares with. Ranked first: the right lesson was the top result. A note says embedding and cosine similarity are from earlier lessons.

Chunk (or piece). A short part of a document, embedded and searched on its own.

Chunking. Cutting documents into chunks before them.

Chunk size. How long each piece is. Here, 200 words.

Best piece. When a document is split, its score for a question is the score of its closest piece.

Ranked first. The right lesson was the top result.

Two Ways to Index the Same Text

An index is the stored list of that a search compares a question with. There are two ways to build one from the same lessons.

Two grids of squares, one square per embedding: 112 for whole lessons, one per lesson, and many more small squares for 200-word pieces, the first 448 of 1,823 shown. A note says pieces cost more to store.

Whole: one embedding per lesson. A question is compared with 112 embeddings, one per lesson.

Pieces: each lesson is cut into 200-word pieces, and each piece gets its own embedding. The 112 lessons became 1,823 pieces. A question is compared with all 1,823, and each lesson is scored by its best piece.

A sequence diagram: a question is scored against every one of 1,823 pieces, each of the 112 lessons keeps its best piece's score, the lessons are sorted, and the top result comes back. A note says you can also return the best piece itself.

The pieces cost more to store: 1,823 lists of numbers instead of 112. The question is whether they find more.

A Small Example You Can Run

Here is the idea on one short document about three things: response format, rate limits and billing. This is the real file, open in my VS Code.

A real screenshot of split_and_search.py open in VS Code: a short document about response format, rate limits and billing, cut into pieces of 25 words, with the question how many requests per minute can I send, scored against the whole document and each piece with bge-m3.

And this is what it printed for the question "How many requests per minute can I send?"

A real screenshot of VS Code's terminal after running python split_and_search.py: the whole document scored 0.654; pieces 1 to 4 scored 0.484, 0.697, 0.570, 0.592. Piece 2, about rate limits, starting with the words what went wrong, scored highest.

The whole document scored 0.654. Piece 2, the one about rate limits, scored 0.697. The other pieces, mostly about the response format and billing, scored lower. The piece is about the answer, so it matches better than the whole.

Look closely at piece 2: it starts with "what went wrong." That is the end of the sentence before. Cutting at a fixed number of words splits sentences in the middle. Try SIZE = 10 and SIZE = 80 and watch what changes.

To run it: install Ollama from ollama.com, open it and leave it running. Run ollama pull bge-m3 in a terminal (about 1.2 GB), save the file below as split_and_search.py, and run python3 split_and_search.py (on Windows: python split_and_search.py). You need only Python 3; the "(venv)" in my screenshot is just my own setup, so it says python. If you see "Connection refused", open the Ollama app first.

The Real Test

Two panels. Searched: 112 lessons with quizzes removed, with bge-m3 reading up to 8,192 tokens. Asked: 470 of the lessons' own quiz questions, the right answer being the lesson each came from. A note says the median lesson is 3,295 words.

  • Documents: the 112 lessons of this AI course, outside this chapter, with their quizzes removed.
  • Questions: the 470 quiz questions inside those lessons, asked exactly as written. The right answer is the lesson each question came from. Because quizzes were removed from the searched text, a question can never find itself.
  • Model: bge-m3, with its reading limit raised to 8,192 tokens (the Ollama settings num_ctx and num_batch, from the last lesson) so it can read long lessons. Half the lessons are shorter than 3,295 words and half are longer.
  • Two indexes: one of whole lessons, and one of 200-word pieces.

An isometric row of four blocks joined by arrows showing the steps of the pieces test: 112 lessons, a cutter making 200-word pieces, a cylinder of 1,823 pieces, and 470 questions asked.

Here are the results, from the terminal.

A real terminal recording of chunk_find.py's report: 112 lessons, 470 questions, 1,823 pieces of 200 words, median lesson 3,295 words. Every lesson was read in full, the longest at 7,241 tokens. Whole lessons: 286 first, 401 in the top 5. Pieces: 356 first, 439 in the top 5. Split by lesson length, pieces helped both shorter and longer lessons. Last, the misses with pieces by chapter: evals 19 of 166, every other chapter 4 or fewer.

Pieces Found More

Paired bars for 470 questions. Ranked first: whole lessons 286, pieces 356. In the top 5: whole 401, pieces 439.

With whole lessons, the right lesson came first for 286 of 470 questions, and was in the top 5 for 401.

With 200-word pieces, it came first for 356, and was in the top 5 for 439.

That is 70 more questions answered by the first result, from the same text, the same model and the same questions. The only change is how the text was cut.

Two panels of questions ranked first out of 470: 286 with one embedding per lesson and 356 with 200-word pieces. A note says 70 more answers first.

Not Only a Fix for Long Text

You might think pieces only help because long lessons get cut off. The test says otherwise. I split the questions by the length of the lesson they belong to.

Paired bars of questions in the top 5, split at the median lesson length of 3,295 words. Shorter lessons: whole 194, pieces 217 of 240. Longer lessons: whole 207, pieces 222 of 230.

  • Shorter lessons, under 3,295 words: top 5 went from 194 of 240 to 217.
  • Longer lessons: from 207 of 230 to 222.

Pieces helped both. And nothing was cut off: with bge-m3 reading up to 8,192 tokens, every lesson fit, the longest at 7,241 tokens (about 1.4 tokens per word). So the whole gain comes from how the text was embedded, not from long text being cut. A quiz question is about one idea, and a 200-word piece is about one idea. A whole lesson is about many ideas at once, and its one stands for all of them.

Pieces Did Not Win Every Question

Looked at question by question, ranked first:

Three boxes, question by question, ranked first: 255 first both ways, 101 first only with pieces, 31 first only with whole lessons; the other 83 were not first either way. A note says some questions are about a whole lesson's topic, so test both.

  • 255 questions were ranked first both ways.
  • 101 were ranked first only with pieces.
  • 31 were ranked first only with whole lessons.
  • The other 83 were not ranked first either way.

So pieces are better overall, but not always. Some questions are about what a whole lesson is about, and the whole-lesson matches that better than any single piece.

A hand-drawn sketch: the question requests per minute has a dashed arrow to the whole document, covering format, limits and billing, scored 0.654, and a solid arrow to the piece about limits, scored 0.697. The Ollama logo sits below.

Where Pieces Still Missed

With pieces, 31 of the 470 questions still did not have their lesson in the top 5. They were not spread evenly across the course.

A bar chart of the percent of questions whose lesson was not in the top 5 with pieces, by chapter of the AI course: evals 19 of 166, about 11%; agents 7%; llm ops 5%; security 4%; core 3%; rag and data engineering 2%.

The Evals chapter has 36 lessons, a third of the course, and 166 of the questions. Pieces missed 19 of them, 11%. Every other chapter missed between 2% and 7%.

The Evals lessons are all about one subject, measuring AI systems, from many close angles. My guess is that a question about one of them often finds a very similar lesson first. But this test did not record which lesson came first on a miss, so that is a guess, not a measurement.

This is true in both cases: when many documents are about nearly the same thing, telling them apart is harder, whatever the chunk size.

What Pieces Cost

Pieces are not free. Each from bge-m3 is 1,024 numbers, and each number takes 4 bytes to store.

Two bars of storage for the embeddings alone with bge-m3 at 4 bytes a number: whole lessons, 112 embeddings, 0.46 MB; 200-word pieces, 1,823 embeddings, 7.5 MB. A note says this is tiny for one course but a real cost at millions of documents.

  • Whole lessons: 112 embeddings, about 0.46 MB.
  • 200-word pieces: 1,823 embeddings, about 7.5 MB, 16 times as much.

Each search also compares the question with 16 times as many embeddings. For this course that is still tiny. For millions of documents, it becomes a real cost in storage and search time, and later lessons in the course cover how large systems handle it.

You also need to keep the text of each piece, so that you can show it or pass it to a language model.

How Big Should a Piece Be?

This lesson used one size, 200 words, and did not test others. Size is a real trade-off:

Three hand-drawn boxes on the size trade-off, not tested in this lesson: too small, It returns 429, with no context; too big, 5 ideas at once, blurred; cut badly, went wrong, half a sentence. A note points to Retrieval and RAG in Production for measured sizes.

  • Too small: a piece loses the words around it. "It returns 429" (429 is an error code) means little without the sentence before it.
  • Too big: a piece covers several ideas, and we are back to the whole-document problem.
  • Cut in the middle: fixed word counts split sentences, as in the small example.

The retrieval chapter of this course, Retrieval and RAG in Production (RAG means search plus a language model), measures eight sizes on this site's lessons, with a different model. Its lesson Chunk Size, Measured found that embedding search did best with small pieces and got steadily worse as the pieces grew. It also found that a plain keyword search, with no model at all, scored higher at most sizes.

Count It Yourself

This box holds the real ranks from the test: for each of the 470 questions, where the right lesson came, whole and with pieces. Press Run.

Then change TOP = 1 to TOP = 5, or TOP = 3.

When to Split, and When Not To

A flowchart: are documents longer than a few paragraphs? If no, keep them whole; if yes, split them into pieces. Either way, test both on your own questions. A note says splitting won overall here, but not every question.

  • Split when documents are longer than a few paragraphs, or longer than the model reads.
  • Split when people ask about one detail at a time, which is most questions.
  • Keep whole when documents are short, like a product title with one line of description, as in lesson 4.
  • Test both on your own questions, as this lesson did. The best choice depends on your text.

Two cards with logos: Ollama running bge-m3, and Python cutting, scoring and ranking. A note says it is free and local.

What This Lesson Measured, and What It Did Not

Two columns. Measured: one model, bge-m3; one piece size, 200 words; 470 of the lessons' own quiz questions. Not measured: other sizes or overlap, other models, questions from real users.

Measured: one model, one piece size of 200 words, 112 lessons and 470 of their own quiz questions.

Not measured: other piece sizes, pieces that overlap or follow sentence breaks, other models, or questions written by real users. The quiz questions were written by the same author as the lessons, often in the same words. That probably makes this search easier than it would be with real users' questions, but this test did not measure how much.

What to Do Next

A hand-drawn list of four things to do: run the example with SIZE set to 10 and 80, split long documents into pieces, score each document by its closest piece, and measure whole against pieces on your own questions.

  1. Run the small example and try different sizes.
  2. Split long documents into pieces before .
  3. Score each document by its best piece, or return the piece itself.
  4. Measure on your own questions, whole against pieces.

The number to keep: questions ranked first out of 470 went from 286 with whole lessons to 356 with pieces, with the same model and text; only the cutting changed.

Knowledge Check

Knowledge Check

4 questions - Score 80% to pass

Q1

With whole lessons the right lesson came first for 286 of 470 questions; with 200-word pieces, 356. What changed between the two?

Q2

Pieces also helped on the shorter lessons, which bge-m3 read in full. Why?

Q3

31 questions were ranked first only with whole lessons. What does that tell you?

Q4

In the small example, piece 2 started with 'what went wrong.' What problem does that show?

# Cut a long text into pieces, embed each piece, and see which piece answers a question.
# 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))

# a short "document" about three different things
document = (
    "Our API returns results in JSON. Every response has a status field and a data field. "
    "Errors use standard HTTP codes, and the body explains what went wrong. "
    "Rate limits: each key may send 100 requests per minute. Going over returns HTTP 429, "
    "and the Retry-After header says how many seconds to wait. "
    "Billing: invoices are sent on the first day of each month, in US dollars, "
    "and can be paid by card or bank transfer within 30 days."
)
question = "How many requests per minute can I send?"

words = document.split()
SIZE = 25   # words per piece; try 10 or 80
pieces = [" ".join(words[i:i + SIZE]) for i in range(0, len(words), SIZE)]

q, whole = embed([question, document])
print(f"whole document, 1 piece: {cosine(q, whole):.3f}")
for i, (text, v) in enumerate(zip(pieces, embed(pieces)), 1):
    print(f"piece {i} of {len(pieces)}:        {cosine(q, v):.3f}  {text[:48]}...")