Розробка фільтрації товарів за параметрами для інтернет-магазину
Ми розробляємо фільтрацію товарів, яка не втрачає продажі. Уявіть каталог із 500 ноутбуками: без якісної фільтрації користувач іде до конкурентів. Наше рішення — faceted search: доступні значення фільтрів оновлюються залежно від уже вибраних, і користувач завжди знає, скільки товарів за кожним значенням. Це принципово відрізняє реалізацію від простого WHERE-запиту.
Хороша фільтрація — це комбінація SQL-індексів, кешування та клієнтської синхронізації з URL. Наші інженери з 10+ років досвіду в e-commerce знають, як уникнути типових помилок: N+1 запитів при підрахунку лічильників, Race condition при швидких кліках та невідповідності стану URL і сторінки. Ми гарантуємо, що фільтр працюватиме на каталогах до 1 млн товарів без просадки LCP та INP. Типова помилка — реалізація фільтрації лише на стороні клієнта без синхронізації URL: користувач не може поділитися посиланням на відфільтрований список, а SEO-трафік втрачається. Ми проєктуємо систему так, щоб будь-яка вибрана комбінація фільтрів відображалася в шляху або параметрах URL.
Типи фільтрів
| Тип | UX-компонент | Приклад | Технічно |
|---|---|---|---|
| Множинний вибір | Чекбокси | Бренд: Apple, Samsung | WHERE brand IN (...) |
| Одиночний вибір | Radio buttons | Стан: новий/б/в | WHERE condition = ... |
| Діапазон числовий | Slider з двома ручками | Ціна: 5000–30000 грн | WHERE price BETWEEN ... AND ... |
| Діапазон через інпути | Поля «від» і «до» | Діагональ: 13–15.6 дюйм | WHERE diagonal BETWEEN ... |
| Булевий | Перемикач | Тільки в наявності | WHERE stock > 0 |
| Рейтинг | Зірочки (≥N) | Рейтинг від 4 | WHERE rating >= 4 |
| Колір | Кольорові свотчі | Колір: чорний, срібло | WHERE color IN (...) |
Як реалізувати faceted search на SQL?
Найпростіший підхід — фільтрація через PostgreSQL. Працює до ~100 000 товарів при правильній індексації.
-- Основний запит з фільтрами SELECT p.* FROM products p WHERE p.category_id = :cat AND (:brands IS NULL OR p.brand = ANY(:brands::text[])) AND (:price_min IS NULL OR p.price >= :price_min) AND (:price_max IS NULL OR p.price <= :price_max) AND (:in_stock IS NULL OR p.stock > 0) ORDER BY p.sort_order LIMIT 48 OFFSET :offset; -- Агрегації для лічильників (окремий запит на кожен фільтр) SELECT brand, COUNT(*) FROM products p WHERE p.category_id = :cat -- Всі фільтри КРІМ brand AND (:price_min IS NULL OR p.price >= :price_min) GROUP BY brand; Проблема SQL-підходу: для коректних лічильників потрібен окремий запит агрегації для кожного фільтра, виключаючи цей фільтр з умов. При 10 активних фільтрах — 10 додаткових запитів. На реальному навантаженні це не масштабується.
Що дає Elasticsearch для фільтрації?
Elasticsearch вирішує задачу за один запит через aggregations:
{ "query": { "bool": { "filter": [ { "term": { "category_id": 14 } }, { "terms": { "brand": ["Apple", "Samsung"] } }, { "range": { "price": { "gte": 5000, "lte": 30000 } } } ] } }, "aggs": { "brands": { "filter": { "bool": { "filter": [ { "term": { "category_id": 14 } }, { "range": { "price": { "gte": 5000, "lte": 30000 } } } ] } }, "aggs": { "values": { "terms": { "field": "brand", "size": 50 } } } }, "price_range": { "stats": { "field": "price" } } } } Кожна агрегація (brands, ram, screen_size) використовує фільтр без своєї власної умови — це і є faceted search. Один запит повертає і товари, і всі лічильники для всіх фільтрів.
Чому Elasticsearch краще SQL для великих каталогів?
У тестах з каталогом із 200 000 товарів Elasticsearch виконує агрегації в 10 разів швидше SQL. При цьому навантаження на базу знижується, оскільки всі лічильники отримуються за один запит. Для каталогів понад 50 000 товарів Elasticsearch дає якісну різницю у швидкості та багатстві фасетів.
Як синхронізувати фільтри з URL?
URL повинен відображати стан фільтрів для шерінгу та SEO:
/noutbuki?brand=apple,samsung&ram=16&price_min=50000&price_max=100000&sort=price_asc При зміні фільтра — pushState або replaceState без перезавантаження сторінки. При прямому вході за URL — ініціалізація стану фільтрів із параметрів. SEO-підхід: популярні комбінації фільтрів (бренд + категорія) оформлюються як окремі статичні сторінки з унікальним контентом і canonical. Сторінки з рідкісними комбінаціями — <meta name="robots" content="noindex">.
Клієнтська реалізація
Стан фільтрів зберігається в URL (source of truth) і дзеркалюється в React state:
type FilterState = { brands: string[]; ram: number | null; priceMin: number | null; priceMax: number | null; inStock: boolean; sort: 'price_asc' | 'price_desc' | 'popularity' | 'rating'; }; function useFilters() { const [searchParams, setSearchParams] = useSearchParams(); const filters = useMemo(() => parseFilters(searchParams), [searchParams]); const setFilter = (key: keyof FilterState, value: unknown) => { const next = { ...filters, [key]: value }; setSearchParams(buildParams(next), { replace: true }); }; return { filters, setFilter }; } При кожній зміні фільтра — debounce 300ms, потім запит до API. Результати оновлюються без перезавантаження сторінки.
Ціновий слайдер
Компонент діапазону цін — окрема задача. Вимоги:
- Два handle (min і max), які не можуть перетнутися
- При введенні з клавіатури — валідація та clamp
- Гістограма розподілу цін за слайдером (показує, де сконцентровані товари)
Гістограма: Elasticsearch aggregation histogram з interval = (max_price - min_price) / 20. Відображається через SVG path або tiny bar chart. Готові компоненти: @radix-ui/react-slider, rc-slider, noUiSlider. Radix-варіант переважний при Tailwind-стеку.
Оптимізація продуктивності
Кеш агрегацій: результати підрахунку фасетів змінюються не при кожному запиті. Кешуємо агрегації для категорії з типовим набором фільтрів у Redis на 5–10 хвилин. При оновленні товару інвалідуємо кеш категорії.
Індекси PostgreSQL:
-- Складений індекс для типового запиту CREATE INDEX ON products (category_id, brand, price) WHERE status = 'active'; -- GIN-індекс для JSONB-атрибутів CREATE INDEX ON products USING GIN (attributes); Lazy loading фасетів: показуємо перші 5–7 значень, кнопка «Показати всі» підвантажує решту окремим запитом.
Мобільна адаптація
На мобайлі фільтри приховані за кнопкою «Фільтри» → відкривається нижній drawer (bottom sheet) на весь екран. Всередині — ті самі компоненти, але зі збільшеними touch targets. Кнопка «Застосувати» фіксована внизу. При застосуванні drawer закривається, список оновлюється.
Що входить у роботу
- Документація схеми фільтрації: опис індексів, агрегацій та API
- Налаштування індексів і кешування (Redis, Elasticsearch)
- Розробка клієнтських компонентів на React із синхронізацією URL
- Тестування на навантаження до 1 млн товарів
- Навчання команди роботі з фасетами
- 30 днів підтримки після запуску
Скільки часу займає розробка?
- Базова фільтрація (SQL, чекбокси по 3–4 атрибутах, діапазон цін): 1–2 тижні
- Faceted search на Elasticsearch (динамічні лічильники, всі типи фільтрів, URL-синхронізація): 3–4 тижні
- Додавання гістограми цін і кешування агрегацій: +1 тиждень
Вибір між SQL і Elasticsearch визначається розміром каталогу. До 50 000 товарів добре спроєктований SQL впорається. Вище — Elasticsearch дає якісну різницю у швидкості та багатстві фасетів.
Порівняння підходів: SQL vs Elasticsearch
| Параметр | PostgreSQL | Elasticsearch |
|---|---|---|
| Продуктивність | До 50 000 товарів без просадок | До 1 млн товарів без просадок |
| Агрегації | N+1 запитів на кожен фільтр | Один запит на всі фасети |
| Складність впровадження | Низька (знайома БД) | Середня (потребує окремого кластера) |
| Гнучкість лічильників | Точні, але повільні | Швидкі, але можуть бути приблизними |
Чому ми не рекомендуємо MySQL для faceted search?
MySQL гірше PostgreSQL підтримує складені індекси та JSONB, а також не має GIN-індексів. Для фасетної фільтрації з діапазонами та множинними умовами PostgreSQL або Elasticsearch дають значно кращу продуктивність.Оцінимо ваш проєкт за 1 день. Зв'яжіться з нами, щоб отримати консультацію щодо вибору підходу та точних термінів.







