Разработка сортировки товаров для интернет-магазина
Сортировка определяет, что пользователь видит в первую очередь. Дефолтная сортировка по дате добавления — не стратегия. Правильная сортировка по умолчанию увеличивает конверсию: вперёд выходят товары с высоким рейтингом, хорошими остатками и подходящей ценой. Каждый вариант сортировки — это запрос с разным ORDER BY, и каждый требует правильного индекса.
Стандартный набор вариантов
| Вариант | SQL | Комментарий |
|---|---|---|
| Популярность | ORDER BY sales_count DESC |
Требует отдельного счётчика |
| Рейтинг | ORDER BY rating DESC, reviews_count DESC |
Двойная сортировка: рейтинг + вес |
| Цена: по возрастанию | ORDER BY price ASC |
Базовый |
| Цена: по убыванию | ORDER BY price DESC |
Базовый |
| Новинки | ORDER BY created_at DESC |
По дате добавления |
| Акции | ORDER BY discount_percent DESC |
Сначала самые выгодные |
| Релевантность | По score поисковика | Только в режиме поиска |
Дефолтная сортировка — как правило, «Популярность» или кастомный «Рейтинг магазина» (ручная сортировка merchandise-менеджером).
Взвешенный рейтинг
Наивная сортировка по среднему рейтингу некорректна: товар с одним отзывом на 5 звёзд окажется выше товара с 200 отзывами на 4.8. Используем Bayesian average или формулу Wilson score:
-- Bayesian average: (C * m + sum_ratings) / (C + reviews_count)
-- C = prior weight (обычно среднее кол-во отзывов по каталогу)
-- m = prior mean (ожидаемый рейтинг без данных, обычно 3.0–3.5)
UPDATE products SET
bayesian_rating = (50 * 3.5 + rating_sum) / (50 + reviews_count)
WHERE id = :id;
Это вычисляемое поле обновляется при каждом новом отзыве. Индекс по bayesian_rating для быстрой сортировки.
Счётчик продаж и популярности
sales_count — кумулятивный счётчик всех продаж с момента создания товара. Проблема: старый популярный товар всегда будет выше нового, который сейчас активно покупают.
Решение — time-decayed popularity score:
-- Обновляется ежедневно через cron
UPDATE products SET
popularity_score = (
SELECT SUM(quantity * EXP(-0.1 * EXTRACT(DAY FROM NOW() - o.created_at)))
FROM order_items oi
JOIN orders o ON oi.order_id = o.id
WHERE oi.product_id = products.id
AND o.created_at >= NOW() - INTERVAL '90 days'
)
Коэффициент 0.1 настраивается: больше — сильнее обесцениваются старые продажи, меньше — дольше «помнит» историю. Для быстроменяющегося ассортимента (мода, сезонные товары) — больше. Для стабильных категорий (инструменты, бытовая техника) — меньше.
Ручная сортировка (merchandising)
Менеджер магазина хочет управлять тем, что видит пользователь в начале категории: продвигать новинки, спонсируемые товары, залежавшийся товар. Для этого нужен sort_order — ручное числовое поле.
Интерфейс: drag-and-drop список товаров в административном разделе категории. Технически — сохраняем массив product_id в упорядоченном виде или sort_order: integer на каждом товаре.
Гибридная сортировка: первые N позиций — ручные, остальные — по алгоритму.
ORDER BY
CASE WHEN sort_order IS NOT NULL THEN 0 ELSE 1 END, -- ручные первые
sort_order ASC NULLS LAST,
popularity_score DESC
Сортировка с Elasticsearch
При использовании Elasticsearch сортировка задаётся в параметре sort:
{
"sort": [
{ "popularity_score": { "order": "desc" } },
{ "bayesian_rating": { "order": "desc" } },
{ "_score": { "order": "desc" } }
]
}
Для ручной сортировки: поле pinned_position с null для неприкреплённых. В ES есть специальный тип запроса pinned query — он поднимает конкретные ID в начало результатов без нарушения релевантности остальных.
Персонализированная сортировка
Продвинутый уровень — показывать каждому пользователю разный порядок на основе его истории. Пользователь часто покупает Apple — для него Apple-товары поднимаются выше. Пользователь смотрел товары в определённой ценовой категории — учитываем это.
Реализуется через пользовательские boost-факторы в Elasticsearch:
{
"query": {
"function_score": {
"query": { "term": { "category_id": 14 } },
"functions": [
{
"filter": { "term": { "brand": "apple" } },
"weight": 2.0 // персональный boost для этого пользователя
}
]
}
}
}
Boost-факторы вычисляются offline (батч-процесс на основе истории просмотров и покупок) и кешируются в Redis по user_id.
UI компонент сортировки
Стандартный select-дропдаун с вариантами. На мобайле — bottom sheet или отдельная страница (чтобы не мешать тапам). Текущий вариант сортировки отображается в URL (?sort=price_asc) и синхронизируется с состоянием компонента.
При изменении сортировки — запрос к API без перезагрузки страницы, скролл вверх к первому товару. Skeleton-плейсхолдеры пока список обновляется.
Индексы для производительности
-- Сортировка по цене
CREATE INDEX ON products (category_id, price ASC) WHERE status = 'active';
CREATE INDEX ON products (category_id, price DESC) WHERE status = 'active';
-- Сортировка по дате
CREATE INDEX ON products (category_id, created_at DESC) WHERE status = 'active';
-- Сортировка по рейтингу
CREATE INDEX ON products (category_id, bayesian_rating DESC) WHERE status = 'active';
-- Ручная сортировка + популярность (гибрид)
CREATE INDEX ON products (category_id, sort_order ASC NULLS LAST, popularity_score DESC);
Каждый дополнительный вариант сортировки — потенциальный отдельный индекс. При 8–10 вариантах сортировки это существенно влияет на размер индексов и скорость INSERT/UPDATE. Правильное решение: использовать ES для сложных сортировок, оставив в PostgreSQL только simple ORDER BY.
Сроки
- Базовые сортировки (цена, дата, рейтинг, выбор в select): 2–4 рабочих дня
- С взвешенным рейтингом и time-decayed popularity: 1 неделя
- Ручная merchandising-сортировка с drag-and-drop интерфейсом: +1 неделя
- Персонализация на основе истории пользователя: 2–3 недели (требует истории поведения и офлайн-расчётов)







