Типичный сценарий: клиент загружает 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, etc.), проектируем пайплайн.
- Реализация: пишем код парсеров и интеграцию с вашей 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 лет на рынке, гарантия качества на каждый этап.







