1. The Document Processing Nightmare When Building RAG Pipelines
Last month, our team took on a familiar challenge: building an internal Q&A chatbot for the Operations and Finance departments. The client handed over a Google Drive folder weighing nearly 15 GB, packed with over 400 files in every imaginable format:
- Financial reports saved as
.xlsxspreadsheets with 15–20 sheets and complex, tangled tables. - Standard Operating Procedures (SOPs) written in Word
.docxcontaining lookup tables and bullet points nested 4–5 levels deep. - Staff training slide decks in PowerPoint
.pptxformat. - Policy documents exported as mixed
.pdffiles (both native text and scanned pages).
Initially, the team opted for a homegrown DIY solution. We chained together popular libraries like pypdf, python-docx, and openpyxl to extract raw text and push it straight into a text splitter. The testing results disappointed everyone.
When we asked a test question: “What is the hotel per diem limit for a Tech Lead in Hanoi?”, the official policy explicitly stated 1,200,000 VND/night. Yet the bot answered 800,000 VND, mistakenly pulling the allowance tier for probation-period employees.
Opening the vector database to inspect the retrieved context immediately revealed the culprit. The entire allowance matrix in Excel had been extracted as a single contiguous string of text, completely losing column and row headers. The chunk splitter sliced right through a data row between the dollar figure and the job title. Deprived of context, the model began hallucinating.
2. Why Raw Text Destroys RAG Accuracy
Many newcomers to RAG assume that simply extracting text from a file is sufficient. In reality, Large Language Models (LLMs) depend heavily on semantic structure to properly understand the relationships between distinct pieces of information.
Raw text breaks RAG pipelines in three critical ways:
- Loss of hierarchical structure: Section headers (H1, H2) and bullet points define the scope and authority of a rule. Flattened into an undifferentiated wall of text, the LLM cannot tell general terms apart from specific policy exceptions.
- Flattened data tables: A numerical cell only carries meaning when tied to its respective column and row headers. Careless parsing turns a two-dimensional data matrix into a meaningless string of numbers.
- Fragmented chunking: Text splitters like
RecursiveCharacterTextSplitterperform best when guided by Markdown syntax (such as#,##, and table row dividers|---|). Stripped of these structural anchors, text gets arbitrarily chopped by character count, tearing vital information across separate chunks.
3. Two Common Dead Ends in Document Processing
Before discovering MarkItDown, our team explored two conventional approaches:
Approach 1: Writing custom Python parsers
We wrote bespoke scripts: pdfplumber for PDFs, python-docx for Word, and pandas for Excel. While this seemed fast at first, the codebase quickly ballooned past 1,200 lines. A steady stream of edge cases plagued the pipeline: merged cells in Excel, nested tables in Word, and misaligned lines in PDFs. Maintenance overhead turned into a massive project bottleneck.
Approach 2: Calling commercial parsing APIs (Unstructured, LlamaParse)
The extraction quality of these SaaS tools was impressive. However, we hit two major roadblocks: cost and security. Processing tens of thousands of pages caused monthly API bills to spike sharply. More critically, the client’s strict data compliance policies strictly prohibited sending internal financial records to third-party external servers.
4. Standardizing Data with Microsoft MarkItDown
In late 2024, Microsoft open-sourced MarkItDown. This Python library focuses on solving one specific problem: converting heterogeneous file formats (PDF, DOCX, XLSX, PPTX, HTML, CSV, JSON) into a single, standardized format: Markdown.
Why Markdown? First, it faithfully preserves headings and table structures via standard syntax like | Col1 | Col2 |. Second, Markdown is exceptionally token-efficient compared to HTML or JSON, while offering optimal compatibility with LLM context reasoning.
Installing the Library
You can install the standard build or the extended version depending on your needs:
# Base package for common office document formats
pip install markitdown
# Full package with OCR for images and audio transcription
pip install "markitdown[all]"
Basic Document Conversion
The initialization syntax is clean and straightforward. Instantiate the MarkItDown class and execute the convert() method:
from markitdown import MarkItDown
md = MarkItDown()
# Parse a Word policy document (.docx)
result_docx = md.convert("quy-che-cong-tac-phi.docx")
print(result_docx.text_content[:300])
# Convert an Excel spreadsheet (.xlsx) to a Markdown table
result_xlsx = md.convert("bang-gia-dich-vu.xlsx")
print(result_xlsx.text_content)
The Excel spreadsheet output is transformed into a clean Markdown table, preserving column headers and cell values line by line:
| Service_ID | Service_Name | Monthly_Fee | Notes |
| --- | --- | --- | --- |
| SRV01 | Cloud VPS Basic | 150000 | 2GB RAM, 1 Core |
| SRV02 | Cloud VPS Pro | 350000 | 4GB RAM, 2 Core |
Building a Batch Processing Pipeline
In production scenarios, you will typically iterate through an entire directory containing hundreds of files, batch-convert them to .md, and only then ingest them into your vector database:
from pathlib import Path
from markitdown import MarkItDown
def batch_convert_to_markdown(source_dir: str, target_dir: str):
md = MarkItDown()
valid_extensions = {".pdf", ".docx", ".xlsx", ".pptx", ".csv", ".html"}
src = Path(source_dir)
dst = Path(target_dir)
dst.mkdir(parents=True, exist_ok=True)
for file_path in src.iterdir():
if file_path.suffix.lower() not in valid_extensions:
continue
try:
print(f"[+] Processing: {file_path.name}")
parsed = md.convert(str(file_path))
output_file = dst / f"{file_path.stem}.md"
output_file.write_text(parsed.text_content, encoding="utf-8")
print(f"[OK] Saved: {output_file.name}")
except Exception as err:
print(f"[!] Error processing {file_path.name}: {err}")
if __name__ == "__main__":
batch_convert_to_markdown("./raw_docs", "./cleaned_markdown")
Integrating Vision Models to Describe Diagrams and Images
A standout feature of MarkItDown is its native integration with OpenAI or Azure OpenAI clients. When parsing PPTX decks containing infrastructure diagrams, the library automatically invokes a vision model to summarize the graphics into descriptive prose and embeds it directly into the Markdown file:
import os
from openai import OpenAI
from markitdown import MarkItDown
# Configure LLM client
ai_client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
# Initialize MarkItDown with GPT-4o for image analysis
md_vision = MarkItDown(llm_client=ai_client, llm_model="gpt-4o")
# Convert PowerPoint slides containing technical diagrams
result = md_vision.convert("so-do-ha-tang.pptx")
output_path = Path("so-do-ha-tang.md")
output_path.write_text(result.text_content, encoding="utf-8")
Battle-Tested Production Tips
- Split oversized Excel files: For spreadsheets exceeding 2,000 rows, the resulting Markdown table can easily blow past context window limits (surpassing 8k–16k tokens). Filter by sheet or segment your tabular data before running the conversion.
- Do not rely solely on the parser for scanned PDFs: MarkItDown extracts native PDF text cleanly. However, for poorly scanned documents or slanted receipts, you still need a dedicated OCR pre-processing pipeline (such as PaddleOCR or Tesseract) first.
- Append YAML Frontmatter: Before writing out the
.mdfile, inject structured metadata at the very top (source path, issuing department, revision date). During retrieval, the retriever can apply metadata filters prior to vector search, significantly boosting both retrieval speed and answer precision.
After migrating our entire data preparation workflow to MarkItDown, our chatbot’s retrieval precision on detailed numerical figures surged from 68% to over 92%. It is a lightweight, local utility, but it comprehensively solves the “garbage in, garbage out” problem before data ever reaches your model.
