Document Indexing for RAG: PDF, DOCX, HTML, Markdown Parsing

A typical scenario: a client uploads a 500-page PDF with tables and multi-column layout, and the RAG system returns broken answers—column text merged, headings lost, tables turned into mush. We know how to avoid this: quality parsing is the foundation of any RAG pipeline. Over 5 years, we've handled

AI Development Areas

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1284
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1240
  • image_logo-advance_0.webp
    B2B Advance company logo design
    696
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    982
  • image_logo-aider_0.webp
    AIDER company logo development
    917
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1031

A typical scenario: a client uploads a 500-page PDF with tables and multi-column layout, and the RAG system returns broken answers—column text merged, headings lost, tables turned into mush. We know how to avoid this: quality parsing is the foundation of any RAG pipeline. Over 5 years, we've handled more than 200 projects of varying complexity and have seen firsthand that skimping on parsing leads to lost answer accuracy.

Why Parsing Quality Determines RAG Success

Modern RAG systems, such as those built on LangChain or LlamaIndex, require clean, structured text for correct chunking and embedding. If the input is a mess, the search will be chaotic. Studies show that up to 30% of RAG errors are caused by poor document parsing. We use a stack: PyTorch for custom models, pdfplumber for PDFs, BeautifulSoup and markdownify for HTML, python-docx for DOCX.

Which Formats We Support – Document Indexing for

Format Parsing Complexity Features
PDF High Tables, columns, scans (OCR)
DOCX Medium Embedded tables, styles
HTML Low Garbage tags, scripts
Markdown Low Ready headings and lists

How We Parse Complex PDFs

Take a real case: a PDF with financial statements—200 pages, each with a table of financial data. Standard libraries like PyPDF2 or pdfminer lose cell boundaries. We use pdfplumber with custom post-processing:

from pathlib import Path from dataclasses import dataclass @dataclass class ParsedDocument: text: str metadata: dict source_format: str page_count: int = None class DocumentParser: def parse(self, file_path: str) -> ParsedDocument: path = Path(file_path) ext = path.suffix.lower() if ext == '.pdf': return self._parse_pdf(file_path) elif ext in ['.docx', '.doc']: return self._parse_docx(file_path) elif ext in ['.html', '.htm']: return self._parse_html(file_path) elif ext in ['.md', '.markdown']: return self._parse_markdown(file_path) else: raise ValueError(f"Unsupported format: {ext}") def _parse_pdf(self, path: str) -> ParsedDocument: # For complex PDFs (with tables, columns) — pdfplumber import pdfplumber with pdfplumber.open(path) as pdf: pages_text = [] for page in pdf.pages: # Save tables as markdown tables = page.extract_tables() text = page.extract_text() or "" for table in tables: table_md = self._table_to_markdown(table) text += f"\n\n{table_md}\n\n" pages_text.append(text) full_text = "\n\n---PAGE BREAK---\n\n".join(pages_text) return ParsedDocument( text=full_text, metadata={"source": path, "pages": len(pdf.pages)}, source_format="pdf", page_count=len(pdf.pages) ) def _parse_docx(self, path: str) -> ParsedDocument: from docx import Document doc = Document(path) elements = [] for element in doc.element.body: if element.tag.endswith('p'): # Paragraph para = element style = para.style.name if hasattr(para, 'style') else '' text = element.text_content() if style.startswith('Heading'): level = int(style.split()[-1]) if style[-1].isdigit() else 1 elements.append('#' * level + ' ' + text) elif text.strip(): elements.append(text) elif element.tag.endswith('tbl'): # Table table = self._extract_table_from_docx(element) elements.append(table) return ParsedDocument( text='\n\n'.join(elements), metadata={"source": path}, source_format="docx" ) def _parse_html(self, path: str) -> ParsedDocument: from bs4 import BeautifulSoup with open(path, 'r', encoding='utf-8') as f: soup = BeautifulSoup(f.read(), 'html.parser') # Remove scripts and styles for tag in soup(['script', 'style', 'nav', 'footer', 'header']): tag.decompose() # Extract structured text from markdownify import markdownify text = markdownify(str(soup), heading_style="ATX") return ParsedDocument( text=text, metadata={"source": path, "title": soup.title.string if soup.title else ""}, source_format="html" ) 

Structured Metadata Extraction

class MetadataExtractor: def extract(self, doc: ParsedDocument) -> dict: metadata = doc.metadata.copy() # Extract headings for navigation headers = re.findall(r'^#{1,3}\s+(.+)$', doc.text, re.MULTILINE) metadata['headers'] = headers[:20] # First 20 headings # Extract dates date_pattern = r'\b\d{1,2}[./]\d{1,2}[./]\d{2,4}\b' dates = re.findall(date_pattern, doc.text) if dates: metadata['dates_mentioned'] = dates[:5] # Document language from langdetect import detect try: metadata['language'] = detect(doc.text[:1000]) except Exception: metadata['language'] = 'unknown' return metadata 

Preparation for Indexing

After parsing, documents are chunked, embedded, and loaded into a vector DB. Key point: preserving structural markers (headings, page numbers) in chunk metadata ensures source attribution in RAG answers.

For a 1000-page PDF, the full cycle (parsing → chunking → embedding → indexing) takes 5–15 minutes when using OpenAI Embeddings API. Our own GPUs on Triton Inference Server speed up embedding by 2–3x.

What's Included

  • Document audit: analysis of types, volume, complexity.
  • Pipeline development: parsers, chunker, embedder, loader.
  • Integration with vector database: Qdrant, ChromaDB, pgvector—your choice.
  • Testing on metrics: recall@k, precision@k, latency p99.
  • Documentation and training: code handover, architecture description, team training.
  • Support: 3 months warranty on bugs and adaptation for new formats.

Comparison: Off-the-Shelf Services vs Custom Solution

Criterion Off-the-shelf (e.g., Unstructured.io) Our custom solution
Table extraction quality Average (up to 70%) High (95%+)
Support for rare formats Limited Any format on request
Control over metadata Minimal Full control
Cost for 10,000 pages ~$500/month One-time + support
Integration with your stack Via API Deep embedding

Process

  1. Analysis: you send 2–3 sample documents, we assess complexity and timeline.
  2. Design: choose stack (Hugging Face Embeddings, vLLM, etc.), design pipeline.
  3. Implementation: write parser code and integrate with your RAG system.
  4. Testing: run on your data, fine-tune chunking and embeddings.
  5. Deployment: deploy in your infrastructure (AWS, GCP, on-prem).

Estimated timeline: from 2 weeks to 2 months depending on volume and complexity. Price is calculated individually per project.

Chunking Strategies: How Splitting Affects RAG Accuracy

The choice of chunking strategy directly impacts recall@5 in your RAG. Large chunks (2000+ tokens) reduce search accuracy. Tiny chunks (64 tokens) lose context.

Proven strategies:

  • Fixed-size with overlap: 512-token chunks, 64 token overlap. Good for homogeneous text without complex structure.
  • Sentence window: chunk = sentence + 2–3 surrounding sentences. High recall, suitable for FAQs.
  • Heading-based: split by document headings. Ideal for technical documentation and regulations.
  • Semantic chunking: cut at semantic boundaries (SBERT cosine similarity). Best quality, but requires extra computation.

We test several strategies on your documents and choose based on recall@5 and MRR metrics.

Get a consultation — send us sample documents, and we'll give you an estimate within 1 business day. Our experience: 200+ projects, 5 years on the market, quality guarantee at every stage.