Як я будував рекомендаційну систему для книжкового агрегатора: Data Engineering на практиці

💡 Усі статті, обговорення, новини про DevOps — в одному місці. Приєднуйтесь до DevOps спільноти!

Йоу, спільното DOU!

Мене звати Анатолій Шара. Частина з вас може пам’ятати мій попередній матеріал про MLOps, а для тих, з ким ми ще не знайомі: я — NLP Engineer й уже ось як п’ятий рік служу в лавах ЗСУ.

Крайній рік став особисто для мене непростим періодом. Я інтуїтивно відчував, що вдруге в житті на мене насувається ПТСР, тому, щоб пом’якшити його наслідки, я знову звернувся до перевіреного способу — інтенсивного навчання з посиленою практикою.

Але давайте повернемося до основної теми цього блогу. Чесно кажучи, я давно вмів будувати різні ML-моделі, розумів базовий набір процедур у MLOps, працював із пошуком, ранжуванням і трохи з рекомендаціями. Але коли справа доходила до повного циклу роботи з даними — від «сирого» Parquet-файлу до production-grade пайплайнів, оркестрації, інфраструктури та деплойменту, — я реально бачив у себе значні прогалини.

Саме тому я вирішив поєднати приємне з корисним: зайняти голову на повну та системно добрати міцний фундамент у Data Engineering. Значну частину відповідної бази я отримав під час навчання на профільній програмі в УКУ. Проте головне для мене було інше: не просто пройти навчальні модулі й отримати сертифікат, а відразу «приземляти» знання та навички на живий технічний проєкт.

Так у мене під час навчання з’явився масштабний pet project — рекомендаційна система для онлайн-агрегатора книжок. Відверто кажучи, я не мав жодного бажання робити ще один Jupyter-ноутбук зі штучно ускладненим пайплайном і колаборативною фільтрацією. Мені хотілося побудувати систему, яка поводиться як реальний продукт: приймає події, накопичує історію взаємодій, зберігає профілі та метадані, агрегує сигнали, обчислює фічі, оновлює індекси, а потім віддає рекомендації через окремі сервіси.

І саме тут Data Engineering перестає бути абстрактною дисципліною й починає напряму визначати, чи взагалі система житиме.

З чого все почалося: не з моделі, а з архітектури

Перший висновок, до якого я прийшов досить швидко: якщо в системі є важкі NLP-компоненти, кілька джерел даних, різні типи сховищ і потреба в оновленні рекомендацій, монолітний Python-скрипт — це шлях у нікуди. На старті спокуса була очевидна: написати один великий пайплайн, який забирає дані, рахує фічі, генерує ембедінги, перебудовує індекси й десь у кінці повертає щось схоже на рекомендації. Але така схема ламається одразу, щойно в різних частин системи з’являються окремі життєві цикли. Клікстрім оновлюється постійно. Каталог книжок живе у своєму темпі. Ознаки користувача рахуються в іншому. Пошукові індекси та ембедінги — це вже окремий контур. А фінальне ранжування взагалі може жити в іншому шарі обчислень. Тому я почав мислити системою як набором сервісів і доменів, а не одним кодовим файлом. Окремо — події взаємодії користувача з каталогом. Окремо — обробка книжкових метаданих. Окремо — генерація ембедінгів і побудова індексів. Окремо — шар фіч і агрегатів. Окремо — сервіс, який віддає рекомендації. Саме тут підходи з модуля Architecture & Design, які я отримав в УКУ, виявилися для мене найбільш практичними: я перестав думати категорією «як написати пайплайн» і почав думати категорією «як система має поводитися, коли її частини змінюються незалежно одна від одної».

Чому для рекомендаційної системи мені виявилося замало батчів

Коли я тільки починав, моя ментальна модель була простою: є зібраний датасет, напишу якусь обробку, потім створю ML-двигун, і ось є нова версія рекомендацій. Для прототипу цього часто вистачає. Для живої системи — вже ні. У книжковому продукті сигналів багато: перегляд картки книжки, відкриття опису, додавання до вибраного, взаємодія з автором або серією, завершення купівлі. Якщо все це збирати тільки батчами, ти неминуче отримуєш затримку між поведінкою користувача та реакцією системи. Саме тому я перебудував логіку навколо подій. По суті, у мене з’явився event-driven контур: є подія, є контракт, є реакція системи. Природною відповіддю на це став Apache Kafka. У моєму випадку Kafka не була технологією «для галочки». Вона вирішувала дуже конкретну проблему: декуплювала сервіси й дозволяла обробляти сигнали незалежно, але за чітко визначеними контрактами. Один сервіс міг продукувати події взаємодії, інший — споживати їх для оновлення фіч, третій — використовувати для оновлення окремих рекомендаційних представлень. Система переставала бути жорстко з’єднаною. Окремий урок тут — контракти. Як тільки між компонентами з’являються події, швидко стає зрозуміло, що «якось домовимось про JSON» — це не стратегія. Одна непогоджена зміна поля або типу даних — і в кращому випадку падає споживач, у гіршому система тихо починає псувати дані. Тому я заклав схеми подій через Schema Registry і формати Avro/Protobuf. У теорії це виглядає як бюрократія, на практиці — як нормальна страховка від тихого розвалу інтеграцій. Технічно мені було важливо не продавати собі казку про «магічний exactly-once». У реальності я мислив прагматичніше: at-least-once delivery плюс ідемпотентна обробка там, де це критично, а для окремих сценаріїв — більш жорсткий контроль повторної обробки. Для рекомендаційної системи цього більш ніж достатньо, якщо правильно спроєктовані споживачі, стани та повторний запуск задач.

