How to give a language model access to a hundred thousand documents
without ever putting a hundred thousand documents in the prompt.
WORK-ALONG SESSION · Lesson 01 theory, Lesson 02 in code YOU NEED · Python 3.10+, an editor, one OpenAI API key COST · Under ten cents of embeddings EIGHT LIVE DEMOS · every claim on these slides is executed, not asserted
00 · How this session runs
Two windows, eight demos
Slides on one side, a terminal on the other. Whenever you see an orange
bar, I stop talking and run the command in it. The output on the slide is
the real output from that command.
Demo 00./run.sh check
Follow along
One entry point for everything: ./run.sh setup once, then
./run.sh slides for this deck and ./run.sh demo 5
for any demo. demo/ingestion_pipeline.py is the finished
file we build up to.
Part one
Why RAG exists
The problem is not that models are stupid. The problem is that they are small.
01 · The problem
Several hundred internal documents
Policy guidelines. Technical specs. Customer support logs. Contracts.
Someone asks a question that one of those documents answers.
The obvious move is to paste all of them into the model and ask.
That does not work — and the reason it does not work is the
whole reason this lesson exists.
01 · Definition
Retrieval Augmented Generation
A language model combined with a retrieval system. The retrieval
system searches external sources — documents, databases, knowledge bases — and
pulls only the relevant pieces into the prompt, at the moment
the model needs them.
The model does not get everything. It gets the right few pages.
That is the entire idea. Everything else in this session is mechanics.
02 · Tokens
Models do not read words
A token is the unit of text a model processes. Sometimes a whole
word. Often a fragment of one. Models do not read letters and they do not read words.
Demo 01python 01_tokens.py
text : 'Retrieval augmented generation is powerful'
words : 5
tokens : 7
split as : Ret | rie | val | _augmented | _generation | _is | _powerful
One token is roughly three quarters of an English word. Common words are
one token; rare or long words split into several. Spaces travel with the token that follows.
02 · The scale gap
Read the scale carefully
How much text, in tokens — logarithmic scale
One chunk ███ 1,000
These 5 articles ████████ 72,169
A frontier model window ███████████ 1,000,000
The largest advertised ██████████████ 10,000,000
A mid sized company, 1 TB █████████████████████████ 250,000,000,000
An enterprise archive, 1 PB ██████████████████████████████████ 250,000,000,000,000
Each step to the right is ten times larger, not one step larger.
The gap between a model window and an enterprise archive is not a gap you close by waiting
for bigger models.
02 · The second reason
You also pay per token
Even when everything does fit, sending it all is the wrong move.
Send everything
Expensive — you pay for every token
Slow — latency scales with input
Worse answers — the signal is buried
Send the right five paragraphs
Cheap
Fast
Better answers
The point
Sending 500,000 tokens of irrelevant context to answer one question is expensive,
slow, and produces worse answers than sending the five right paragraphs.
Part two
The shape of the system
Two pipelines, not one. Learn them separately and the rest is easy.
03 · The whole system in one picture
Two pipelines
Pipeline 01 · INGESTION · runs once, before any question
Sourcedocuments
→
Chunkingcut to size
→
Embeddingmodel
→
Vectorslists of numbers
→
Vectordatabase
Pipeline 02 · RETRIEVAL · runs on every question
Userquestion
→
Embeddingmodel
→
Retrieverranks by closeness
→
Top matchingchunks
→
LLM writesthe answer
Not a drawing shortcut
The same embedding model appears in both rows. That is the single most
important detail on this slide, and we will break it on purpose at the end to show you why.
04 · Chunking
A chunk is a slice, not a section
Chunking is breaking large documents into small pieces. You choose the size.
Set it to 1,000 tokens and 10 million tokens of documents becomes 10,000 chunks.
The word "chunk" is doing work here
Chunks are cut by size, not by meaning. They do not respect chapters,
headings or arguments. A naive splitter will cut through the middle of a sentence
or a table without hesitating.
Why cut at all? Because nobody wants an entire Wikipedia article returned
because one sentence in it matched. The chunk is your unit of retrieval —
it is the smallest thing the system can hand back.
04 · The embedding model
This is not an LLM
Different model, different job. It does not generate text.
Text goes in; a list of numbers comes out.
Demo 02python 02_embedding_shape.py
one word 1 words in ->1536 numbers out
one sentence 8 words in ->1536 numbers out
a paragraph 337 words in ->1536 numbers out
[ -0.0616 +0.0554 +0.0004 +0.0124 +0.0016 -0.0123 ... ]
Fixed length output. One word in or nine hundred words in,
the vector that comes out is always the same length.
Every group is tighter inside than out. That is a
neighbourhood — and nobody labelled any of it.
07 · Where the vectors live
The vector database stores both
ID
Vector
Original text
Source
0001
[0.021, -0.884, ...]
Refunds are issued within 30 days.
policy.pdf
0002
[-0.412, 0.663, ...]
Q1 revenue reached 4.2 million.
sales.xlsx
0003
[0.907, 0.115, ...]
Guest wifi resets every Monday.
itwiki.md
One row per chunk
The vector is how it gets found. The original text is what gets
used. Store only the numbers and you have nothing to send the model.
Purpose built: Pinecone, Weaviate, Chroma, Qdrant.
FAISS is a library rather than a service. Or add pgvector to a Postgres you already run.
Part three
Retrieval, in principle
Ingestion is finished. Nothing above runs again until your documents change.
08 · Retrieval, step by step
The question takes the same road
The question goes through the same embedding model the documents went through.
It comes out as a vector of the same length.
The retriever compares that one vector against every stored vector.
It ranks them by closeness and returns the top k.
THE QUESTION "What were our sales in the first quarter?"
↓
[-0.398, 0.671, 1.194, ...] same model, same dimensions0.91 Q1 revenue reached 4.2 million. ┐0.88 Quarterly sales by region, Jan to Mar. ├ top 3 to the LLM0.84 Revenue targets for the first quarter. ┘0.42 Annual headcount summary.0.31 Refunds are issued within 30 days.0.19 Guest wifi resets every Monday.
Part four
Build the ingestion pipeline
Five articles in. A searchable vector database out. About sixty lines of Python and one API key.
02 · The API key
One secret, in one place
OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxx
Two things to do nowAdd credit. The API is prepaid and separate from any ChatGPT
subscription. Minimum top-up is around $5. Embedding these five documents costs
well under one cent.
Add .env to .gitignore. Do it before your
first commit, not after. A leaked key gets scraped from public repositories
within minutes.
05 · Step one
Load the files
def load_documents(docs_path):
# Fail loudly and early rather than silently loading nothing
folder = Path(docs_path)
if not folder.is_dir():
raise FileNotFoundError(f"Directory not found: {docs_path}")
documents = [
Document(
page_content=path.read_text(encoding="utf-8"),
metadata={"source": f"{docs_path}/{path.name}"},
)
for path in sorted(folder.glob("*.txt")) # sorted = reproducible
]
if len(documents) == 0:
raise ValueError(f"No .txt files found in {docs_path}")
return documents
Demo 04python 04_load.py
06 · Step two
Chunk them
def split_documents(documents, chunk_size=800, chunk_overlap=0):
splitter = CharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
separator="\n\n", # prefer to break at paragraph boundaries
)
return splitter.split_documents(documents)
Demo 05python 05_chunk.py
Note the unitchunk_size=800 means 800 characters, not 800 tokens.
Roughly 200 tokens. Different splitters count differently — always check which unit you are in.
06 · Overlap
What chunk_overlap actually does
chunk_overlap = 0
[0] Tesla was incorporated in July 2003 by Martin Eberhard and Marc
Tarpenning as Tesla Motors. Its name is a tribute to the
[1] inventor Nikola Tesla. In February 2004 Elon Musk led Tesla's...
chunk_overlap = 40
[0] Tesla was incorporated in July 2003 by Martin Eberhard and Marc
Tarpenning as Tesla Motors. Its name is a tribute to the
[1] Motors. Its name is a tribute to the inventor Nikola Tesla. In...
With overlap at zero, a sentence that straddles the boundary ends up
half in one chunk and half in another — and neither half matches the question well.
Rule of thumb: 10–20% of chunk size. For 800 characters, 100 is a
reasonable default.
06 · Interactive
Try it: watch the chunks re-cut
Chunking playgroundoverlap is highlighted in each chunk
This is the step that costs money and takes time
Every chunk is a call to OpenAI. Expect thirty seconds to a couple of minutes.
07 · The three arguments
The three arguments that matter
Argument
Why it matters
embedding
text-embedding-3-small, 1,536 dimensions.
Write that choice down. Next lesson you must embed the user's question
with exactly the same model, or retrieval returns nonsense with no error message.
persist_directory
Where the database lives on disk. Leave it out and Chroma runs
in memory only — everything disappears when the script ends and you
pay to embed all over again.
collection_metadata {"hnsw:space": "cosine"}
The distance measure used to compare vectors. Cosine similarity is the
standard choice for text. Set it and move on.
08 · Confirm it worked
Ask it something
Demo 07python 07_query.py "Who founded SpaceX?"
question 'Who founded SpaceX?'
collection 547 vectors
0.662 ███████████████ spacex.txt In early 2002, Elon Musk started to look...
0.628 ██████████████ spacex.txt SpaceX was founded in 2002 by Elon Musk...
0.627 ██████████████ spacex.txt Space Exploration Technologies Corp., ...
0.598 █████████████ spacex.txt The company is credited with advances...0.591 █████████████ spacex.txt In early 2012, approximately two-thirds...
All five from spacex.txt, out of a store containing four other
companies. Your ingestion pipeline is finished and correct.
08 · The actual prompt
This is what reaches the model
│ Answer the question using only the context below.
│
│ CONTEXT:
│ In early 2002, Elon Musk started to look for staff for his company,
│ soon to be named SpaceX. Musk approached five people for the initial
│ positions, including Griffin, who declined the position of Chief
│ Engineer, Jim Cantrell and John Garvey ...
│
│ SpaceX was founded in 2002 by Elon Musk with the goal of reducing
│ spaceflight costs ...
│
│ QUESTION: Who founded SpaceX?
prompt length: 2,972 characters, built from the top 3 chunks
Sent: the question, plus the original English text.
Not sent: the vectors. Their job finished the moment the matching was done.
Part five
The mistake that breaks most first builds
Nothing crashes. There is no error message.
09 · The consistency rule
The consistency rule
Use the same embedding model and the same dimension count
for your documents and for your queries. Every time. No exceptions.
Documents embedded in January
text-embedding-3-large 3,072 dimensions
Queries embedded in March
text-embedding-3-small 1,536 dimensions
Think of them as separate languages
A vector written by one model means nothing to another. The two systems
cannot understand each other, and neither one will tell you.
09 · Break it on purpose
Same store. Same question. Nothing crashes.
Demo 08python 08_model_mismatch.py
question: 'Who founded SpaceX?' (answer lives in spacex.txt)RIGHT model text-embedding-3-small
0.662 spacex.txt In early 2002, Elon Musk started to look...
0.628 spacex.txt SpaceX was founded in 2002 by Elon Musk...
0.627 spacex.txt Space Exploration Technologies Corp., ...
WRONG model text-embedding-3-large@1536
0.064 spacex.txt === Starbase, Texas === SpaceX manufact...
0.055 spacex.txt In December 2022, the U.S. Federal Comm...
0.052 microsoft.txt Microsoft became the third publicly ... ← WRONG FILE
Scores collapse from 0.66 to 0.06, and a
Microsoft chunk is returned for a question about SpaceX.
09 · Interactive
Try it: flip the model, break the system
Same store · same question · different query modelreal precomputed scores
0.00
top score
—
model used for the question
0
errors raised
09 · In practice
What this means in practice
Choose your embedding model before you ingest a single document.
Choose your dimension count at the same time, and write it down.
If you switch model later, you re-embed the entire corpus.
There is no partial migration.
Changing dimensions within the same model breaks it too.
A cheap defence
Store the model name and dimension count in the collection metadata when you
build the store, and assert on it when you query. Ten lines of code that turn a silent
failure into a loud one.
10 · Summary
The whole session in ten lines
Models have a token limit. Real document stores are millions of times larger.
So you retrieve the relevant pieces instead of sending everything.
A RAG system is two pipelines. Ingestion runs once; retrieval runs per question.
Ingestion: cut documents into chunks, embed each chunk, store vectors with their text.
An embedding is a fixed-length list of numbers where similar meaning gives similar numbers.
A vector database stores those numbers and searches them fast.
Retrieval: embed the question the same way, rank by closeness, take the top k.
Send the model the question plus the original text. Never the vectors.
Set chunk_overlap so sentences on the seam survive. Set persist_directory or you lose the work.
One embedding model, one dimension count, everywhere. Breaking this fails silently.
Next: the retrieval pipeline
You take a question, embed it with that same model, pull the closest chunks
out of db_chroma/, and hand them to an LLM to answer.
YOU BUILT · 547 vectors on disk, searchable, with sources YOU LOCKED IN · text-embedding-3-small at 1,536 dimensions BRING · a set of documents you actually want to ask questions about