Последние три недели наша команда ударными темпами ведёт работу над сайтом 13-го Томского инновационного форума. Всё бы ничего, но по ходу дела возникли несколько проблем. Обо всём по-порядку:
Теггирование.
Существующее решение позволяет добавлять теги к любой сущности (что нам собственно и надо). Делает это за счет generic relations со стороны тегов (Tag + TaggedItem). И тут то возникает первая проблема – расчет тег-клауда. Проходит это в несколько этапов:
1. сам запрос выглядит довольно просто:
SELECT tags_tag.id, count(tags_tagged_item.id)
FROM tags_tag
JOIN tags_tagged_item ON tags_tagged_item.tag_id = tags_tag.id
GROUP BY tags_tag.id
Плюс, по идее HAVING на минимальное кол-во сущностей, помеченных этим тегом. В принципе, выбирая сразу название тега у нас на руках уже есть все данные для формирования клауда, осталось только вывести всё на шаблон, применив стили на основанию веса каждого тега в их общем множестве, объединив соседние величины, чтобы вписаться в ограниченный набор стилей – в итоге заимеем те самые размеры текста, которые так греют глаз. Но, нам надо формировать ссылку на список всех сущностей (сгруппированных по типу), которые были помечены тегом, нужен цельный экземпляр модели Tag. Итого – чтобы вернуть экземпляр тега + 1 запрос на каждый тег, для получения всех полей и маппинга данных в объект питона.
Самое интересное, что эта выборка-маппинг происходит еще на этапе расчета, т.к. в коде мы оперируем уж экземпляром тега:
max_weight = 10
cloud = []
for tag in Tag.objects.all():
cloud.append([
tag,
math.log(tag.tagged_items.count()) * max_weight / math.log(max_weight)
])
Итог данного решения – жуткие тормоза в приложении, возникающие при передергивании БД на пересчет клауда.
Решение проблемы пришло с неожиданной стороны:
Для поиска по сайту мы используем solr, в индекс которого кладётся много чего, но все протеггированные сущности туда в итоге попадают тоже. Осталось ввести поле tags, объявив его списочным, наложить токенайзер, который разобъёт теги по запятым, и сделать фасеточную выборку из индекса, использовав теги в качестве ключевого поля:
solr = Solr(settings.HAYSTACK_SOLR_URL)
kwargs = {
'facet': 'on',
'facet.field': 'tags',
'rows': 0,
'start': 1
}
result = solr.search(q = 'language:%s' % language, **kwargs)
Возможно, конечно, что проблема тут в SQL бэкенде, в качестве которого был выбран PostgreSQL, и доступа к тюнингу которого у нас нет (сайт как и база крутится на хостинге заказчика, причем доступа к хосту с БД у нас нет). Но сам факт порождения такого числа запросов уже слегка расстраивает. Тегов много, очень много, одних статей в периодике уже >3K, отдельно лежат интервью (~1K) и библиотека мультимедиа (в сумме около 1K записей), и далеко не все теги встречаются с более-менее завидной частотой.
Но и это еще не всё, второй момент никак не затрагивает Django, скорее само решение по теггированию. При добавлении тегов к сущности как и все белые люди в обычное текстовое поле мы заносим список ключевых слов через запятую, при сохранении экземпляра разбираем их на этом основании и, лишив хвостовых пробелов, вызываем следующу магию – определяем, есть ли уже такой тег и если нет – создаём новый экземпляр, обновляем TaggedItem с внешними ключами на сам тег и модель. Это еще три запроса на каждый тег. Все происходит быстро, но только потому, что кол-во тегов к одной сущности вряд ли превышает десять штук. И каждый экземпляр модели обрабатывается контент-менеджером более-менее индивидуально.
Возможно, спасло бы кеширование, собственно, оно и спасало нас на начальных этапах, но выставляя большое время хранения клауда в кеше теряется актуальность, т.к. новые ресурсы поступают активно, и хотелось бы наблюдать динамику изменения клауда в приближении к реальному времени.
В общем – или отказываться от услуг ORM, или использовать стороннее решение, что произошло в нашем случае.
Если кто-то предложит красивый способ решения такой проблемы, буду очень благодарен.