Де і як я зберігав дані

Щойно події пішли потоком, постало друге велике питання: що і де зберігати. Швидко стало видно, що одна універсальна база тут не врятує. У системі було кілька різних типів даних: історія взаємодій, метадані книжок, агреговані фічі, проміжні представлення для рекомендацій, а також «гарячі» ключі для швидкого доступу.

І саме тут почали реально працювати мої знання про розподілені бази даних. Корисно було не просто знати назви NoSQL-систем, а розуміти, який тип навантаження вони люблять. Я дивився на системи через їхню поведінку: document-store — там, де потрібна гнучка модель метаданих; key-value/in-memory — там, де критичний швидкий доступ до гарячого стану; окремі write-heavy або горизонтально масштабовані сховища — там, де важливі обсяги подій і патерни запису. Саме такі речі ми багато розбирали в УКУ, але реальне розуміння прийшло вже тоді, коли ці рішення довелося приймати руками на своєму проєкті. Ще важливіше — я почав мислити не назвами технологій, а read/write-патернами, латентністю, реплікацією, партиціюванням і компромісами storage engine. Ти швидко перестаєш обирати базу «бо вона популярна», коли проходиш шлях від «усе ніби працює» до «чому ця схема почала душити систему під реальним профілем доступу».

Airflow, dbt і момент, коли пайплайн перестає бути просто скриптом за розкладом

Події та сховища — це лише половина системи. Рекомендації з’являються не з самого факту наявності сирих даних, а з правильно підготовлених і стабільно оновлюваних фіч. У моєму проєкті треба було регулярно агрегувати поведінкові сигнали, ознаки популярності, частину текстових ознак, а також оновлювати дані для пошукового та рекомендаційного контурів. Якщо зробити це по-простому, починаються класичні проблеми: повторний запуск ламає стан, backfill дублює дані, а будь-який фейл змушує вручну думати, де система опинилася між двома кроками. Тут для мене добре відкрився Apache Airflow. Не як «зручний інтерфейс, де красиво намалювати DAG», а як інструмент дисципліни. Я почав мислити задачами як ідемпотентними блоками, правильно розводити залежності, налаштовувати retries, продумувати backfill і сценарії відновлення після падіння. У парі з dbt це виявилось особливо корисним. Я будував не просто SQL-трансформації, а керований шар моделей: очищені події, агреговані сигнали, окремі представлення для фіч користувача й об’єкта, а також проміжні таблиці для кандидатної вибірки. Для рекомендаційної системи це критично, бо тут легко заплутатися між сирими подіями, очищеними сигналами, агрегаціями, фічами та готовими представленнями для сервісу видачі. Один із найбільш корисних практичних уроків був простий: оркестрація — це не «поставити джобу на розклад». Це продумати поведінку системи при фейлі так, щоб повторний запуск не ламав дані й не робив систему непередбачуваною.

Пошук, рекомендації та важкі NLP-компоненти

Оскільки це був не просто класичний recommender, а система для книжкового агрегатора, мені було важливо працювати не лише з поведінковими сигналами, а й зі змістом самих книжок. Саме тому в архітектурі з’явився гібридний контур: BM25, dense retrieval, reranking і рекомендаційна логіка. На практиці це означало кілька шарів: збір і нормалізація метаданих книжок, генерація ембедінгів для текстових полів, побудова індексів, а далі — комбінування сигналів через hybrid search, weighted RRF, CrossEncoder та інші NLP-компоненти для більш точного фінального ранжування. І саме тут стало очевидно, чому без хорошого Data Engineering усе це довго не живе. Бо проблема не в тому, щоб один раз порахувати красивий Jupyter-ноутбук. Проблема в тому, щоб оновлювати індекси без хаосу, перераховувати фічі без дублювання, синхронізувати нові дані з рекомендаційним шаром, контролювати залежності між компонентами й робити це так, щоб система відновлювалася після падіння.

