Не секрет, что каждый интернет-магазин должен помогать пользователям найти то, что им нужно. Особенно, если товаров у вас много (> 10). На помощь приходит каталогизация товаров, но разбить товары по категориям — полдела. Товары внутри категории нужно уметь фильтровать по их свойствам. Особенно, если товары у вас разношёрстные, например, одежда, электроника, ювелирные изделия и т.д. И тут любой разработчик, пишущий свой e-commerce продукт, сталкивается с неприятными реалиями жизни: у товаров могут быть совершенно разные свойства, у некоторых товаров они могут отсутствовать, некоторые товары по одному свойству могут попадать под разные значения (цвет платья то ли синий, то ли голубой, соответственно, неплохо бы его показать и по синему и по голубому цвету). Проще говоря, у вас EAV. Бывает ещё, что EAV вам диагностирует заказчик ближе к концу разработки, а то и просит добавить фильтр по динамическим свойствам уже после релиза.
Вы начинаете почёсывать за ухом, понимая, что в реляционную модель у вас ничего не укладывается, вы же уже выбрали MySQL в качестве СУБД, если вы хороший веб-разработчик, или, может, PostgreSQL, если вы про него читали, а адепты разных корпораций могут вообще выбрать их продукты, никто не запрещает. Чаще всего, тем не менее, это всё RDBMS, и динамические свойства туда вкручиваются не очень просто (читай: сложно), не каждому это под силу.
Вот, для примера, маленький кусочек диаграммы БД у популярной e-commerce платформы Magento:
(по клику в полный размер)
Вот и перед нами была поставлена задача сделать такой фильтр каталога для ювелирного магазина. А свойства у товаров у нас тогда вообще лежали в json-е в MySQL, т.к. нужны были только на странице самого товара и нигде более. Немного улучшало наше положение то, что это был ювелирный интернет-магазин, и свойства в нём можно было установить изначально, такие как размер кольца, тип металла, цвет металла, вставка. Тем не менее, полученное решение универсально, доработать код можно легко под полностью динамические наборы свойств у товаров.
Решено было, что менять половину БД и больше половины кода для добавления фильтра не есть хорошо, поэтому на помощь мы позвали in-memory key-value storage called Redis, в частности, его крутую возможность работать побитово со строками, операции: SETBIT, GETBIT, BITOP, BITCOUNT. О значении команд легко догадаться не заглядывая в документацию.
Схема хранения фильтров в Redis была следующей:
- Один ключ — это одно значение свойства товара, например, size-18: или color-red:
- Данными в каждом ключе являлся битмап длиной N бит, где N — количество товаров всего в магазине. Соответственно, позиция бита в битмапе — это ID товара, а сам бит показывает, принадлежит ли данный ID фильтру с таким значением.
Пример, для лучшего понимания:
ID товаров (позиция бита) | ID: 1 | ID: 2 | ID: 3 | ID: 4 | ID: 5 |
redis-key | redis-value | ||||
size-17: | 0 | 0 | 1 | 0 | 1 |
size-18: | 1 | 1 | 0 | 0 | 0 |
size-19: | 0 | 0 | 0 | 1 | 0 |
color-red: | 1 | 1 | 1 | 0 | 1 |
color-green: | 0 | 0 | 1 | 1 | 1 |
Таким образом, в редисе у нас 5 пар ключ-значение, т.к. имеем два фильтра — по цвету (2 варианта) и по размеру (3 варианта). В магазине всего 5 товаров, поэтому битмап состоит из 5 бит. Из таблицы видно, что товар с ID 2 — красного цвета, размера 18, а товар с ID 3 — размера 17, но имеет в себе как красный, так и зелёный цвет.
Для применения фильтра к каталогу товаров, достаточно произвести операцию побитового AND для выбранных пользователем значений фильтров. Например, человек хочет товар зелёного цвета размера 18, тыкает в фильтре две галки, а мы делаем:
BITOP AND result-key size-18 color-green
после чего в result-key у нас будет лежать битмап, представлющий собой побитовое умножение этих двух битмапов. Нам остаётся только посчитать места, в которых у нас стоят единички, позиции единичек и будут ID товаров с заданными фильтрами.
Генерация битмапов происходит на моменте добавления / изменения товара в админке, в зависимости от имеющихся свойств товара мы делаем BITSET в нужный фильтр, и всё.
плюсы такого решения:
1) Жрёт мало памяти. У нас > 50000 товаров, около 100 значений фильтров, то есть 50000 * 100 = 5 000 000 бит = всего 625 килобайт памяти.
2) Очень быстро. Сложность побитовой операции O(N), тем не менее, строки у нас не миллионами байт измеряются, а перемножить пару-тройку битмапов ио 50000 бит — задача пары микросекунд для процессора. Overall, в худшем случае (перемножение всех фильтров), замеряя разницу времени в PHP до отправки команды в REDIS и после получения результата — 40мс (это с доп. функцией из п.3, далее). Вполне реалтаймовая генерация страницы, для веба пойдёт. Если кажется много — просьба кешируйте результат, но нас это удовлетворило вполне.
3) Возможность подсчёта кол-ва товаров в каждом фильтре и категории. Это стало полезным side-эффектом. Мы теперь можем посчитать количество товаров для каждого значения фильтра, доступное в данный момент. Да, это требует побитовое умножение текущего result-key (текущей выборки товаров) на каждое значение фильтра, а затем выполнение BITCOUNT. Мы это реализовали, теперь можем динамически скрывать фильтры с пустым множеством товаров (человек, выбрав платиновые кольца с бриллиантами, не видит фильтра по цене «до 3 000 рублей»).
минусы такого решения:
1) Невозможность закодировать фильтры типа range, например, где цену пользователь может фильтровать вручную влоть до рубля. Ну такие, с ползунками ОТ и ДО, знаете. Которые ещё на мобилах никогда не работают. В нашем магазине фильтр по ценам представлял из себя просто пять вариантов (до 3000, 3000-10000 и т.д.), соответственно, закодировали их как 5 битмапов price-0-3000:, price-3000-10000: и т.д.
2) Необходимость передачи списка выбранных ID в MySQL для выборки их данных. Это, конечно, нехорошо, что мы из редиса кидаем список ID для выборки
SELECT * FROM products WHERE id IN (....)
Но, как оказалось, работает крайне быстро. В худшем случае вся страница каталога со всеми выбранными фильтрами для всех категорий генерировалась за 600мс, если не ошибаюсь. Пруф для нескольких фильтров:
Итогом, прикрутить это дело оказалось очень быстро, биндинги Redis для PHP имеются, сам Redis очень примитивен и лёгок для освоения за один день.
Комментарии (56)
vitalybaev
25.06.2015 15:54+1Мы в своем магазине (сейчас около 8000 товаров) для хранения товаро перешли на MongoDB
Это дало нам много преимуществ:
1) отсутствие схемы позволяет добавлять поля товара на лету (например одни имеют вес, а другие количество таблеток в упаковке
2) возможность хранить массивы — используем в случае когда товар может быть сразу в 2-3 и более категориях
3) эти же массивы используем для фильтров
Скорость поразительна, разумеется важно проиндексировать важные поля
evnuh Автор
25.06.2015 16:10Постгресовская скорость работы с jsonb ещё поразительнее: obartunov.livejournal.com/175235.html
vitalybaev
25.06.2015 16:12Не буду спорить, просто с MongoDB был опыт работы в другом проекте, к Postres присматриваюсь, изучаю.
Fesor
25.06.2015 22:41Это первый бенчмарк который обходит монгу из тех что я видел. Их столько много, всяких разных…
alekciy
25.06.2015 16:25Но при этом это не снимает проблему именования атрибутов? На вскидку: менеджер для одного товара создал поле «цена» в том время как у всех товаров используется «розничная цена». Получаем два разных поля, так ведь?
vitalybaev
26.06.2015 10:56Несмотря на многообразие полей у товаров, они все-таки жестко прописаны в административной панели. Я лишь описал наш кейс, пока что MongoDB нас полностью устраивает.
И цены у нас вручную нельзя прописать, они пересчитываются автоматически на основе прайсов поставщиков, равно как и скидки :)
AxVPast
25.06.2015 22:01Через год будуте читать статью с Хабра — как и почему публика с Монго переходила на постгресс и плакать :).
artifex
25.06.2015 16:00+3Делал сложные фильтры на много тысяч товаров с помощью Elasticsearch, заодно получил возможность полнотекстового поиска по названию, описанию и составу, учитывая опечатки.
Ну а Postgres — это стандарт, зря вы так.evnuh Автор
25.06.2015 16:16+1Видимо мой юмор не очень понятен, но я всего-лишь сделал акцент на том, что MySQL в вебе используется чаще, чем Postgres, добавив чувства гордости большинству :) На то есть как исторические причины (уверен, вы знаете это и без меня), так и объективные. Так и быть, заменил предложение.
foxmuldercp
25.06.2015 21:11Веб — он бывает разным — если это домашний вордпрессик установленный за 5 минут на самом дешевом тарифе, то на хостингах постгрес если и есть то по запросу или «покупайте наш крутой VPS/VDS» и ставьте что хотите.
А на серьезных проектах — за последние лет 5 моих собеседований — про MySQL я не услышал ни одного вопроса. ни бекапы, ни репликации, вообще ни одного. PG, Oracle, MSSQL спрашивают много и активно, MySQL — остался только у хостеров или у тех, у кого задача с него смигрировать.evnuh Автор
25.06.2015 21:20+1Не думаю, что стоит снова начинать этот спор.
foxmuldercp
25.06.2015 21:24+1Это не спор, а факты моего личного опыта на собеседованиях на хайлоадные проекты, куда мне было интересно попасть.
А спорить таки да, толку нету.
saterenko
27.06.2015 22:49Begun (если не путаю), Mamba, Badoo используют mysql. Mysql как и Postgre – это инструменты, а эффективно их использует программист.
Fesor
27.06.2015 23:24— богомерзские автоинкременты в mysql (до insert не выйдет получить id записи что в некоторых случаях делает жизнь болью и вынуждает использовать UUID)
— отсутствие возможности делать вложенные транзакции
— alter запросы не покрываются транзакциями (не шибко удобно для миграций).
Это тот минимум почему в некоторых задачах mysql в принципе не выйдет эффективно использовать и вам придется подстраиваться под инструмент и городить костыли.
А так… компаний много разных всяких, кто использует mysql или postgres (допустим skype на нем работал, а может и до сих пор).saterenko
27.06.2015 23:30-1Я с mysql работаю 15 лет (а так же работал с MSSQL, Oracle), ни чего из перечисленного не создавало мне проблемы.
И ещё раз, это инструменты, не нравится эта лопата, возьмите другую, которая вам нравится. Бессмысленно ругаться на совковую лопату, что ей трудно копать огород, а на штыковую, что ею долго перекидывать кучу земли. А если вам нужно перекидать кучу гравия, то возьмите шахтёрскую…Fesor
28.06.2015 00:08Так я ж не спорю, у меня тоже с этим проблем не было пока не перешел на инструмент, где с этим все хорошо (хотя у оракла тоже с этим все хорошо). Как бы мелочь но приятно. По поводу выбора инструментов — это так, согласен. Просто что бы знать из чего выбирать люди должны лучше представлять что зачем.
alekciy
25.06.2015 16:27Как впечатления? Опечатки может строить из коробки или нужна настройка?
artifex
25.06.2015 16:31В простом случае достаточно поиграться с магическими константами :) Elastic из коробки много чего умеет и неплохо справляется.
'query': { 'fuzzy_like_this': { 'fields': ["name", "brand"], 'like_text': query, 'fuzziness': '0.5', 'prefix_length': '1' } }
vitalybaev
26.06.2015 10:58Кстати, да! Elastic одна из замечательных вещей, которая активно используется во всех наших проектах. Фильтрацию товаров с ее помощью делать просто сказка
alekciy
28.06.2015 12:28А на сколько быстро работает? Сколько под себя ресурсов при этом хочет (Java же)? Фасетчатый поиск?
mgyk
25.06.2015 16:19+3А вы не думали над тем, чтобы просто вынести поиск в Elastic? Такие задачи туда как-раз очень хорошо подходят
evnuh Автор
25.06.2015 16:40Не просто думал, а даже смотрел в эту сторону. Но внедрить и настроить эластик — мне показалось задачей посложнее, чем разобраться с редис и добавить десяток строк кода.
xmdy
25.06.2015 17:00Эластик очень хорош, вернее, даже не эластик, а solr, он очень хорошо умеет искать, группировать, выводить количество найденных совпадений.
Даже сам на интервалы разбивать и считать попадания в интервалы) Сам использую на около 30К товаров, летает.tolkkv
26.06.2015 08:36+1Извиняюсь, но мне казалось, что ElasticSearch основан на lucene, а не на громоздком Solr :) Разве это не так?
Fesor
26.06.2015 08:50-1Разве lucene и solr не одно и то же нынче?
tolkkv
26.06.2015 09:29+2В моем пониманию ES выстроен на lucene. Solr в свою очередь так же построен на базе lucene.
Т.е lucene это такая core сущность, на основе которой уже сделаны полноценные решения для поиска — Solr/ES. И как мне кажется ES появился, потому что Solr многим не нравился. Согласны?)
PS: Я бы с радостью почитал где нибудь о истории развитии ES и Solr, так как сам не имею четкого представления об этом :(
psman
25.06.2015 16:26С диапазонами — мое решение было таким habrahabr.ru/post/114113
evnuh Автор
25.06.2015 16:35Я кстати вашу статью читал когда думал, с диапазонами у меня проблемы не было, кому-то может и пригодится
dgstudio
25.06.2015 21:16Респект! Но почему бы не искать прямо по EAV-модели в SQL? Это же очень легко делается, даже по произвольному количеству атрибутов. Взяли первый атрибут, по таблице связей entity_id — value_id получили множество товаров (entity_ids), далее взяли второй атрибут и сократили это множество, далее взяли следующий атрибут… и так далее.
По каким причинам этот подход был отвергнут?ITweb
25.06.2015 22:48-1EAV не жизнеспособна на сколько-нибуть значимых объемах товарной базы
Rupper
26.06.2015 13:05+1Ну как бы 400 000 товаров легко крутились. Так что я бы не делал таких громких заявлений.
Конкретно — ulmart.ru его предыдущий дизайн сайта.ITweb
29.06.2015 14:17А причем тут дизайн и структура БД? Вы точно уверены что ulmart применяет EAV? Да и по скорости работы ulmart не показатель (по крайней мере в старой версии), я бы даже сказал наоборот.
Rupper
29.06.2015 14:30Я точно уверен :)
А у вас опять много громких заявлений.ITweb
29.06.2015 14:37Ну ок, но я как пользователь могу однозначно сказать, что ulmart очень медленно работает
Rupper
29.06.2015 14:45текущую версию делали не мы. И даже при этом, я могу сказать что тормозит скорее клиентская часть.
На нашем движке страницы отдавались не более чем за 0.5с
ITweb
29.06.2015 14:490.5 c это адски медленно, ну видимо у каждого свой порог медленно-быстро. У нас критическая планка для бекенда 100 мс.
ITweb
29.06.2015 14:36Тут еще важно сколько у товаров свойств по которым возможны фильтры. Я не поверю, что выборка по 3-4 условиям из EAV таблицы в десятки миллионов записей с нагрузкой в 30-40 запросов в секунду будет отрабатывать за приемлемое время без танцев с бубнами.
Rupper
29.06.2015 14:43Вера важна в определении технологии, безусловно. Однако Смекалка творит чудеса.
ITweb
29.06.2015 14:53Зачастую смекалки уже недостаточно и нужно принципиально новое решение, с EAV вы как раз примерно очертили границы его возможностей, это 0.5 с. Запаса прочности у такого решения практически нет, да и масштабировать его трудно и затратно.
Rupper
29.06.2015 14:59А что за проект? Где посмотреть?
ITweb
29.06.2015 15:09neolink
29.06.2015 15:18ну справедливости ради у вас там больше 100мс (по крайней мере сейчас)
кстати то что выбранные значения исчезают немного расстраивает, посмотришь цвет, а твои фильтры уже сбросились наполовинуITweb
29.06.2015 15:28Вы смотрите ответа на http запрос, а я упоминал про бекэнд. Фильтр сбрасывается только если вы перешли в другой раздел.
Rupper
29.06.2015 15:21По моим измерениям те же 0.5с на отдачу страницы.
Предлагаю закончить фехтование линейками и писюнами. Модель EAV достаточна для 99% интернет-магазинов рунета, удобна в поддержка и обладает многими преимуществами структурированности данных. Никто не спорит, что есть случаи, когда EAV не позволит выполнить требования. Ни EAV, ни другие решения не являются догмами, и ВСЕ имеют свои плюсы и минусы и области применения.ITweb
29.06.2015 15:35Согласен, но я в первом же комменте сказал про серьезные нагрузки.
Модель EAV достаточна для 99% интернет-магазинов рунета, удобна в поддержка и обладает многими преимуществами структурированности данных
Это можно отнести тогда ко всей статье в целом.
alekciy
28.06.2015 12:32Конкретику в студию. Сколько точно в граммах? И каков негативный, видимо, личный опыт использования данной модели?
FB3
26.06.2015 12:16Чтобы не делать SELECT * FROM IN () по БД, достаточно хранить минимальный набор инфы о товаре в том же редисе. Будет ещё быстрее выбирать, а минимум данных не сожрёт память.
Особенно оно понадобится, когда будет миллион товаров и они будут шардиться по разным БД. Тогда с разных серверов MySQL страница будет собираться значительно дольше, чем из редиса.
erlyvideo
за постгрес минус
alekciy
Особенно в контексте того, что он умеет индексировать JSON.
P.S. Тем неменее минусовать не стал, имхо, больше статей по Redis хороших и полезных.
evnuh Автор
Я разве что-то плохое сказал про Postgres? По-моему вполне оригинально использовать постгресовский jsonb заместо Mongo или MySQL, и я бы даже так и сделал, тем более, что оно быстрее монги. Статья о другом, о том, как можно добавить поверх существующего кода Redis и получить быстрый результат.
neolink
я думаю дело в фразе «вы же уже выбрали MySQL в качестве СУБД, если вы хороший веб-разработчик, или, может, PostgreSQL, если вы про него читали»
evnuh Автор
Ну юмор же, всем приятно знать, что он хороший веб-разработчик, даже если он кроме MySQL ничего и не пробовал :)
neolink
ну тут дело не в том что вы использующих MySQL возвысили, а в том что адептов PostgreSQL принизили
gwer
А вдруг подразумевалось, что хороший разработчик использует MySQL, а отличный разработчик хотя бы читал про PostgreSQL?