Вебформат

Главная/Блог/Сайты и e-commerce

Сайты и e-commerce7 июля 20269 мин

Каталог, который фильтрует и сортирует по цене конкретного покупателя

Один и тот же фильтр "от 30 до 50 тысяч" отдаёт рознице 217 товаров, дилеру - 172, и только 100 из них общие. Без личных цен дилер не увидел бы 72 товара, которые искал. Разбор, как фильтр, сортировка и "в наличии" научились считать по ценам конкретного покупателя - не отказываясь от штатного каталога.

Фильтр и сортировка - по цене покупателя
Фильтр и сортировка - по цене покупателя

У магазина 19 сегментов покупателей, и у каждого свои цены на один и тот же товар: розница, опт, дилеры, партнёры с индивидуальными условиями. Как эти цены рассчитываются, мы разобрали в статье про персональные цены для 19 сегментов. Но мало показать покупателю его цену в карточке товара - каталогом пользуются так, как привыкли пользоваться любым магазином.

  • 01"Покажи оборудование от 30 до 50 тысяч".
  • 02"Отсортируй: сначала дешёвые".
  • 03"Только то, что есть в наличии".
  • 04"От какой суммы вообще начинается этот раздел".

Все эти вопросы - про цену. А цена у каждого покупателя своя, и до того, как посетитель открыл страницу, она неизвестна. Обычный магазинный фильтр в такой системе перестаёт работать.

Цена вопроса

Это не теоретическая тонкость. Вот один и тот же фильтр в рабочем каталоге - глазами розничного клиента и глазами дилера.

Один и тот же фильтр - два разных каталога
Фильтр каталога
Цена от 30 000 до 50 000 ₽, сортировка - сначала дешёвые
один запрос - две разные выборки
Глазами розничного клиента
Инструментальная тележка - около 39 800 ₽, в списке
Компрессор - около 50 400 ₽: дороже 50 000, в выборку не попал
217 товаров
Глазами дилера
Компрессор - около 37 700 ₽, в списке
Тележка - около 29 900 ₽: дешевле 30 000, в выборку не попала
172 товара
Реальные товары и цены рабочего каталога, цены округлены. Один и тот же товар у одного покупателя попадает в диапазон, у другого - нет.

Инструментальная тележка стоит рознице около 39 800 ₽ и попадает в диапазон, а дилеру - около 29 900 ₽, и в выборку "от 30 000" она уже не входит. Компрессор наоборот: рознице он стоит около 50 400 ₽ и в диапазон не проходит, а дилеру - около 37 700 ₽, и он должен его увидеть.

Если посчитать по всему каталогу, расхождение перестаёт быть частным случаем.

Насколько расходятся выборки по фильтру "от 30 000 до 50 000"
Розничный клиент
217 товаров
Дилер
172 товара
как пересекаются эти выборки
Видит только розница
для дилера они дешевле 30 000 - и в диапазон не попадают
117
Попадают у обоих
общая часть двух выборок
100
Видит только дилер
для розницы они дороже 50 000 - и в диапазон не попадают
72
если бы цена была единой для всех
Фильтр показал бы дилеру список розницы
117 лишних товаров - и ни одного из 72, которые он искал
Совпало бы только 100 позиций из 289
Выборки совпадают лишь на треть: у дилера другие цены - значит, в один и тот же ценовой диапазон попадают другие товары. Фильтр, считающий по единой цене, вводил бы покупателя в заблуждение на каждой странице каталога.

Из 289 товаров, попадающих в диапазон хотя бы у одного из двух покупателей, общих - всего 100. Фильтр, который считал бы по единой для всех цене, показал бы дилеру список розницы: 117 товаров не из его диапазона и ни одного из 72, которые он искал. На каждой странице каталога, при каждом обращении к фильтру.

Почему это не решается настройкой магазина

Стандартный каталог собирает страницу одним запросом к базе данных: там же отбор, там же сортировка, там же деление на страницы. Это быстро и надёжно ровно до того момента, пока цена хранится в базе рядом с товаром. Персональной цены там нет: она своя для каждого из 19 сегментов и хранится отдельно. Отсюда сразу три препятствия.

  • Сортировать нечем. Магазин умеет упорядочить товары по колонке в базе данных. Колонки "цена этого посетителя" не существует.
  • Фильтровать нечем. Условие "от 30 до 50 тысяч" магазин переводит в условие для базы данных - но переводить его не во что.
  • Сложнее всего с постраничным выводом. Номера страниц и общее число найденных товаров магазин берёт из того же запроса, которым отбирал товары. Если порядок и состав выборки определяются вне этого запроса, счётчики оказываются неверными: число найденного меняется от страницы к странице, а часть товаров не показывается вовсе. Именно на этом обычно и останавливаются попытки добавить собственную цену в готовый каталог.

