From Demo to Production
A RAG prototype fits in a few lines: split a text every 200 words, compute vectors, find the nearest ones, paste them into the prompt. In production, the difficulties are elsewhere: hundreds of heterogeneous documents, daily updates, different access rights depending on the team, and users who want to verify the source of an answer. This course tackles these questions, starting from the foundation: a reliable indexing pipeline.
The Indexing Pipeline
Indexing is a separate process from querying, run as a background task whenever a document is added or modified:
- Extract the text (PDF, web pages, documentation tool, tickets). The quality of the extraction determines everything else: repeated page headers, flattened tables, and mixed-up columns degrade search.
- Clean: remove repetitive elements, normalize whitespace, keep the headings.
- Split into chunks.
- Encode each chunk as a vector (lesson 2).
- Store text, vector, and metadata in the index.
The pipeline must be reproducible: rerun on the same documents, it produces the same chunks with the same identifiers.
Splitting by Structure
Fixed-size splitting cuts in the middle of sentences and mixes two sections in the same chunk. Company documents almost always have a structure (Markdown or HTML headings, heading styles in a word processor): use it.
def split_sections(markdown, doc_id):
sections, path, buffer = [], [], []
for line in markdown.splitlines():
match = re.match(r"^(#{1,6})\s+(.+)$", line)
if match:
# save the previous section, then update the path
...
level = len(match.group(1))
path[:] = path[:level - 1] + [match.group(2).strip()]
else:
buffer.append(line)
The heading path is kept: a paragraph “Carry-over is limited to 5 days” only makes sense with its context “Paid leave > Carry-over”.
Then Bound the Size
A section can be very long. You then group its paragraphs into chunks that do not exceed a maximum size (in words here, in tokens in production), never cutting a paragraph if it fits:
- add paragraphs one by one as long as the limit is not exceeded;
- when the next one no longer fits, close the chunk;
- the new chunk starts with the last paragraph of the previous one (overlap), if that fits within the limit.
Overlap prevents information that straddles two chunks from being unfindable. It has a cost: duplicated text in the index and sometimes in the prompt. A single paragraph longer than the limit forms its own chunk (or is split again by sentences).
Identifiers and Metadata
Each chunk gets a stable identifier, for example politique-conges#3 (document and rank). It is used for citations, evaluation, and updates. You attach metadata to it:
{
"chunk_id": "politique-conges#3",
"doc_id": "politique-conges",
"path": "Paid leave > Carry-over",
"text": "Carry-over is limited to 5 days...",
"embed_text": "Paid leave > Carry-over\n\nCarry-over is limited to 5 days...",
"organization_id": 12, "visibility": "rh", "version": "2026-09",
}
embed_text (path + text) is what you encode; text is what you display and cite. The organization_id and visibility fields will be used for filtering by access rights (lesson 5).
Tuning and Checking
There is no universal ideal size. Chunks that are too small lose their context; too large, they dilute the information and fill the prompt. Start from a reasonable value (a few hundred tokens), then measure on a set of reference questions (lesson 4), comparing several settings. Also inspect random chunks: it is the fastest way to spot a failed extraction.
Key Takeaways
- Indexing is a reproducible pipeline, separate from querying.
- Split by structure, bound the size, add an overlap.
- Stable identifiers and metadata make citations, access rights, and updates possible.
Exercises
Solutions and tests stay on the server. In the student space, your code is run and checked, and the tutor gives hints before the solution.
Split a Markdown document by sections
Write
split_sections(markdown, doc_id)which returns the list of non-empty sections, each in the form{"doc_id": ..., "path": ..., "text": ...}:- a heading line starts with 1 to 6
#followed by a space; its level is the number of#; pathis the current heading path, joined by" > ": a heading of level n replaces the heading of level n and removes the deeper levels;textis the content between two headings, lines joined by\nthen stripped of leading and trailing whitespace and line breaks; a section without text is ignored;- text placed before any heading has an empty
path("").
import re def split_sections(markdown, doc_id): ...- a heading line starts with 1 to 6
Group paragraphs with overlap
Write:
pack_paragraphs(paragraphs, max_words, overlap=1)which groups consecutive paragraphs into chunks of at mostmax_wordswords (words =split()): add paragraphs as long as the limit is not exceeded; when the next one no longer fits, close the chunk, then the new chunk starts with the lastoverlapparagraphs of the previous one only if they and the new paragraph fit within the limit (otherwise it starts empty). A paragraph that is too long on its own forms its own chunk. Each chunk is returned as text, paragraphs joined by\n\n;build_chunks(sections, max_words, overlap=1)which, for each section (format of the previous exercise), splitstextinto paragraphs (separated by\n\n, empty ones ignored), groups them withpack_paragraphs, and returns dictionarieschunk_id("<doc_id>#<n>", n counted from 0 per document),doc_id,path,text, andembed_text(path + "\n\n" + text, ortextalone if the path is empty).
def pack_paragraphs(paragraphs, max_words, overlap=1): ... def build_chunks(sections, max_words, overlap=1): ...Why encode the heading path?
A chunk contains only “Carry-over is limited to 5 days.” Why encode “Paid leave > Carry-over” with this text?
- To reduce the size of the vector
- Because, without context, the chunk does not mention leave and will be poorly retrieved by a question about carrying over leave
- Because embedding models refuse short texts
- To speed up the index
End-of-lesson quiz
What should an indexing pipeline produce when rerun on the same documents?
- Different chunks to vary the results
- The same chunks with the same identifiers
- An empty index
What is the overlap between two chunks for?
- To prevent information that straddles two chunks from being unfindable
- To reduce the size of the index
- To encrypt the text
Which metadata is essential for filtering results according to the user?
- The length of the chunk
- The organization and visibility level of the document
- The color of the heading
The rest of the course
- Embeddings and pgvector: Storing and Searching Vectors20 min
- Hybrid Search: Keywords, Vectors, and Fusion20 min
- Evaluating a RAG: Search, Citations, and Answers20 min
- Updates and Access Rights20 min