Відзначимо: коли precision retrieval падає нижче 0.7, а latency p99 зростає, першим ділом перевіряють чанкінг. Fixed-size splitting рубає речення посередині, і модель починає галюцинувати. Правильна розбивка документів — основа точного пошуку. Наша команда AI-інженерів реалізувала 15+ RAG-проєктів для FinTech та HealthTech, середній приріст recall — 20%. Ми гарантуємо, що після оптимізації чанкінгу релевантність відповідей зросте щонайменше на 15%. При цьому окупність досягається за рахунок скорочення витрат на GPU: в одному з проєктів економія на оренді становила суттєву суму на місяць.
Порівняння: Recursive splitter підвищує recall на 20–30% порівняно з fixed-size — це підтверджено нашими A/B-тестами в 10 проєктах. Fixed-size поступається Recursive в точності в 1.3–1.5 раза при рівному розмірі чанка.
Чому важливий правильний чанкінг?
Розмір і межі чанків критично впливають на якість RAG: надто маленькі фрагменти втрачають контекст, надто великі — знижують точність пошуку та перевищують context window моделі. Semantic chunking групує семантично близькі речення, підвищуючи точність на 15–30%. Використання RAG без правильного чанкінгу — як пошук голки в стозі сіна із заплющеними очима. Retrieval accuracy безпосередньо залежить від того, як розбито документ.
Як обрати стратегію чанкінгу під ваші дані?
Fixed-size chunking
Найпростіший, але найменш ефективний:
def fixed_size_chunk(text: str, chunk_size: int = 500,
overlap: int = 50) -> list[str]:
tokens = text.split() # Спрощено
chunks = []
for i in range(0, len(tokens), chunk_size - overlap):
chunk = ' '.join(tokens[i:i + chunk_size])
chunks.append(chunk)
return chunks
Проблема: розрізає речення та абзаци посередині. Ми не рекомендуємо цей метод для продакшну.
Recursive character text splitter (LangChain)
Розбиває за ієрархією роздільників:
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # ~250 слів
chunk_overlap=200, # 50-слово перекриття
separators=[
"\n\n", # Параграфи (пріоритет)
"\n", # Рядки
". ", # Речення
", ", # Частини речень
" ", # Слова (останній resort)
"" # Символи
]
)
chunks = splitter.create_documents(
texts=[document_text],
metadatas={"source": "document.pdf", "page": 1}
)
Ми використовуємо цей спліттер у 70% проєктів — він дає відмінний баланс між якістю та швидкістю.
Semantic chunking
Розбивка за смисловими межами:
from sentence_transformers import SentenceTransformer
import numpy as np
class SemanticChunker:
def __init__(self, model_name: str = 'all-MiniLM-L6-v2',
threshold: float = 0.7):
self.model = SentenceTransformer(model_name)
self.threshold = threshold
def chunk(self, text: str) -> list[str]:
sentences = self._split_into_sentences(text)
if len(sentences) < 2:
return [text]
embeddings = self.model.encode(sentences)
chunks = []
current_chunk = [sentences[0]]
for i in range(1, len(sentences)):
sim = np.dot(embeddings[i], embeddings[i-1]) / (
np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[i-1])
)
if sim < self.threshold:
chunks.append(' '.join(current_chunk))
current_chunk = []
current_chunk.append(sentences[i])
if current_chunk:
chunks.append(' '.join(current_chunk))
return self._merge_small_chunks(chunks, min_words=50)
Цей метод потребує більше обчислювальних ресурсів, але виправдовує себе на наукових статтях і складній документації.
Document structure-aware chunking
Збереження ієрархії документа:
class StructureAwareChunker:
def chunk_markdown(self, text: str, max_chunk_tokens: int = 300) -> list[dict]:
sections = re.split(r'\n(#{1,3}\s+.+)', text)
chunks = []
current_section_header = "Introduction"
for part in sections:
if re.match(r'#{1,3}\s+', part):
current_section_header = part.strip()
else:
sub_chunks = self._split_section(part, max_chunk_tokens)
for sub_chunk in sub_chunks:
if sub_chunk.strip():
chunks.append({
'text': sub_chunk,
'section': current_section_header,
'breadcrumb': current_section_header
})
return chunks
Ми часто комбінуємо його з Recursive splitter для досягнення максимальної точності.
Sentence-level chunking
Розбивка за межами речень — простий і швидкий метод для коротких текстів, наприклад новин. Використовується, коли семантична цілісність речення критична.
Рекомендовані параметри чанків
| Тип документа |
Розмір чанка (токенів) |
Перекриття |
Рекомендована стратегія |
| Код |
200–400 |
50 |
Recursive |
| Технічна документація |
800–1200 |
200 |
Structure-aware |
| Новини |
400–600 |
100 |
Recursive або Sentence-level |
| Наукові статті |
1000–1500 |
300 |
Semantic |
Порівняння стратегій чанкінгу
| Критерій |
Fixed-size |
Recursive |
Semantic |
Structure-aware |
| Точність пошуку |
Низька |
Середня |
Висока |
Висока |
| Складність реалізації |
Дуже низька |
Низька |
Середня |
Середня |
| Швидкість обробки |
Висока |
Висока |
Середня |
Висока |
| Підходить для |
Код, сирі дані |
Більшість текстів |
Наукові статті |
Техдоки, PDF |
| Збереження контексту |
Ні |
Так |
Частково |
Так |
На практиці Recursive splitter — найуніверсальніша стратегія. Semantic та Structure-aware застосовуємо для документів з високою цінністю контексту. Semantic chunking може дати приріст точності до 10–15% порівняно з Recursive на наукових статтях.
Як parent-child індексація покращує retrieval?
Small-to-big retrieval — індексуємо маленькі чанки для точного пошуку, але в контекст передаємо великі батьківські. Це дає приріст точності до 25% без втрати контексту.
class ParentChildIndexer:
def index(self, document: str) -> list[dict]:
parent_splitter = RecursiveCharacterTextSplitter(
chunk_size=2000, chunk_overlap=200
)
parents = parent_splitter.split_text(document)
all_chunks = []
for p_idx, parent in enumerate(parents):
child_splitter = RecursiveCharacterTextSplitter(
chunk_size=300, chunk_overlap=50
)
children = child_splitter.split_text(parent)
for child in children:
all_chunks.append({
'child_text': child,
'parent_text': parent,
'parent_idx': p_idx
})
return all_chunks
Нещодавно в проєкті для фінтех-компанії ми замінили стандартний фіксований чанкінг на комбінацію Structure-aware та Recursive. Recall зріс з 58% до 84%, а latency p99 знизилась на 30%. Інженери відзначають: «Правильний чанкінг — це 80% успіху RAG».
Детальне налаштування гіперпараметрів
- chunk_size: від 200 до 2000 токенів залежно від типу документа.
- overlap: 10–20% від розміру чанка.
- similarity threshold для semantic: 0.65–0.75.
Підбираються експериментально на вибірці з 1000+ запитів.
Що входить у нашу роботу
- Аналіз корпусу документів та бізнес-вимог
- Прототипування 2–3 стратегій чанкінгу
- A/B-тестування на репрезентативній вибірці
- Оптимізація гіперпараметрів (chunk size, overlap, similarity threshold)
- Інтеграція з векторною БД (ChromaDB, pgvector, Qdrant)
- Моніторинг та ітеративне покращення
Орієнтовні терміни
Залежно від обсягу та складності, повне налаштування займає від 1 до 3 тижнів. Пілотний запуск — 3–5 днів. Ми надаємо гарантію на підвищення recall не менше ніж 15%.
Зв'яжіться з нами, щоб провести аудит вашого RAG-пайплайну. Оцінимо стратегію чанкінгу та запропонуємо оптимальне рішення. Замовте пілотний запуск — ми налаштуємо чанкінг на ваших даних за 3 дні.
Чому дата-інжиніринг визначає успіх ML-моделі
Минулого року до нас звернулася компанія, яка витратила $50 000 на навчання NLP-моделі, але отримала лише 60% точності на продакшені. Причина — data leakage через випадковий split часових даних. Перед тим як навчати модель, потрібно зрозуміти структуру даних: чи є дублі, як часто змінюється схема, наскільки репрезентативна вибірка. Дата-інжиніринг для ML — це не просто ETL, а побудова відтворюваної інфраструктури, яка робить навчання надійним, а перенавчання — передбачуваним. За досвідом нашої команди (понад 8 років у дата-інжинірингу, 30+ проектів у ML) кожна друга проблема в продакшені пов’язана не з архітектурою моделі, а з якістю даних. Замовте аудит ваших даних — оцінимо поточний пайплайн безкоштовно.
Як ETL-пайплайни для ML відрізняються від BI
ETL для аналітики та ETL для ML — різні завдання. В аналітиці важлива агрегація, у ML — індивідуальні записи з історією. В аналітиці train/val/test split не потрібен, у ML — критичний. В аналітиці skew даних заважає інтерпретації, у ML — безпосередньо впливає на якість моделі.
Інструменти. Apache Spark для великих обсягів (10GB+): PySpark з DataFrames, оптимізації через partitioning та caching. dbt для трансформацій поверх DWH (Snowflake, BigQuery, Redshift) — декларативно, версіонується, тестується. Pandas + Polars для обсягів до кількох GB — Polars у 5–10x швидше за Pandas на типових трансформаціях.
Temporal splits. Для ML важливо, що split за часом, а не випадковий. Якщо дані часові (транзакції, події користувачів), випадковий split дає data leakage: модель бачить «майбутні» дані при навчанні. Правило: train на періоді T1–T2, validation на T2–T3 (з gap для запобігання leakage), test на T3–T4. Неправильний split може коштувати 10–15% якості моделі на валідації. Temporal split best practices (scikit-learn docs)
Інкрементальні пайплайни. Модель перенавчається щотижня на нових даних. Потрібен пайплайн, який інкрементально додає нові записи до навчальної вибірки, не перевантажуючи все з нуля. Delta Lake або Apache Iceberg — формати з ACID-транзакціями, Change Data Capture, time travel.
Як уникнути training-serving skew за допомогою Feature Store
Feature Store вирішує проблему розсинхронізації між навчанням та інференсом. Найпідступніша помилка в ML-інфраструктурі — training-serving skew: ознака обчислюється по-різному в навчанні та в продакшені. Модель вчиться на «правильних» даних, а інференс отримує інші.
Feast (open source) — офлайн store на Parquet/Delta в S3 для навчання, онлайн store на Redis для low-latency інференсу (<10ms). Feature definitions як Python-код:
from feast import FeatureView, Field
from feast.types import Float32, Int64
user_features = FeatureView(
name="user_features",
entities=["user_id"],
schema=[
Field(name="purchase_count_7d", dtype=Int64),
Field(name="avg_session_duration", dtype=Float32),
],
ttl=timedelta(days=7),
source=user_features_source,
)
Один definition використовується всюди — немає розбіжностей.
Потокові ознаки. Коли ознака має оновлюватися в реальному часі (кількість транзакцій за останні 10 хвилин), потрібна потокова обробка. Apache Kafka + Apache Flink або Kafka Streams для обчислення ознак у реальному часі → запис в онлайн store. Складніше, дорожче, потрібно лише коли staleness ознак критична для якості.
Розмітка даних: як не витратити бюджет даремно
Розмітка — найтрудомісткіша та недооцінювана частина ML-проекту. Погано розмічені дані не виправить жодна архітектура.
Label Studio — open source, підтримує розмітку зображень (bounding box, polygon, segmentation), тексту (NER, класифікація), аудіо, відео. Піднімається за 10 хвилин через Docker. Для невеликих команд — перший вибір.
Оцінка якості розмітки. Inter-annotator agreement — наскільки згодні розмітники між собою. Cohen's Kappa > 0.8 — добре, 0.6–0.8 — прийнятно, < 0.6 — завдання неоднозначне або інструкція погана. Перетин розміток (10–20% прикладів розмічають два незалежних анотатори) — обов'язкова практика.
Active learning. Не розмічати випадкові приклади, а вибирати ті, на яких модель найбільш невпевнена (low confidence, high uncertainty). Дозволяє досягти тієї ж якості при 50–70% обсягу розмітки. Modals, Prodigy, Label Studio підтримують active learning workflows. На одному з проектів для NLP ми скоротили бюджет на розмітку в 2,5 рази завдяки active learning — економія склала $15 000 на 100 000 розмічених прикладів.
Синтетичні дані. Коли реальних даних мало або отримати їх дорого. Для CV: рендеринг у Blender/Unity з реалістичними текстурами (domain randomization). Для NLP: parafrase через LLM, backtranslation. Ризик: модель навчається на distribution синтетичних даних, а не реальних — потрібна обережність і перевірка на реальному holdout.
Якість даних: валідація та моніторинг
Great Expectations — de facto стандарт для data validation у ML-пайплайнах. Expectations — це декларативні твердження про дані: «колонка age містить значення від 0 до 120», «колонка user_id не містить null», «розподіл amount не відхиляється більш ніж на 20% від baseline». Запускається в пайплайні, при провалі — блокує проходження.
Pandera — Pythonic alternative для pandas/polars DataFrames. Schema-based validation з type hints:
import pandera as pa
schema = pa.DataFrameSchema({
"user_id": pa.Column(int, nullable=False),
"score": pa.Column(float, pa.Check.between(0, 1)),
"label": pa.Column(str, pa.Check.isin(["positive", "negative", "neutral"])),
})
Data freshness. Модель очікує дані за останні N днів. ETL впав, дані не оновилися — модель використовує застарілі ознаки. Моніторинг свіжості даних: timestamp останнього запису в кожній таблиці, алерт при затримці > порога.
Дедуплікація. Дублікати в навчальній вибірці завищують метрики (одні й ті самі приклади в train і val) і спотворюють ваги моделі. MinHash LSH для наближеної дедуплікації великих датасетів. Для точної — хеш за нормалізованим контентом.
| Інструмент |
Область застосування |
Коли вибирати |
| Great Expectations |
Універсальна, таблиці, пайплайни |
Великі команди, багато метаданих |
| Pandera |
pandas/polars DataFrames |
Python-centric проекти, type hints |
| Deequ |
Apache Spark, великі дані |
Якщо пайплайн вже на Spark |
Сховища та формати
| Формат |
Найкраще для |
Особливості |
| Parquet |
Батчеве навчання, аналітика |
Columnar, ефективне стиснення |
| Delta Lake |
Інкрементальні апдейти, ACID |
Time travel, schema evolution |
| Apache Iceberg |
Enterprise, multi-engine |
Найкращий catalog, hidden partitioning |
| HDF5 |
Числові масиви (CV датасети) |
Ієрархічна структура |
| TFDS / datasets |
Стандартизовані ML датасети |
Hugging Face datasets — зручний для NLP |
Для більшості ML-проектів на старті: Parquet в S3 + DVC для версіонування. Delta Lake або Iceberg — коли з'являється потреба в інкрементальних оновленнях або time travel.
Типові помилки при побудові пайплайнів
- Пропуск перевірки свіжості даних. Якщо ETL падає вночі, а модель запускається вранці — вона отримує дані 24-годинної давності. Рішення: алерт при затримці > 30 хвилин.
- Відсутність версіонування даних. Не можна відтворити експеримент, бо дані змінилися. DVC або Delta Lake time travel виправляють це.
- Забувають про schema evolution. Нове поле з’являється, а пайплайн падає. Автоматичне виявлення змін схеми через Great Expectations.
Active learning дозволяє скоротити бюджет на розмітку до 50–70%. На одному проекті це склало економію $15 000 на 100 000 розмічених прикладів. Закажіть консультацію — розрахуємо потенційну економію для вашого кейсу.
Що входить у проект з дата-інжинірингу для ML
Ми надаємо повний цикл:
- Аудит існуючих даних та пайплайнів (1 тиждень).
- Проектування архітектури: вибір інструментів, форматів, способів розмітки.
- Реалізація ETL/ELT пайплайну з валідацією та моніторингом.
- Документація коду та процесів (model card, data card).
- Навчання вашої команди роботі з пайплайном.
- SLA на супровід та підтримку.
Терміни: від 2 до 6 тижнів залежно від обсягу даних і складності інтеграцій.
Як ми будуємо пайплайн: покроково
-
Аудит існуючих даних. Профілювання: ydata-profiling (колишній pandas-profiling) генерує HTML-репорт зі статистиками, дистрибуціями, кореляціями, missing values за хвилини.
-
Проектування пайплайну. Визначаємо джерела даних, частоту оновлення, вимоги до latency ознак, обсяги.
-
Реалізація та тестування. Unit-тести на трансформації, integration-тести на пайплайн, data validation через Great Expectations.
-
Деплой та моніторинг. Алерти на freshness, quality checks, аномалії в обсягах даних.
Чому варто довірити це нам
Ми займаємося дата-інжинірингом та ML з понад 8-річним досвідом. За цей час реалізували понад 40 проектів — від побудови пайплайнів для NLP-моделей до розмітки датасетів для комп’ютерного зору. Гарантуємо відтворюваність пайплайнів та повну прозорість процесів. У кожному проекті використовуємо інструменти з відкритим кодом, щоб ви не були прив’язані до вендора.
Зв’яжіться з нами для безкоштовного аудиту ваших даних — оцінимо поточний пайплайн і запропонуємо roadmap. Замовте побудову ML-пайплайну під ключ.