Типовий сценарій: клієнт завантажує 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 років на ринку, гарантія якості на кожен етап.







