Розробка каталогу товарів для інтернет-магазину
Каталог — центральний модуль інтернет-магазину. Від його архітектури залежить всьо: швидкість пошуку, зручність навігації, 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 (MPPT) — кожна запись зберігає lft та rgt значення. Вибір поддерева: WHERE lft BETWEEN :parent_lft AND :parent_rgt — один запит без рекурсії. Запис складніший: при додаванні вузла оновлюють всі правих сусідів. Пакет kalnoy/nestedset для Laravel.
Closure Table — окрема таблиця всіх пар предок-нащадок. Найбільш гнучкий, займає більше місця. Для складних операцій з деревом (переміщення поддерев) оптимальний.
Рекомендація: для каталогів до 10 000 категорій Adjacency List з кешованим деревом у Redis — достатньо. MPPT — при частих виборках поддерев без кеша.
Модель атрибутів
Товари різних категорій мають різні набори атрибутів. Три підходи:
Таблиця з фіксованими колонками: 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
При відображенні карточки: завантажуємо батька + всі варіанти. Користувач вибирає комбінацію атрибутів → знаходимо відповідний варіант → оновлюємо ціну, фото, наявність.
Матриця варіантів для відображення вибору:
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 використовуємо курсор по останньому 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
Адміністративний інтерфейс каталогу:
- Масове редагування: виділити 50 товарів → змінити категорію/статус/ціну
- Імпорт з CSV/XLSX: маппинг колонок, попередпереглядом з помилками, фонове завантаження через queue
- Drag-and-drop сортування категорій: візуальне дерево з можливістю перетягування
- Управління атрибутами: додати атрибут у категорію → він з'являється на формах редагування всіх товарів категорії
Для масового імпорту: Laravel Jobs + Horizon. Файл завантажується в S3, завдання беруть з черги, парсяться рядок за рядком (через league/csv або PhpSpreadsheet), товари вставляються батчами по 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 тижні