Решение

Отбор идёт воронкой

Воронка отбора: от дешёвых проверок к дорогим
Раздел каталога
все товары раздела
самый дешёвый шаг - первым
1. Ярлыки: распродажа, новинки, хиты
отсекаются свойством товара - эту работу делает сама база данных
дальше идёт уже суженный список
2. Цена "от - до" - по личным ценам
готовая таблица персональных цен отдаёт список подходящих товаров
фильтр "от 0" не прячет товары без цены - их можно заказать под заказ
самый дорогой шаг - последним
3. "В наличии"
нужны и цена, и остаток на складах именно его сегмента
есть цена сегмента и остаток на его складах?
Да - товар в выдаче
покупатель может его купить
Нет - не показываем
в наличии его для этого покупателя нет
прошедшие проверки товары
Сортировка по личной цене
товары без цены - в начало при "сначала дешёвые", в конец при "сначала дорогие"
готовый упорядоченный список
Страница и постраничная навигация
покупатель видит свой каталог со своими ценами
Каждый шаг работает пакетно: сколько бы товаров ни было в разделе, запросов к данным - единицы, а не по одному на каждый товар.

Каждый фильтр не пытается переписать запрос к базе данных - он превращается в список товаров. Списки пересекаются между собой и с обычным фильтром по характеристикам. Поэтому личная цена совместима со штатным умным фильтром: "дилерская цена от 30 до 50 тысяч, напряжение 380 В, в наличии" - это одна выдача, а не конфликт двух механизмов.

Порядок шагов выбран по стоимости вычисления. Сначала работает база данных - она отсекает товары по ярлыкам: распродажа, новинки, хиты. Затем подключаются готовые персональные цены. И только в конце считается наличие - самый дорогой шаг, потому что нужно собрать склады, доступные именно этому сегменту. Каждый шаг получает на вход уже суженный список.

Наличие - тоже персональное

"В наличии" здесь означает не "на складе что-то лежит", а "этот покупатель может это купить": у него есть цена на этот товар и есть остаток на складах, которые ему доступны. Разным сегментам доступны разные склады, а часть покупателей видит ещё и товары в пути, с датами прихода. Поэтому один и тот же товар обоснованно показывается "в наличии" одному покупателю и не показывается другому.

Страница собирается по личным ценам

Как страница каталога собирается по личным ценам
Покупатель открыл раздел
фильтр "от 30 до 50 тысяч", сортировка по возрастанию цены
каталог узнал сегмент покупателя
Компонент каталога
гость и непрошедший модерацию клиент получают общий сегмент
какие товары попадают в диапазон по его ценам?
Готовая таблица персональных цен
отдаёт отбор и полный упорядоченный список
товары без цены встают в начало или в конец
каталог выделил ровно одну страницу и передал счётчики явно
Штатная постраничная навигация
знает, сколько всего товаров и страниц, и выводит верные номера
результат на экране
Страница каталога
его цены, его наличие, верная навигация
Обычно счётчики страниц магазин берёт из своего же запроса к базе данных. Здесь порядок задан вне запроса - поэтому счётчики передаются явно, и штатная навигация работает без изменений.

Порядок товаров вычисляется по готовой таблице персональных цен, страница выделяется заранее - в запрос к базе данных уходят только те товары, которые действительно попадут на экран. А счётчики постраничного вывода передаются штатному механизму навигации явно, вместо тех, что он обычно берёт из запроса. Внешне это выглядит как обычный каталог: те же номера страниц, то же "найдено 217 товаров" - только всё это посчитано по ценам конкретного посетителя.

Отдельно решён вопрос товаров без цены - их около 2 000 из 13 125. Они не выпадают из каталога: при сортировке "сначала дешёвые" встают в начало, при "сначала дорогие" - в конец, и попадают в фильтр "от 0". Такой товар можно заказать, и скрывать его от покупателя было бы неправильно.


Как это устроено внутри

