xavier-ramirez.com
← Your own AI Overview: the spine

Your own AI Overview · 02

Your own AI Overview: chunking

Split the document wrong and you lose the answer.

Before anything gets retrieved, the document is chopped into passages. That chop is the most under-rated decision in retrieval: a chunk is the smallest thing search can hand back, so if the sentence that says “premium increased 18% following claim 88431” lands half in one chunk and half in the next, neither chunk answers the question. Here you drive the chunker yourself over a real underwriting note and watch the answer survive — or get cut.

01 · Spine02 · Chunking03 · Retrieve & rerank04 · Access control05 · Grounding & citations06 · Evals & guardrails

↳ You are here — the split that decides what’s findable. Post 01 built the whole spine; this post deepens the chunking stage.

Worked exampleThis series uses property & casualty (P&C) insurance as its running example — policies, claims and underwriting notes. The pipeline itself is domain-agnostic; the data just happens to be an insurer’s.

The split

The chunk you keep is the answer you can find

Drag the two knobs and watch the same underwriting note re-chop live. The question only gets answered when its evidence lands inside a single chunk.

chunk the document

↳ Small chunks fracture the fact; big chunks bury it. Overlap is what saves a fact sitting on a boundary.

The answer, whole in chunk 1

Following claim 88431, the account was re-tiered and the annual premium increased 18% at renewal, reflecting the water-loss history and the absence of a leak-detection device.

This split keeps the whole answer sentence inside one chunk, so a search for “why did POL-55012 go up?” can return the reason in full. That is a retrieval hit.

Why overlap earns its storage

Set overlap to 0 and shrink the chunk size until a boundary lands inside the answer sentence — the verdict flips to split. Now nudge overlap up: the same fact reappears whole in the next chunk, because neighbours share their edges. That shared margin is cheap insurance against a fact falling on a seam.

Two ways to cut

Cut by character count, or cut where the meaning changes

The baseline counts characters. The upgrade cuts on the document’s own structure — and that changes what each chunk is about.

Naive · fixed-size window
end of coveragestart of premium reason
chunk 2 = two topics
chunk 3premium reason + roof note
chunk 4recommendation + billing

A single window straddles two unrelated topics — the match for “why did premium rise” is diluted by coverage text it happened to include.

Structure-aware · cut on seams
loss historyclaim 88431 · water damage
rating decisionpremium +18% · the reason
recommendationleak-detection discount

Each chunk is about one thing. The “rating decision” chunk is the reason and nothing else — easy to retrieve, easy to cite.

Both strategies split the exact same document. The naive window is blind to meaning, so some chunks mix topics and dilute the match. Cutting on the document’s own structure gives you chunks that are each about one thing — easier to retrieve and easier to cite. The spine ships only the naive splitter today; structure-aware is the upgrade this post argues for, not shipped code.

What rides along

A chunk carries its document’s guardrails, not just its words

When you cut a document into chunks, each piece has to keep the labels that say who can see it and where it came from — or you’ve quietly broken security and citations.

That’s why the real splitter copies scope, doc_type, source_id, acl_roles and updated_at onto every chunk verbatim — it isn’t incidental, it’s what lets the retrieval filter run in the query and every answer cite its source. Chunking preserves the guardrails; it doesn’t just cut the text. That thread — permissions enforced at retrieval — is what Post 04 pulls on.

Run it yourself · own-overview @ v2-chunking

This isn’t pseudocode. The spine ships NaiveChunker: a fixed-size character window with overlap that copies each document’s isolation and ACL metadata onto every chunk, so the rest of the pipeline keeps working. Check out the tag and read the whole thing — it’s about 40 lines.

terminal
git clone https://github.com/xaviramirezcom/open-ai-overview
cd open-ai-overview
git checkout v2-chunking

# local, zero-cloud mode — no keys, no AWS. Split the sample note and print
# each chunk's char range plus which chunk holds the answer sentence.
uv run python -m own_overview.demo.chunking \
    --doc samples/pol-55012.txt --size 800 --overlap 100

The whole chunker is the fixed window you just drove, plus the metadata copy that keeps each chunk filterable and citable:

pipeline/chunk.py
_DEFAULT_SIZE = 800
_DEFAULT_OVERLAP = 100


class NaiveChunker:
    """Fixed-size character window with overlap. Implements the Chunker Protocol."""

    def __init__(self, chunk_size: int = _DEFAULT_SIZE, overlap: int = _DEFAULT_OVERLAP) -> None:
        if chunk_size <= 0:
            raise ValueError("chunk_size must be positive")
        if not 0 <= overlap < chunk_size:
            raise ValueError("overlap must be in [0, chunk_size)")
        self.chunk_size = chunk_size
        self.overlap = overlap

    def split(self, doc: Document) -> list[Chunk]:
        text = doc.text or ""
        if not text.strip():
            return []

        step = self.chunk_size - self.overlap
        chunks: list[Chunk] = []
        start = 0
        idx = 0
        while start < len(text):
            piece = text[start : start + self.chunk_size]
            chunks.append(
                Chunk(
                    chunk_id=f"{doc.doc_id}#{idx}",
                    doc_id=doc.doc_id,
                    # Isolation + ACL metadata copied verbatim so the retrieval
                    # filter and citations keep working on the chunk.
                    scope=doc.scope,
                    doc_type=doc.doc_type,
                    source_id=doc.source_id,
                    text=piece,
                    acl_roles=doc.acl_roles,
                    updated_at=doc.updated_at,
                    metadata=dict(doc.metadata),
                )
            )
            idx += 1
            start += step
        return chunks

It implements one small Chunker Protocol, so swapping in a structure-aware splitter later is a one-line change in the wiring — nothing else in the pipeline moves:

pipeline/contracts.py
@runtime_checkable
class Chunker(Protocol):
    """Splits a Document into retrievable Chunks. The naive splitter ships in
    the spine; structure-aware splitting is its own deep-dive post."""

    def split(self, doc: Document) -> list[Chunk]: ...

Teaching-grade reference implementation, not a production insurance product. Only the naive splitter ships at this tag; structure-aware chunking is the argued next step, not shipped code. Bring your own data and keys. MIT-licensed. The demo command splits the sample note locally and prints which chunk holds the answer — illustrative, and it runs.

Explain it back

A teammate says: “Chunking is just preprocessing — we’ll tune it later once retrieval and the model are good.” In one or two sentences, why is that backwards?

Reveal a model answer

Chunking isn’t downstream of retrieval — it defines what retrieval can return, because a chunk is the smallest unit search can hand back. If the split tore the answer across a boundary, it’s already gone: no reranker, bigger model or better prompt can retrieve a passage that was never stored whole. Chunking is the first thing to get right, not the last — a retrieval decision, not preprocessing.

Next in the series · 03
Retrieve & rerank: dense recall finds candidates; reranking decides the answer

Now the document is split into clean, self-contained, permission-carrying chunks. Next we go find them: turn the question into a vector, pull the closest chunks out of the store, then rerank so the passage that actually answers the question rises to the top of a small context budget. Good chunks make retrieval possible — the next post makes it precise.

Continue to Retrieve & rerank →