Замість висновку: заземлення в реальність через Kubernetes

Але насправді вся ця архітектура лишалася б лише красивою схемою на дошці, якби не пройшла найважливіший тест — тест реальним середовищем. Останнім, але чи не найважливішим етапом для мене стало «заземлення» всієї цієї системи. Я перестав думати про генерацію ембедінгів, оновлення індексів і сервіси рекомендацій як про локальні скрипти, які «колись» переїдуть на сервер, і почав пакувати їх як повноцінні мікросервіси. Частину пайплайнів я загорнув у Docker, описав для них конфігурацію розгортання в Kubernetes і саме там по-справжньому зіткнувся з реальністю продакшну.

Швидко з’ясувалося, що в production код падає не так часто, як нам хочеться думати. Значно частіше закінчується пам’ять, сервіс упирається в обмеження CPU, починаються проблеми з мережею, IO або таймаутами. Саме тому налаштування requests/limits, liveness/readiness probes, health checks і базової спостережуваності стало для мене не «девопс-шоком», а частиною самої архітектури. Kubernetes змусив мене мислити системою, яка не просто працює, а працює передбачувано, деградує контрольовано і залишається придатною до дебагу в той момент, коли щось іде не так.

Паралельно стало очевидно, що без CI/CD і хмарної інфраструктури така система швидко перетворюється на набір ручних дій і крихких домовленостей. Тому наступним логічним кроком стало винесення збірки, перевірок і розгортання в більш відтворюваний процес, а самої інфраструктури — в декларативний опис. І саме тут для мене остаточно зійшлися докупи всі шматки пазла: дані, пайплайни, сервіси, інфраструктура, ML-компоненти і реальні експлуатаційні обмеження.

Цей шлях — від сирого Parquet і перших експериментів до контейнеризованих сервісів, оркестрації та Kubernetes — став для мене найкращим підтвердженням того, що навчання працює лише тоді, коли проходить перевірку практикою. УКУ дало мені фундаментальний архітектурний каркас, а цей pet-проєкт дав найцінніше — реальні інженерні мозолі, без яких професійне зростання залишається теорією.

Мабуть, це і є головний висновок усього цього шляху: Data Engineering — це не просто робота з таблицями, пайплайнами чи черговим «модним стеком». Це спосіб мислити про систему як про єдине ціле, де дані, обчислення, сервіси й інфраструктура мають бути спроєктовані так, щоб усе це не розвалилося при першому ж серйозному навантаженні.

Якщо вам цікава тема архітектури рекомендаційних систем, Data Engineering-підходів у ML-продуктах або побудови подібних пайплайнів — пишіть у коментарі. З радістю поділюся тим, що можу розкрити без шкоди для внутрішньої кухні проєкту.

Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

👍ПодобаєтьсяСподобалось7
До обраногоВ обраному6
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Дуже цікаво було почитати, подяка. Я тільки-но свічнувся в Data Engineering і щось-трохи зрозумів :)

Цікавий топік.. з технічної точки зору...
Я оце все потроху вивчав місяць тому... поки раптом не потрапив в армію.

І тут, за 20 км від лбз... які там пет проекти...
Є поточні проблеми, дрони над головою... і де тут Data Engineering ..

Для мене ключове питання — як швидше закінчити війну... що для цього треба?

Як ти з цими питаннями справляєшся ?

Іншими словами — для чого військовому під час війни мирні пет-проекти?
Хіба це не затягує війну на невизначений термін? 🤔

Я оце все потроху вивчав місяць тому... поки раптом...

Все ответы можно получить у себя же из прошлого месячной давности. Горячая стадия войны идёт пятый год. Но только месяц назад для Вас случилось «раптом». И только после этого у Вас возникают вопросы «а почему не все остальные?», до того момента этих вопросов у Вас не было, Вы потроху вивчали.
Где же были эти правильные мысли и эти важные вопросы полтора месяца назад и вообще четыре года? Вопросы, конечно, риторические, то есть ответы и так известны.

до чого ця демагогія ?

тобі ж не кажуть, шо працівники сільського господарства врятували тебе від голоду .. лікарі від епідемії.. пожежники від пожежі... вчителі від неписьменності і т.д.

Реально демагогия. Прикрываешься «кем-то, которые...», в то время пока ты на вялике вивчав потроху всё что тебе хотелось. А теперь твоя очередь рятувати, а другие в это время повивчають. Життя, воно таке, спочатку тобі однаково на всіх, потім розумієш, що і всім на тебе так само.

не переживай... раз моя черга приймати рішення... вилікую усіх )

Підписатись на коментарі