Разработка каталога товаров для интернет-магазина
Каталог — центральный модуль интернет-магазина. От его архитектуры зависит всё: скорость поиска, удобство навигации, SEO-трафик, простота управления ассортиментом. Ошибки в модели данных каталога — самые дорогостоящие, потому что правятся с миграцией существующих данных и переработкой зависимых модулей.
Иерархия категорий
Дерево категорий хранится в БД. Два распространённых подхода:
Adjacency List — каждая запись хранит parent_id. Простота записи, но выборка всего дерева требует рекурсивного CTE:
WITH RECURSIVE category_tree AS (
SELECT id, name, parent_id, 0 AS depth
FROM categories WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, ct.depth + 1
FROM categories c
JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree ORDER BY depth, name;
Nested Sets (MPTT) — каждая запись хранит lft и rgt значения. Выборка поддерева: WHERE lft BETWEEN :parent_lft AND :parent_rgt — один запрос без рекурсии. Запись сложнее: при добавлении узла обновляются все правые соседи. Пакет kalnoy/nestedset для Laravel.
Closure Table — отдельная таблица всех предок-потомок пар. Самый гибкий, занимает больше места. Для сложных операций с деревом (перемещение поддеревьев) оптимален.
Рекомендация: для каталогов до 10 000 категорий Adjacency List с кешированием дерева в Redis — достаточно. MPTT — при частых выборках поддеревьев без кеша.
Модель атрибутов
Товары разных категорий имеют разные наборы атрибутов. Три подхода:
Таблица с фиксированными колонками: products.color, products.size, products.weight. Работает только при однородном ассортименте. Добавление нового атрибута — ALTER TABLE, миграция, деплой.
EAV (Entity-Attribute-Value):
attributes (id, name, type, unit, filterable, sortable, category_id)
product_attributes (product_id, attribute_id, value_text, value_numeric, value_boolean)
Гибко, но медленно при JOIN-ах. Для фильтрации по атрибутам нужен Elasticsearch или денормализованный индекс.
JSONB-колонка в PostgreSQL:
ALTER TABLE products ADD COLUMN attributes JSONB;
-- Индекс для конкретного атрибута:
CREATE INDEX ON products ((attributes->>'color'));
-- GIN-индекс для любого атрибута:
CREATE INDEX ON products USING GIN (attributes);
Компромиссный вариант: гибкость EAV, но без избыточных JOIN-ов. Подходит для каталогов до 500 000 товаров.
Варианты товара (SKU)
Товар с вариантами (цвет × размер) — распространённая задача. Два паттерна:
Simple SKU: каждая комбинация — отдельная запись в products. Просто, но сложно управлять «родительской» карточкой.
Parent-Child:
products (id, type, parent_id, sku, name, price, stock)
-- type: 'simple' | 'variable' | 'variant'
-- variant: parent_id → variable product
При отображении карточки товара: загружаем родителя + все его варианты. Пользователь выбирает комбинацию атрибутов → находим соответствующий variant → обновляем цену, фото, наличие.
Матрица вариантов для отображения выбора:
type VariantMatrix = {
[attributeId: string]: {
[value: string]: {
variantId: number;
inStock: boolean;
price: number;
}
}
}
Структура URL и SEO
URL категорий — критично для SEO. Три варианта:
- Плоский:
/catalog/noutbuki— просто, теряет контекст иерархии - Иерархический:
/catalog/elektronika/kompyutery/noutbuki— лучше для SEO, сложнее при перемещении категории - Гибридный:
/noutbuki-c142— читаемый slug + уникальный ID (устойчив к переименованиям)
Для фильтрованных страниц: /noutbuki?brand=apple&ram=16 с canonical на /noutbuki или отдельные SEO-страницы для популярных комбинаций (/noutbuki-apple-16gb как статичная страница-агрегатор).
Schema.org: ItemList на страницах категорий с ListItem для каждого товара в листинге.
Пагинация и бесконечная прокрутка
Offset-пагинация: LIMIT 48 OFFSET 144. Работает, но при глубоких страницах (OFFSET 10000) PostgreSQL всё равно читает 10048 строк. Решение — keyset pagination:
-- Вместо OFFSET используем cursor по последнему ID
SELECT * FROM products
WHERE (sort_value, id) > (:last_sort_value, :last_id)
ORDER BY sort_value, id
LIMIT 48;
Keyset pagination мгновенна при любой глубине, но не поддерживает переход на произвольную страницу.
Бесконечная прокрутка vs пагинация: для мобайла — бесконечная прокрутка с IntersectionObserver, для десктопа с SEO-приоритетом — классическая пагинация (поисковики лучше индексируют страницы с явными номерами).
Управление каталогом в CMS
Административный интерфейс каталога:
- Bulk editing: выделить 50 товаров → изменить категорию/статус/цену
- Импорт из CSV/XLSX: маппинг колонок, предпросмотр с ошибками, фоновая загрузка через queue
- Drag-and-drop сортировка категорий: визуальное дерево с возможностью перетаскивания
- Управление атрибутами: добавить атрибут в категорию → он появится на формах редактирования всех товаров категории
Для bulk-импорта: Laravel Jobs + Horizon. Файл загружается в S3, задача берётся из очереди, парсится построчно (через league/csv или PhpSpreadsheet), товары вставляются batch-ами по 100 записей.
Кеширование
Страницы каталога — основная нагрузка на БД. Стратегия кеширования:
| Уровень | Что кешируем | TTL |
|---|---|---|
| Redis | Дерево категорий | 1 час, инвалидация при изменении |
| Redis | Листинг с фильтрами | 5–15 минут |
| CDN (Cloudflare) | HTML страниц категорий | 5 минут, stale-while-revalidate |
| Браузер | Статика (изображения, JS, CSS) | immutable |
При изменении товара инвалидируем кеш только тех страниц, где он присутствует. Cache tags в Laravel: Cache::tags(['category:electronics'])->flush().
Сроки
- Базовый каталог (категории, список товаров, карточка, пагинация): 2–3 недели
- С вариантами, EAV-атрибутами, импортом и кешированием: 4–7 недель
- Интеграция с Elasticsearch для поиска и фильтрации добавляет 2–3 недели







