Turning a complex PDF into truly RAG-ready markdown
A technical PDF sitting on a hard drive is dead data. A technical PDF properly converted, indexed and queryable via RAG is active knowledge. Between the two lies a step we rarely talk about, because it looks like old-fashioned manual labor. And yet it’s that step that decides the quality of everything that follows.
I have a RAG project under way on BMW engine documentation. The reference corpus is the Funktionsrahmen of the MSS60 control unit, 448 pages of dry technical German produced by Siemens in 2005, describing down to the comma the workings of the engine control unit of the BMW M3 E92 (S65) and M5/M6 E60 (S85). The kind of document the BMW forums have been quoting in fragments for fifteen years without any unified base ever having been built.
To get started, I isolated a single chapter: the Modulbeschreibung Momentenmanagement, 51 pages, detailing engine torque management. Representative enough to calibrate the methodology, compact enough to iterate quickly.
What was meant to be a small task turned into two evenings of dense work, because I discovered along the way that the interesting work isn’t the one I thought it was. The output of an automatic parser, even a good one, is raw material. Turning it into something a RAG system will really know how to exploit demands semantic decisions that neither LlamaParse, nor Docling, nor Voyage AI can make for you.
What I’m going to recount here is what happens after the automatic parsing. The step that conditions the final quality of retrieval more than all the chunking parameters combined.
Three features of the PDF that defeat the parsers
In my previous article on visual parsing, I explained why complex PDFs sabotage RAG pipelines when you rely on naive extractions. LlamaParse and its modern alternatives fix part of the problem by reading PDFs visually rather than via their often-corrupt internal structure. It’s a real qualitative leap.
But on the MSS60, I was able to measure concretely the limits of these tools, even the most advanced ones. Three features of the document give the parsers trouble.
The diagrams are rasterized Simulink block diagrams. No recoverable text, no underlying XML structure, just bitmap images embedded in the PDF. A modern visual parser will manage to OCR the labels (md_eta_zw_ve, →, Limiter MIN) but will at best produce a linear transcription that loses the flow logic. And it’s precisely this logic, which signal enters where, which produces what, in what order, that makes the whole value of the document. A chunk containing md_eta_zw_ve → Limiter MIN(md_max_begr) → md_ind_wunsch_begr with no descriptive sentence around it is a semantically poor chunk, despite its apparent information density.
The formulas are interwoven with technical German and code labels. An expression like md_ind_ve = (md_wi_opt_korr * md_eta_zw_ve) * K_RF_HUBVOLUMEN / 0.012566 is readable for a human who knows how to read an engine control unit. For a parser, it’s an ambiguous mix of words, identifiers, operators and magic constants that often ends up sliced into decontextualized fragments.
The tables of applicable parameters (chapters 16 and 17 of the document) are dense and low in redundancy: each row is two or three words, the context carried only by the distant table title. A good parser will transcribe them into clean markdown, but cut without care by a classic chunker, these tables will produce orphan chunks of the type K_MD_SK_PWGMIN | PWG threshold below which zero-torque-request monitoring is active with not the slightest piece of context surviving. For semantic search, it’s a dead loss.
No automatic parser will decide for you that these tables need to be split into individual subsections with a context reminder per entry. It’s a semantic decision, specific to your use case, that requires a human (or a well-briefed LLM) to understand what these tables contain and how they’ll be queried downstream.
Why I kept the German instead of translating (and why that’s counterintuitive)
My first wrong intuition was to want to translate. The document is in German, my queries will be in French, so let’s translate at conversion time. This intuition is doubly wrong.
First, because modern embedding models are cross-lingual, not just multilingual. It’s a distinction you rarely read in French-language RAG guides and that changes everything. A multilingual model knows how to handle several languages. A cross-lingual model places equivalent concepts, German Zündwinkel and French angle d'allumage, in a nearby region of the vector space. A French query then activates the right chunks even when those chunks are in German. I’d discussed this point in the article on embedding models: voyage-3-large is among the models that maintain ~85-90% of mono-lingual recall when you cross languages on benchmarks like MIRACL.
Second, because translating a technical term means freezing a debatable interpretation into a corpus that will cross paths with others. Schleppmoment can be rendered as “couple traînée”, “couple de traînée moteur”, “couple résistant moteur entraîné”, “couple de friction étendu”. None of these translations is canonical. If I translate in the MSS60 .md, and I later ingest another BMW PDF where I leave the German word, I fragment my vector space for nothing. My embeddings for “Schleppmoment” and “couple traînée” will be close, but not identical. I artificially create dispersion.
So I did the opposite: preserve the German as the main body of the document, and add a layer of targeted French annotations. This decision structured everything that followed.
Concretely, that gives:
- The technical text stays in German, faithful to the source PDF
- First occurrences of key terms are annotated in French in parentheses:
Das Reibmoment (couple de friction moteur entraîné), this annotation is enough to activate cross-linguality at query time - A complete French glossary of the technical terms, in a dedicated markdown section at the start of the document, embeddable like the rest
- French summaries in blockquote at the head of each major chapter, which play the role of an implicit “summary embedding” and capture high-level conceptual queries
This strategy has a non-trivial effect on retrieval: a query like “how does the Vmax regulation work” in French finds its relevant chunk even if the chunk’s content is entirely in German, because the French summary at the head of the section and the glossary anchor the French vocabulary in the shared vector space.
For those who want to dig into the mathematical why of this mechanism, I traced the odyssey of the concept of the vector from the seventeenth century to modern embeddings. It’s exactly the property that makes cross-lingual search possible: different languages activate common dimensions in the latent space when they describe the same concepts.
Four structural errors in the V1 that nothing flagged
I produced a first version of the markdown that seemed clean to me. Heading hierarchy respected, glossary in YAML frontmatter, formulas in code blocks, standard markdown tables. To the eye, it looked like a good RAG-ready document.
On the second read-through, I understood that this V1 contained four structural problems that would noticeably degrade retrieval quality. None would have been caught by a basic functional test: you have to understand how chunkers and embedders actually handle markdown to see them.
The glossary in the YAML frontmatter was stillborn. Most standard markdown chunkers, LangChain’s MarkdownHeaderTextSplitter, the LlamaIndex splitters, ignore or strip the frontmatter before chunking. My glossary of 60 technical terms, carefully written, would have been present in no chunk at all. Embedding nonexistent, retrieval impossible. It’s exactly the kind of error that’s paid in silence: no error surfaces, simply queries on French terms like “couple traînée” or “régulateur de ralenti” fall flat without anyone understanding why.
The French/German bilingualism mixed sentence by sentence produced a fragmentation of the vector space. Embedding a chunk where each paragraph alternates between a French explanation and a German detail blurs the semantic center of gravity. The resulting vectors are less discriminating than monolingual vectors that would have kept a consistency of language per chunk.
The diagrams were transcribed in pseudo-code alone. Something like:
md_ind_wunsch_filter
→ Limiter MAX(md_ind_vmax)
→ Limiter MIN(md_max_begr)
→ md_ind_wunsch_begr
It’s humanly readable, it’s even too readable, you get the impression the information is there. But embedded, those → arrows and those parentheses are parasitic tokens that carry almost no semantic signal. A natural-language query like “how does the Vmax limitation interact with the gearbox protection” will match poorly a chunk made up solely of pseudo-code, because no word in the chunk carries the idea of interaction, of protection or of Vmax limitation: you have to deduce all that from reading the flow, which an embedder can’t do.
The long tables of chapters 16 and 17 were going to produce orphan chunks. With a chunk_size of 5000 characters, an 80-row table would be cut right in the middle, giving a chunk whose | Name | Description | header sits in the previous chunk and whose last 30 rows float with no definition context at all. For a query on a specific parameter like K_MD_BEZUG_ZW, the returned chunk would be ambiguous at best, useless at worst.
The V2 fixed all that with four transformations.
The glossary moved from the frontmatter to a dedicated markdown section at the start of the document, guaranteeing it would be in its own indexable chunk. While I was at it, I added a subsection on the BMW parameter naming convention (K_ for constants, KL_ for 1D curves, KF_ for 2D maps, B_ for booleans), a convention no document in the field formalizes and that helps enormously to interpret the retrieved chunks.
The body of the document returned to faithful German, with French annotations only on the first occurrences of key terms. No more fragmentation of linguistic consistency within the chunks.
The diagrams are systematically doubled: a French prose description that tells the flow in natural language (“The filtered desired torque passes through five Limiters in series. The first imposes an upper bound via the Vmax regulation…”), followed by the structured pseudo-code for the reader or the downstream LLM that wants the technical detail. The prose is fully embeddable, the pseudo-code stays there for those who’ll read it.
The tables of chapters 16 and 17 were split into individual subsections at the #### level. Each constant, each map, each variable now has its own standalone chunk with a context reminder. Instead of having one orphan chunk with fifteen table rows, I have fifteen chunks each of ~200 characters containing: the parameter name, its definition, and the chapter number where it appears. That’s exactly what’s needed for a precise parametric query.
At the end of the document I added a section of cross-cutting syntheses: six high-level paraphrases that cross the document’s concepts (separation of the filling path / ignition path, DSC coordination principle, multi-layer protection, real-torque computation, Katheizen function, Vmax regulation). These syntheses are there to capture the conceptual queries that use no precise variable name. They don’t appear in the source PDF: it’s value added by the LLM at conversion time. And it’s precisely what no automatic parser could produce.
Visually, the gap between the two versions is massive:
| Metric | V1 (naive) | V2 (optimized) |
|---|---|---|
| Size | 66 KB | 119 KB |
| Words | 8,700 | 14,400 |
| Lines | 1,414 | 2,100 |
| Glossary | Frontmatter (invisible to chunkers) | Indexable markdown section |
| Bilingualism | Mixed sentence by sentence | Faithful German + targeted FR annotations |
| Diagrams | Pseudo-code only | French prose + pseudo-code |
| Dense tables | Raw (orphan chunks) | Split into #### subsections |
| Cross-cutting syntheses | Absent | 6 paraphrases at end of doc |
| Parametric retrievability | Low | High |
| Conceptual retrievability | Mediocre | Strong (FR summaries + syntheses) |
The document nearly doubled in size, but this extra density is entirely redundancy useful to retrieval: annotations, summaries, cross paraphrases, repeated contexts. Embedding this density costs €0.01 more in Voyage API. Gaining it in retrieval quality pays for that cost from the very first hour of use.
Six reusable patterns for any technical PDF
In doing this work, I identified six patterns that seem to me generalizable to any conversion of a technical PDF into RAG-ready markdown. I list them here so they serve those who’ll take on similar documents.
The glossary lives in a markdown section, not in the frontmatter. YAML frontmatter is useful for filtering metadata (doc_id, version, tags), but everything that must be found by semantic search has to be in the body of the document.
Blockquote summaries at the head of a section play the role of a summary embedding. With a chunk_size of 5000 characters and a splitter that respects headers, these summaries always end up in the first chunk of their section. They capture the conceptual queries the detailed chunks would miss.
Diagrams must be doubled in French prose and in structured pseudo-code. The prose is for embedding, the pseudo-code is for the downstream LLM that will have to restore the technical precision. Neither suffices alone.
Dense tables must be split into individual subsections. One parameter = one chunk = one distinct embedding. It’s the only way to guarantee precise retrieval on parametric queries.
Cross-cutting syntheses at the end of the document capture high-level conceptual queries. Five to ten paraphrases that cross the document’s main concepts. They don’t duplicate the content, they reformulate it in a different vocabulary, creating alternative entry points into the vector space.
Rich metadata in the frontmatter serves only query-time filtering, not semantic retrieval. doc_id, version, chapitre, mots_cles as an array. This metadata will be added to each chunk at upsert time into the vector database and will allow later searches to be filtered (by document version, by chapter, etc.). But it doesn’t take part in the cosine similarity.
There are no doubt other patterns depending on document types. For legal documents, for poetry, for code, the best practices differ. But on technical engineering documentation, manuals, datasheets, specs, these six patterns seem to me a solid foundation.
Why the manual work is paid only once and pays off indefinitely
The obvious objection, reading the above, is: “OK but this doesn’t scale. If you have to do this by hand for every document, your pipeline is dead the moment there are more than five PDFs in the corpus.”
It’s an objection to take seriously and to dismantle methodically.
First, this work isn’t really “manual”. It’s LLM-assisted. On the 51 pages of the Momentenmanagement chapter, I produced the V2 in two evenings of work. Not by reading and retyping 51 pages. By dialoguing with an LLM that had ingested the full PDF and that produced the structured markdown under my semantic direction. My work consisted of: deciding the overall strategy, validating the structuring choices, spotting the V1 errors, requesting the V2 corrections, checking the critical passages. An editor’s work, not a typist’s.
Second, and this is where the scalability argument really collapses, the effort drops dramatically from the second document of the same corpus onward. Converting the first chapter is an exploration phase: you have to discover the domain’s patterns, calibrate the glossary, decide the linguistic strategy, identify the recurring structures. On the Momentenmanagement chapter, I spent about 70% of the time building the methodology and 30% applying it. For the second chapter, the Modulbeschreibung Lambdaregelung, for example, the methodology is set. The common BMW glossary is already built. The conventions are stabilized. The LLM can reproduce 70 to 80% of the work autonomously under a simple instruction of the type “apply the same structuring as on the Momentenmanagement, report the ambiguities to me”. My work shrinks to validation and the resolution of edge cases.
For the remaining 397 pages of the Funktionsrahmen MSS60, I’ll probably spend between 15 and 20 hours total with this methodology to produce the whole thing, not 51 times 4 hours. A day and a half of effective work to turn a technical documentation unique in the world into a knowledge base queryable in perpetuity.
Third, this work is done only once per document. The MSS60 PDF was produced in 2005, it’s set in stone, it won’t change. The V2 markdown I produced will live ten, fifteen, twenty years in my corpus. The marginal cost per query is zero. The upfront investment pays for itself the moment you start using the RAG, and keeps paying indefinitely.
Fourth, and this is the economic argument that should close the debate, the cost of bad retrieval isn’t measured in Voyage or Pinecone bills. It’s measured in user-hours. When a RAG system returns mediocre chunks, the user loses time filtering the noise, reformulating queries, cross-checking against other sources, correcting the hallucinations the downstream LLM embroidered on ambiguous context. After a few weeks, they abandon the system and go back to bookmarks and PDFs open in parallel. Embedding costs €0.01 per document. The hidden cost of a bad embedding can reach hundreds of user-hours over the system’s lifetime, plus the loss of trust that makes the tool unusable. Saving two evenings on conversion can cost weeks of downstream fumbling to understand why the system answers badly. And as I documented elsewhere, the degradation worsens as the corpus grows.
The real subject isn’t “manual work doesn’t scale”. The real subject is: where in the RAG pipeline does human investment pay off the most.
The answer, after building this pipeline, seems clear to me. Investment in document conversion pays off a lot. Investment in the choice of embedding model pays off a little. Investment in chunking parameters pays off marginally. Investment in reranking, in the downstream prompt, in the vector database itself: marginal.
It’s a reversal of perspective compared with what the French-language RAG blogosphere is putting forward in 2026. There’s a lot of talk about Pinecone vs Qdrant, OpenAI vs Voyage, chunk_size 500 vs 1500. Very little about the quality of the markdown that goes into the pipeline. Yet that’s where 60% of the final quality is decided. The rest is fine-tuning on tens of percent.
Another trend that sheds light on this point: the gradual shift from RAG to CAG (Cache-Augmented Generation), enabled by the explosion of modern LLMs’ context windows. If tomorrow my 448 pages fit entirely in the context of a call to Claude or GPT, I won’t even need to chunk or embed any more. But, and this is the important point, I’ll still need the source markdown to be well structured.
There’s a widespread belief that a 2-million-token context solves the problem of document quality. The Needle In A Haystack benchmarks, which measure an LLM’s ability to retrieve a precise piece of information buried in a long context, show the opposite: attention performance degrades significantly in the middle of long contexts (the famous “lost in the middle”), and this degradation is more pronounced on poorly structured documents than on well-organized ones. An LLM with a 10-million-token window still reads a document with section-head summaries, glossarized terms, and a clear hierarchy better than a raw dump. The editorial quality of the document is the invariant that survives every evolution of the infrastructure. That’s what makes this investment, unlike the tech-stack choices that go obsolete, profoundly durable.
That’s why, in the end, I find this step of intelligent conversion the most profitable and most durable of the whole pipeline. And it’s paradoxically the one we talk about least.
For the remaining 397 pages of the Funktionsrahmen MSS60, I’m going to replicate the methodology chapter by chapter, refining the patterns as I go. When the full corpus is ingested, I’ll have, for the first time, a queryable knowledge base covering the entirety of the engine control unit of the S65 and the S85, something that exists nowhere else. The BMW forums have been quoting fragments for fifteen years without any unified base ever being built. RAG isn’t an end, it’s the tool that makes this unification possible. And well-made markdown is the raw material that makes RAG honest.