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

When a RAG system receives a jumble of PDF, DOCX, or HTML files, answers lose accuracy and users lose trust. We develop high-quality document parsing that turns unstructured files into clean text while preserving structure and metadata. Our team delivers turnkey indexing—from auditing source formats to configuring the pipeline—providing a reliable foundation for your RAG solution.

AI Development Areas

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1344
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1306
  • B2B Advance company logo design
    B2B Advance company logo design
    753
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1049
  • AIDER company logo development
    AIDER company logo development
    992
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1097

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.