Типовий сценарій: клієнт завантажує 500-сторінковий PDF з таблицями та багатоколонковою версткою, а RAG-система повертає биті відповіді — текст із колонок зливається, заголовки втрачаються, таблиці перетворюються на кашу. Ми знаємо, як цього уникнути: якісний парсинг — фундамент будь-якого RAG-пайплайну. За 5 років ми обробили понад 200 проєктів різної складності та переконалися: економія на парсингу обертається втратою точності відповідей.
Чому якість парсингу визначає успіх RAG?
Сучасні RAG-системи, такі як ті, що будуються на LangChain або LlamaIndex, вимагають чистого структурованого тексту для коректного чанкування та ембеддингу. Якщо на вході — «каша», то й пошук буде хаотичним. Дослідження показують, що до 30% помилок RAG викликані саме поганим парсингом вихідних документів. Ми використовуємо стек: PyTorch для кастомних моделей, pdfplumber для PDF, BeautifulSoup і markdownify для HTML, python-docx для DOCX.
Які формати ми підтримуємо? — індексація документів для
| Формат | Складність парсингу | Особливості |
|---|---|---|
| Висока | Таблиці, колонки, скани (OCR) | |
| DOCX | Середня | Вбудовані таблиці, стилі |
| HTML | Низька | Сміттєві теги, скрипти |
| Markdown | Низька | Готові заголовки та списки |
Як ми парсимо складні PDF?
Візьмемо реальний кейс: PDF з бухгалтерською звітністю — 200 сторінок, на кожній таблиця з фінансовими даними. Стандартні бібліотеки PyPDF2 або pdfminer втрачають межі комірок. Ми застосовуємо pdfplumber з кастомною постобробкою:
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: # Для складних PDF (з таблицями, колонками) — pdfplumber import pdfplumber with pdfplumber.open(path) as pdf: pages_text = [] for page in pdf.pages: # Збереження таблиць як 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'): # Параграф 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 = 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') # Видалення скриптів і стилів for tag in soup(['script', 'style', 'nav', 'footer', 'header']): tag.decompose() # Вилучення структурованого тексту 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" ) Структуроване вилучення метаданих
class MetadataExtractor: def extract(self, doc: ParsedDocument) -> dict: metadata = doc.metadata.copy() # Вилучення заголовків для навігації headers = re.findall(r'^#{1,3}\s+(.+)$', doc.text, re.MULTILINE) metadata['headers'] = headers[:20] # Перші 20 заголовків # Вилучення дат 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] # Мова документа from langdetect import detect try: metadata['language'] = detect(doc.text[:1000]) except Exception: metadata['language'] = 'unknown' return metadata Підготовка до індексації
Після парсингу документи чанкуються (розбиваються на фрагменти), ембеддяться та завантажуються у векторну БД. Ключовий момент: збереження структурних маркерів (заголовки, номери сторінок) у метаданих чанків для забезпечення атрибуції джерела у відповідях RAG.
Для 1000-сторінкового PDF повний цикл (парсинг → чанкінг → ембеддинг → індексація): 5-15 хвилин при використанні OpenAI Embeddings API. Власні GPU на базі Triton Inference Server прискорюють ембеддинг у 2-3 рази.
Що входить у роботу?
- Аудит документів: аналіз типів, обсягу, складності.
- Розробка пайплайну: парсери, чанкер, ембеддер, завантажувач.
- Інтеграція з векторною базою: Qdrant, ChromaDB, pgvector — на ваш вибір.
- Тестування на метриках: recall@k, precision@k, latency p99.
- Документація та навчання: передача коду, опис архітектури, навчання вашої команди.
- Підтримка: 3 місяці гарантії на баги та адаптацію під нові формати.
Порівняння: готові сервіси проти кастомного рішення
| Критерій | Готові сервіси (наприклад, Unstructured.io) | Наше кастомне рішення |
|---|---|---|
| Якість вилучення таблиць | Середня (до 70%) | Висока (95%+) |
| Підтримка рідкісних форматів | Обмежена | Будь-які формати під замовлення |
| Контроль над метаданими | Мінімальний | Повний контроль |
| Вартість на 10 000 сторінок | ~$500/міс | Одноразово + підтримка |
| Інтеграція з вашим стеком | Через API | Глибоке вбудовування |
Процес роботи
- Аналітика: ви надсилаєте 2-3 зразки документів, ми оцінюємо складність і терміни.
- Проектування: обираємо стек (Hugging Face Embeddings, vLLM, тощо), проектуємо пайплайн.
- Реалізація: пишемо код парсерів та інтеграцію з вашою RAG-системою.
- Тестування: прогоняємо на ваших даних, підганяємо чанкінг та ембеддинги.
- Деплой: розгортаємо у вашій інфраструктурі (AWS, GCP, on-prem).
Орієнтовні терміни: від 2 тижнів до 2 місяців залежно від обсягу та складності. Вартість розраховується індивідуально під кожен проєкт.
Стратегії чанкування: як розбивка впливає на точність RAG
Вибір стратегії чанкування безпосередньо впливає на recall@5 у вашому RAG. Занадто великі чанки (2000+ токенів) знижують точність пошуку. Занадто малі (64 токени) втрачають контекст.
Перевірені стратегії:
- Fixed-size з overlap: чанки по 512 токенів, перекриття 64 токени. Добре для однорідних текстів без складної структури.
- Sentence window: чанк = речення + 2–3 речення по боках. Високий recall, підходить для FAQ.
- Heading-based: розбивка за заголовками документа. Ідеально для технічної документації та нормативів.
- Semantic chunking: нарізка за семантичними межами (SBERT cosine similarity). Найкраща якість, але потребує додаткових обчислень.
Ми тестуємо кілька стратегій на ваших документах і обираємо за метриками recall@5 та MRR.
Отримайте консультацію — надішліть зразки документів, і ми протягом 1 робочого дня дамо оцінку. Наш досвід: 200+ проєктів, 5 років на ринку, гарантія якості на кожен етап.







