1. Знакомство с сервисом
2. Архитектура
3. Важные нюансы
4. Развёртывание в PROD
Представим, что «Знаток концертов» — Ваш умный библиотекарь 📚. Он настоящий специалист в своём деле, и по каждой книге (концерту) у него есть заметка с кратким содержанием 📝
Вы приходите к библиотекарю с мыслью "что-то этакое почитать" 💭. Размахиваете руками и говорите ему ваши желания. Библиотекарь вас внимательно выслушивает и записывает ✍️. Когда вы договорили, он уходит покопаться в своих заметках, чтобы сопоставить ваши желания с теми заметками, что у него есть 🕵️♂️. Он читает краткие содержания книг и находит наиболее подходящие для вас.
Когда библиотекарь находит нужное количество книг, он возвращается с ними к вам и с улыбкой на лице даёт почитать 😊
Вот такая добрая и ненавязчивая история )
Как им пользоваться ?
При первом заходе в «Знаток концертов» и нажатии /start Вам будет предложено выбрать город
После выбора города Вам будут рекомендоваться мероприятия в нём
Вводим текстовый запрос и получаем наиболее подходящие мероприятия в выбранном Вами городе
Какие нюансы ?
«Знаток концертов» хранит в сжатой форме информацию о названии, месте проведения и описании мероприятия 📌. Поэтому он НЕ чувствителен к информации о времени проведения концерта ⏰
Так сделано, чтобы информация о времени не перекрывала содержательную информацию о мероприятии.
Все рекомендуемые события актуальны ✅, и если есть желание найти событие по конкретной дате, то удобно воспользоваться Календарём 📅
Модуль парсинга получает данные о мероприятиях из внешних источников 🌐 и вызывает текстовый модуль (Selenium + BeautifulSoup4)
Текстовый модуль обрабатывает данные о мероприятиях: очищает их и нормализует (pymorphy3) 🧹
Подготавливает текстовые запросы для Yandex Cloud AI по шаблону:"{names} {places} {descriptions}"
Отправляет запросы к "text-search-doc" модели в Yandex Cloud AI с помощью библиотеки yandex_cloud_ml_sdk. От неё получает embeddings — векторные представления информации о мероприятиях.
Текстовый модуль возвращает контроль выполнения модулю парсинга 🔄
Модуль парсинга заносит мероприятия с embeddings в БД (PostgreSQL) 💾
Пользователь отправляет текстовый запрос 💬 в "Знаток концертов"
Запрос регистрируется хендлером Telegram-бота (AIOgram)
Telegram-бот оращается к текстовому модулю 🔗
Текстовый модуль обрабатывает данные из запроса: их очищает и нормализует
Отправляет запрос к "text-search-query" модели в Yandex Cloud AI и получает embedding
Достаёт из БД НЕ прошедшие (актуальные ✅) мероприятия в городе, который выбран у пользователя
Подсчитывает косинусные расстояния 📐 между embedding запроса пользователя и embeddings мероприятий
Определяет события с ближайшим косинусным расстоянием - это и есть наиболее подходящие события по запросу пользователя
Текстовый модуль возвращает найденнные мероприятия в модуль Telegram-бота
Telegram-бот отправляет пользователю найденные мероприятия в удобном формате
Пользователь счастлив 🍾
📌 НЕ забывать производить очистку временных файлов хромдрайвера
def clean_chrome_tmp(): shutil.rmtree("/tmp/.com.google.*", ignore_errors=True) shutil.rmtree("/tmp/.org.chromium.*", ignore_errors=True)
Лучше делать очистку как минимум перед каждым новым стартом парсера. Если разворачивать сервис на удалённом сервере с ограниченными ресурсами, то временные файлы могут сожрать много места. У меня так и было )
📌 НЕ создавать лишних хромдрайверов для парсинга
Я изначально наткнулся на то, что для перехода на страницу с конкретным мероприятием создавался отдельный драйвер, а потом он закрывался. Это действие лишнее: достаточно одного драйвера, который перейдёт на целевую страницу с событием и получит необходимую информацию, а потом его можно вернуть обратно через метод driver.back()
📌 Используйте WebDriverWait и обработку WebDriverException
Это Вам даст стабильность работы парсера, т. к. сбои и задержки - обычное дело.
📌 Используйте многопоточность для ускорения парсинга данных
parsing_indexes = get_parsing_indexes( start_ind=1, end_ind=number_of_posters+1, count_bot=NUMBER_PARSER_THREADS, ) threads = [Thread(target=parse_events, args=( website, base_site_path, bot_i, parsing_indexes[bot_i], parsing_indexes[bot_i + 1], target_table_in_DB, model) ) for bot_i in range(NUMBER_PARSER_THREADS)] for t in threads: t.start() for t in threads: if t.is_alive(): logger_parser.info(f'Thread №{t} ALIVE') else: logger_parser.info(f'Thread №{t} DEAD') for t in threads: t.join() for t in threads: if t.is_alive(): logger_parser.info(f'Thread №{t} ALIVE') else: logger_parser.info(f'Thread №{t} DEAD')
При парсинге основная время расходуется на ожидание прогрузки страниц и возможные сетевые задержки. Добиться прироста в производительности можно запуском нескольких потоков - в каждом из которых будет создан свой хромдрайвер, несущий ответственность за парсинг отведённых ему событий. Это хороший пример решения I/O bound задачи.
📌 Выбор моделей Yandex Cloud AI для получения эмбеддингов
from yandex_cloud_ml_sdk import YCloudML from conf.settings import FOLDER_ID, AUTH_TOKEN_CLOUD sdk = YCloudML(folder_id=FOLDER_ID, auth=AUTH_TOKEN_CLOUD) model_query = sdk.models.text_embeddings("query") model_doc = sdk.models.text_embeddings("doc") class EmbeddingEngine: def __init__(self, model_query, model_doc): self.model_query = model_query self.model_doc = model_doc def get_query_embedding(self, text): return self.model_query.run(text) def get_doc_embedding(self, text): return self.model_doc.run(text) embedding_engine = EmbeddingEngine(model_query, model_doc)
На начальном этапе я решил использовать Yandex Cloud модели, чтобы не тратить время и ресурсы на обучение собственных. В перспективе планируется использовать собственные модели - это позволит избежать тарификации запросов и более тонко их настроить. Так же это даст независимость от внешнего иснтруммента.
Правила тарификации для Yandex AI Studio: https://yandex.cloud/ru/docs/ai-studio/pricing
📌Нормализовать текст перед векторизацией
Нормализованный и очищенный текст будет преобразован в более точный embedding. Это повысит точность работы рекомендательной системы.
📌 Обращать внимание на лимиты при работе с Yandex AI Studio
Яндекс устанавливает лимиты на кол-во запросов в секунду. Учитывайте лимиты, чтобы избежать блокировок или неожиданного поведения сервиса.
Лимиты для Yandex Cloud моделей: https://yandex.cloud/ru/docs/ai-studio/concepts/limits
📌Кешируйте тяжёлые запросы
Текстовый модуль кеширует запрос на получение самых подходящих мероприятий, чтобы избежать лишней нагрузки.
Telegram-бот
📌 Используйте FSM (Машину состояний)
Чтобы режимы не пересекались и все сценарии с пользователем работали корректно используйте машину состояний. На её основе разграничены сценарии работы обратной связи, календаря и текстового поиска.
📌 Жёстко регистрируйте хендлеры в нужном порядке
Был пойман баг, когда хендлер доната не отрабатывал и вместо него вызывался другой.
Важно чтобы сервис стабильно работал, самостоятельно восстанавливался после сбоев и не требовал постоянного ручного вмешательства. В этом блоке я покажу, как упакован «Знаток концертов» для работы в production.
Сервис поднят в timeweb.cloud
На данный момент парсер даёт основную нагрузку по ресурсам ⚡
Он запускается периодически, чтобы поддерживать актуальность базы 🔄
На графиках приведён мониторинг следующих ресурсов 📊:
📌 Нагрузка на процессор 🔥
📌 Трафик 📡
📌 Оперативная память 💾
Каждый компонент системы находится в своём контейнере 📦 и это даёт:
Изоляцию зависимостей 🛡️
Упрощение развёртывания 🚀
Лёгкое масштабирование 📈
Повышение отказоустойчивости всей системы 💪
app (главное приложение)
Telegram-бот на AIOgram
Роль: принимает запросы от пользователей, управляет диалогом с использованием FSM, координирует поиск событий и формирует ответы.
Зависит от: pg (база данных), redis (кеш).
parsers (парсер мероприятий)
Многопоточный парсер на Selenium + BeautifulSoup4
Роль: периодически обходит сайты афиш городов Золотого кольца 🏛️, извлекает информацию о событиях, нормализует её и сохраняет в базу вместе с эмбеддингами.
Ключевая особенность: имеет жёсткие лимиты памяти, так как запускает несколько драйверов Chrome🌐, чтобы избежать ошибки Out of memory💥
pg (хранилище данных)
Роль: хранит информацию о событиях с эмбеддингами, данные о пользователях и сервисную информацию ⚙️
redis (оперативный кеш)
Роль: кеширует результаты семантического поиска. Снижает нагрузку на базу данных и Yandex Cloud AI API при повторяющихся запросах 🔄
app_debug
parsers_debug
Позволяют отлаживать работу через debugpy из IDE 🖥️ непосредственно в контейнерах.
Как они работают вместе: контейнеры связаны в единую сеть Docker Compose🌐. Основной цикл выглядит так: parsers наполняет pg, app по запросу извлекает данные из pg, используя redis для ускорения 📈. Отладочные контейнеры запускаются только по необходимости.
Основные принципы:
DRY через шаблоны YAML: Вместо дублирования настроек для похожих компонентов (бот, парсер) я использовал якоря (&app_base_template) и алиасы (<<: *app_base_template). Это в разы сократило файл и сделало его поддержку проще.
Защита от падений: Для всех контейнеров задана политика перезапуска on-failure с 30-секундной задержкой и неограниченным числом попыток. Если парсер упадёт из-за временной сетевой проблемы, Docker Compose сам его поднимет.
Жёсткие лимиты памяти: Каждому контейнеру прописаны deploy.resources.limits. Это предотвращает ситуацию, когда один «прожорливый» модуль (тот же Chrome в парсере) лишит ресурсов другие контейнеры и «уронит» удалённый сервер.
Отладка без боли: Рядом с основными контейнерами (app, parsers) есть их debug-версии (app_debug, parsers_debug). Они запускают контейнер с пробросом порта для debugpy. Если на сервере что-то пошло не так, я могу подключиться из IDE к удалённому контейнеру и отладить код так же, как на локальной машине. Это экономит время.
# Пример debug-контейнера app_debug: <<: *app_base_template ports: - "6789:6789" command: python -m debugpy --listen 0.0.0.0:6789 --wait-for-client music_events_bot.py
Makefile — это не просто туториал, а рабочий инструмент инженера. Я максимально упаковал в него все рутинные операции. Ниже приведу основные:
make init_env — магия для нового разработчика. Команда сама ставит нужную версию Python, создаёт виртуальное окружение и через poetry ставит зависимости. Вся настройка локальной среды — одна команда
make run_service / make run_parsers — запуск отдельных частей системы в продакшене. Под капотом: сборка образов и docker-compose up -d
make run_app_debug / make run_parsers_debug — запуск контейнеров для отладки
make stop_all_and_remove — полный сброс для чистого перезапуска
Код должен быть защищен тестами 🛡️. Это НЕ бюрократическая процедура, а страховой полис для разработчика. Тесты гарантируют, что сегодняшнее изменение НЕ сломает вчерашнюю функциональность, и позволяют писать код со спокойной душой 🙌
В качестве библиотеки для тестирования использовалась Pytest.
Разделение ответственности через типы тестов:
Юнит-тесты 🧩 проверяют работу отдельных функций в изоляции (нормализация текста, расчёт косинусного расстояния).
Интеграционные тесты 🔗 проверяют взаимодействие модулей (например, как парсер сохраняет данные в БД, как бот обрабатывает цепочку команд).
Полная изоляция тестов 🔒: для тестирования поднимается отдельный контейнер с тестовой базой. Данные инициализируются через фикстуры перед каждым тестом и после полностью очищаются. Это гарантирует, что тесты НЕ зависят от состояния продакшен-базы и НЕ влияют друг на друга.
Мокирование внешних зависимостей 🎭: любые обращения к внешним сервисам (Yandex Cloud AI API, Telegram Bot API) заменяются на заглушки unittest.mock. Это делает тесты:
📌 Быстрыми ⚡ (НЕ ждём сетевых ответов)
📌 Стабильными 🛡️ (НЕ зависим от доступности сторонних сервисов)
📌 Дешёвыми 🐸 (НЕ тратим деньги на облачные вызовы)
Тестирование критических путей:
Парсер: проверка, что извлечённые с сайта данные корректно очищаются и укладываются в заданный шаблон:{names} {places} {descriptions}.
Работа с эмбеддингами: проверка логики поиска ближайших событий (корректность работы top_closest_meaning_event с заранее известными векторами).
Бот: проверка сценариев FSM — корректность переходов между состояниями (выбор города, обработка текстового запроса, обратная связь и т.д.).
Кеширование: проверка, что Redis действительно сохраняет и возвращает результаты для одинаковых запросов.
Инвестирование времени на написание тестов — это экономия времени ⏳ в будущем на отладке. Тесты делают код предсказуемым и управляемым. Тестирование - единственный способ сохранить здоровье проекта 💚 в долгосрочной перспективе.
Создание современного полезного продукта с использованием ИИ — это в первую очередь инженерная задача 🛠️. Языковые модели LLM (в нашем случае — Yandex Cloud AI) — это мощный инструмент в руках умелого разработчика. Его сила раскрывается, когда система продумана: качественные данные 📊, грамотное их хранение 📚 и эффективное кеширование ⚡. Это позволяет быстро доставлять качественный результат пользователю 🚀.
«Знаток концертов» 🏛️ — это хороший пример синтеза современных технологий 🔗. Парсинг, облачные ML-модели и микросервисная архитектура объединены в один живой и полезный продукт.
Что дальше? У сервиса есть куда расти:
Рекомендательная система 🎯
Продвинутый мониторинг 📈
Полноценный CI/CD-пайплайн 🔄
Но это тема для следующих статей 📚...
💪 Спасибо, что прошли этот путь !
Если есть вопросы — задавайте в комментариях 💬
Можно писать мне или в группу в ВК 😉
Источник

