Как это началось?

В 2019 году я начал работать на фанфикус.ком. Это социальная сеть русскоязычных писателей/читателей фантастики. Потратил около месяца на размышления о том, как структурировать архитектуру веб-приложения. В начале я не знал точно, над чем я работаю. Изначально это казалось небольшим сайд-проектом на несколько месяцев.

При запуске я решил выбрать полный стек MEAN (MongoDB, Angular, ExpressJs, NodeJs). Однако возникла дилемма, что выбрать MySQL или MongoDB. Потому что раньше у меня был некоторый опыт работы с MySQL, и я знал, что базы данных SQL занимают большую долю рынка в веб-проектах. MongoDB был выбран, потому что он основан на объектах javascript, поэтому его естественно использовать в текущем стеке.

Большая часть веб-приложения основана на сообщениях. Они могут быть разных типов и включать множество параметров. Такие как возрастной рейтинг, жанры, теги и т.д.

Понимая, что база данных должна быть гибкой, я решил избегать вложенных массивов объектов. Потому что это создавало риск несоответствия между коллекциями. Например, есть запись с объектами массива жанров. Жанр содержит название и идентификатор. Но в то же время есть коллекция жанров. Где перечислены все жанры. Мы используем его при создании новых постов.

post: {
  genres: [id1, id2, id3],
  tags: [id1, id2, id3]
}

Что произойдет, если мы переименуем жанр? в коллекции жанров он переименован, но во всех постах, содержащих жанр, остался со старым названием. Таким образом, мы получаем сообщение, содержащее несуществующий жанр.

Да, вы можете сказать, что каждый раз, когда мы обновляем жанр, мы можем обновлять все сообщения, содержащие этот жанр. Хотя я хотел избежать этой устаревшей нагрузки на базу данных.

Был выбран способ хранения в посте только массива идентификаторов жанров. Это казалось самым эффективным решением. В любом случае, это было оптимальнее, чем идти по пути SQL и иметь три коллекции: посты, жанры, постжанр.

Проблема

Сайт был запущен и работал достаточно хорошо. Даже по каждому запросу заполнял все посты возрастным рейтингом, жанрами и т.д. В итоге на фронтенде я получал полные объекты и не нужно было запускать дополнительные запросы.

Однако через несколько месяцев количество постов увеличилось. Мы начали упоминать медленное время загрузки. Потратив некоторое время на расследование, выяснили, в чем была проблема. И проблема заключалась вмассиве идентификаторов. Каждый пост содержал до десяти таких вложенных массивов. Это означает, что каждый пост должен был выполнять десять команд населения.

PostModel.find({}).populate(‘genres’).populate(‘tags’).populate(‘ageRating’).exec();

Это была не единственная проблема. То, как мы выполняем поисковые запросы к сообщениям, также зависит от того, как мы храним вложенные идентификаторы. Каждый раз, когда мы запускали поиск на веб-сайте, он искал заголовки тегов, затем мы брали идентификаторы и запускали запрос сообщений.

const tagsFound = await TagModel.find({‘title’: { $in: keywordsRegArr }}).exec();
const tagsIdsArr = tagsFound.map( tag=> tag._id );
PostModel.find({tags:tagsIdsArr}).exec();

Помимо производительности загрузки страниц, мы увидели, что наш сервер MongoDB Atlas слишком сильно заряжается.

Как это было решено?

После долгих споров с самим собой я понял, что наиболее оптимальным решением будет хранить полные объекты внутри каждого поста. Да, это увеличивало размер базы данных. Тем не менее, это решало основную проблему с производительностью и запросами.

Теперь коллекция постов выглядела так:

post: {
  genres: [{id: 1, title: 'one'}, {id: 2, title: 'two'}],
  tags: [{id: 1, title: 'one'}, {id: 2, title: 'two'}]
}

Это означало, что теперь каждый раз, когда мы ищем сообщения, нам не нужно искать теги, чтобы получить их идентификаторы. Теперь он просто выполняет текстовый поиск по всем сообщениям.

Добавлена ​​еще одна важная вещь — кеширование. Для этого я использовал пакет node-cache npm. Часть запросов кешируется на NodeJs. Таким образом мы снизим нагрузку на базу данных. Некоторые запросы кешируются часами, некоторые минутами.

Результат

Как уже было сказано, теперь мы смогли выполнить запрос текстового поиска и избежать множественных заполнений.

Объекты постов извлекались из пост-коллекции напрямую, без каких-либо манипуляций.

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

Недостатки

  1. Теперь каждый раз, когда мы меняем жанры, возрастные рейтинги и т. д., нам нужно обновлять все сообщения, содержащие эти объекты. Но эти предметы меняются редко, поэтому мы можем усвоить этот.
  2. Затем мне также пришлось изменить поисковые запросы из клиентского приложения. Потому что постколлекция содержала вложенный массив объектов вместо массива идентификаторов
  3. Размер хранилища увеличен. В итоге размер базы существенно не увеличился. Мы даже не упомянули об этом.

Заключение

Проект вышел год назад. На данный момент у нас 22 тысячи пользователей, и это число растет. У нас реализовано много функций, но еще больше находится в разработке. Есть много проблем, которые мы встречаем на дороге. Однако преодолевая их и покоряя следующие горные вершины.

Сейчас рефакторинг базы данных тестируется на нашем тестовом сервере и скоро будет выпущен.