РешениеКак сделано
Компонент каталогаНаследник штатного компонента Битрикса: переопределены 8 методов, снаружи - равноценная замена. Шаблоны, умный фильтр и постраничная навигация остаются стандартными
Фильтр по ценеНе транслируется в запрос к базе данных, а материализуется в список идентификаторов и пересекается с уже собранным фильтром - за счёт этого совместим со штатным умным фильтром
СортировкаПерехватывается на входе (CUSTOM_PRICE и любое CATALOG_PRICE_N), изымается из параметров сортировки и заменяется полным упорядоченным списком идентификаторов из таблицы персональных цен
Постраничный выводСтраница выделяется на стороне PHP; счётчики NavRecordCount, NavPageCount и NavPageNomer проставляются в объекте результата до вызова родительского метода - штатная навигация выводит верные страницы
Порядок фильтровРаспродажа, затем цена, затем наличие - с ранним выходом на пустом результате; самый дорогой шаг получает минимум кандидатов
НаличиеПакетный расчёт: остатки по складам сегмента и, при необходимости, товары в пути; критерий - есть цена сегмента и есть положительный остаток. Ни одного отдельного запроса на каждый товар
ХранениеПредрассчитанная таблица "товар × сегмент × раздел" в оперативной памяти: сотни тысяч строк занимают около 53 МБ
Кеширование и персонализацияСтраница отдаётся из кеша, а персональные блоки (наличие, кнопка корзины) досчитываются отдельным механизмом отложенного контента - кеш не мешает персонализации

Два решения, которые обычно вызывают вопросы у коллег.

Почему раздел входит в ключ таблицы. Цена от раздела не зависит, но каталогу нужны ответы в разрезе раздела: границы фильтра, "Цена от…" на странице раздела, сортировка внутри раздела. Раздел в ключе избавляет от соединения с таблицей привязок при каждом таком запросе, а оно обходится заметно дороже, чем хранение готовых строк.

Почему постраничный вывод не потребовал переписывать компонент. Можно было отказаться от штатного компонента и написать свой каталог целиком. Вместо этого перехвачены ровно три вещи: сортировка, состав страницы и счётчики навигации.

Было

Отказаться от штатного компонента и написать каталог целиком: свой умный фильтр, свои SEO-настройки, своё кеширование - и всё это поддерживать самим при каждом обновлении платформы.

Стало

Перехвачены ровно три вещи: сортировка, состав страницы и счётчики навигации. Шаблоны, умный фильтр, SEO-настройки и кеширование работают штатно и обновляются вместе с платформой.

Что это дало бизнесу

РаньшеСтало
Фильтр по цене показывал бы всем одинаковый результат - неверный для 18 сегментов из 19Каждый покупатель фильтрует по своим ценам: дилер находит те 72 товара, которых при единой цене не увидел бы вовсе
"Сначала дешёвые" означало "дешёвые для розницы"Сортировка корректна для каждого сегмента
"В наличии" - справочная информация"В наличии" означает "вы можете это купить": есть ваша цена и остаток на ваших складах
"Цена от…" на разделе - общая для всехСвоя для каждого сегмента: дилер видит свою нижнюю границу
Персональные цены - повод отказаться от штатного каталога и писать свойШтатный каталог сохранён: обновления платформы, умный фильтр и SEO работают как прежде

В цифрах

ПоказательЗначение
Товаров в каталоге13 125 активных
Разделов каталога756 активных
Сегментов покупателей19
Готовых персональных ценниковоколо 220 тысяч
Объём таблицы персональных цен в памятиоколо 53 МБ
Товаров без цены, корректно обработанныхоколо 2 000 (15% каталога)
Складов17 активных
Расхождение выборки по фильтру 30-50 тысячрозница - 217 товаров, дилер - 172, совпадают - 100
Запросов к данным на страницу каталогаединицы: все проверки пакетные, независимо от числа товаров в разделе

Каталог с личными ценами - часть большой B2B-платформы: персональные цены, дилерский кабинет и двусторонний обмен с 1С. Что вошло в проект целиком - в кейсе B2B e-commerce платформы.

Нужен каталог, который считает по ценам покупателя?

Расскажите, как устроены ваши цены и склады. Покажем на живом проекте, как фильтр, сортировка и наличие работают по личным условиям каждого сегмента, и оценим доработку вашего каталога.