Не секрет, что каждый интернет-магазин должен помогать пользователям найти то, что им нужно. Особенно, если товаров у вас много (> 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 была следующей:
  1. Один ключ — это одно значение свойства товара, например, size-18: или color-red:
  2. Данными в каждом ключе являлся битмап длиной 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)


  1. erlyvideo
    25.06.2015 15:40
    -3

    за постгрес минус


    1. alekciy
      25.06.2015 15:46
      +2

      Особенно в контексте того, что он умеет индексировать JSON.

      P.S. Тем неменее минусовать не стал, имхо, больше статей по Redis хороших и полезных.


      1. evnuh Автор
        25.06.2015 16:08
        +3

        Я разве что-то плохое сказал про Postgres? По-моему вполне оригинально использовать постгресовский jsonb заместо Mongo или MySQL, и я бы даже так и сделал, тем более, что оно быстрее монги. Статья о другом, о том, как можно добавить поверх существующего кода Redis и получить быстрый результат.


        1. neolink
          25.06.2015 18:06

          я думаю дело в фразе «вы же уже выбрали MySQL в качестве СУБД, если вы хороший веб-разработчик, или, может, PostgreSQL, если вы про него читали»


          1. evnuh Автор
            25.06.2015 18:08
            +1

            Ну юмор же, всем приятно знать, что он хороший веб-разработчик, даже если он кроме MySQL ничего и не пробовал :)


            1. neolink
              25.06.2015 18:24
              +2

              ну тут дело не в том что вы использующих MySQL возвысили, а в том что адептов PostgreSQL принизили


          1. gwer
            25.06.2015 22:34
            +3

            А вдруг подразумевалось, что хороший разработчик использует MySQL, а отличный разработчик хотя бы читал про PostgreSQL?


  1. vitalybaev
    25.06.2015 15:54
    +1

    Мы в своем магазине (сейчас около 8000 товаров) для хранения товаро перешли на MongoDB

    Это дало нам много преимуществ:

    1) отсутствие схемы позволяет добавлять поля товара на лету (например одни имеют вес, а другие количество таблеток в упаковке
    2) возможность хранить массивы — используем в случае когда товар может быть сразу в 2-3 и более категориях
    3) эти же массивы используем для фильтров

    Скорость поразительна, разумеется важно проиндексировать важные поля


    1. evnuh Автор
      25.06.2015 16:10

      Постгресовская скорость работы с jsonb ещё поразительнее: obartunov.livejournal.com/175235.html


      1. vitalybaev
        25.06.2015 16:12

        Не буду спорить, просто с MongoDB был опыт работы в другом проекте, к Postres присматриваюсь, изучаю.


      1. Fesor
        25.06.2015 22:41

        Это первый бенчмарк который обходит монгу из тех что я видел. Их столько много, всяких разных…


    1. alekciy
      25.06.2015 16:25

      Но при этом это не снимает проблему именования атрибутов? На вскидку: менеджер для одного товара создал поле «цена» в том время как у всех товаров используется «розничная цена». Получаем два разных поля, так ведь?


      1. vitalybaev
        26.06.2015 10:56

        Несмотря на многообразие полей у товаров, они все-таки жестко прописаны в административной панели. Я лишь описал наш кейс, пока что MongoDB нас полностью устраивает.

        И цены у нас вручную нельзя прописать, они пересчитываются автоматически на основе прайсов поставщиков, равно как и скидки :)


    1. AxVPast
      25.06.2015 22:01

      Через год будуте читать статью с Хабра — как и почему публика с Монго переходила на постгресс и плакать :).


  1. artifex
    25.06.2015 16:00
    +3

    Делал сложные фильтры на много тысяч товаров с помощью Elasticsearch, заодно получил возможность полнотекстового поиска по названию, описанию и составу, учитывая опечатки.

    Ну а Postgres — это стандарт, зря вы так.


    1. evnuh Автор
      25.06.2015 16:16
      +1

      Видимо мой юмор не очень понятен, но я всего-лишь сделал акцент на том, что MySQL в вебе используется чаще, чем Postgres, добавив чувства гордости большинству :) На то есть как исторические причины (уверен, вы знаете это и без меня), так и объективные. Так и быть, заменил предложение.


      1. foxmuldercp
        25.06.2015 21:11

        Веб — он бывает разным — если это домашний вордпрессик установленный за 5 минут на самом дешевом тарифе, то на хостингах постгрес если и есть то по запросу или «покупайте наш крутой VPS/VDS» и ставьте что хотите.

        А на серьезных проектах — за последние лет 5 моих собеседований — про MySQL я не услышал ни одного вопроса. ни бекапы, ни репликации, вообще ни одного. PG, Oracle, MSSQL спрашивают много и активно, MySQL — остался только у хостеров или у тех, у кого задача с него смигрировать.


        1. evnuh Автор
          25.06.2015 21:20
          +1

          Не думаю, что стоит снова начинать этот спор.


          1. foxmuldercp
            25.06.2015 21:24
            +1

            Это не спор, а факты моего личного опыта на собеседованиях на хайлоадные проекты, куда мне было интересно попасть.
            А спорить таки да, толку нету.


        1. saterenko
          27.06.2015 22:49

          Begun (если не путаю), Mamba, Badoo используют mysql. Mysql как и Postgre – это инструменты, а эффективно их использует программист.


          1. Fesor
            27.06.2015 23:24

            — богомерзские автоинкременты в mysql (до insert не выйдет получить id записи что в некоторых случаях делает жизнь болью и вынуждает использовать UUID)
            — отсутствие возможности делать вложенные транзакции
            — alter запросы не покрываются транзакциями (не шибко удобно для миграций).

            Это тот минимум почему в некоторых задачах mysql в принципе не выйдет эффективно использовать и вам придется подстраиваться под инструмент и городить костыли.

            А так… компаний много разных всяких, кто использует mysql или postgres (допустим skype на нем работал, а может и до сих пор).


            1. saterenko
              27.06.2015 23:30
              -1

              Я с mysql работаю 15 лет (а так же работал с MSSQL, Oracle), ни чего из перечисленного не создавало мне проблемы.

              И ещё раз, это инструменты, не нравится эта лопата, возьмите другую, которая вам нравится. Бессмысленно ругаться на совковую лопату, что ей трудно копать огород, а на штыковую, что ею долго перекидывать кучу земли. А если вам нужно перекидать кучу гравия, то возьмите шахтёрскую…


              1. Fesor
                28.06.2015 00:08

                Так я ж не спорю, у меня тоже с этим проблем не было пока не перешел на инструмент, где с этим все хорошо (хотя у оракла тоже с этим все хорошо). Как бы мелочь но приятно. По поводу выбора инструментов — это так, согласен. Просто что бы знать из чего выбирать люди должны лучше представлять что зачем.


    1. alekciy
      25.06.2015 16:27

      Как впечатления? Опечатки может строить из коробки или нужна настройка?


      1. artifex
        25.06.2015 16:31

        В простом случае достаточно поиграться с магическими константами :) Elastic из коробки много чего умеет и неплохо справляется.

            'query': {
                'fuzzy_like_this': {
                    'fields': ["name", "brand"],
                    'like_text': query,
                    'fuzziness': '0.5',
                    'prefix_length': '1'
                }
            }
        


        1. vitalybaev
          26.06.2015 10:58

          Кстати, да! Elastic одна из замечательных вещей, которая активно используется во всех наших проектах. Фильтрацию товаров с ее помощью делать просто сказка


          1. alekciy
            28.06.2015 12:28

            А на сколько быстро работает? Сколько под себя ресурсов при этом хочет (Java же)? Фасетчатый поиск?


  1. mgyk
    25.06.2015 16:19
    +3

    А вы не думали над тем, чтобы просто вынести поиск в Elastic? Такие задачи туда как-раз очень хорошо подходят


    1. evnuh Автор
      25.06.2015 16:40

      Не просто думал, а даже смотрел в эту сторону. Но внедрить и настроить эластик — мне показалось задачей посложнее, чем разобраться с редис и добавить десяток строк кода.


      1. xmdy
        25.06.2015 17:00

        Эластик очень хорош, вернее, даже не эластик, а solr, он очень хорошо умеет искать, группировать, выводить количество найденных совпадений.
        Даже сам на интервалы разбивать и считать попадания в интервалы) Сам использую на около 30К товаров, летает.


        1. tolkkv
          26.06.2015 08:36
          +1

          Извиняюсь, но мне казалось, что ElasticSearch основан на lucene, а не на громоздком Solr :) Разве это не так?


          1. Fesor
            26.06.2015 08:50
            -1

            Разве lucene и solr не одно и то же нынче?


            1. tolkkv
              26.06.2015 09:29
              +2

              В моем пониманию ES выстроен на lucene. Solr в свою очередь так же построен на базе lucene.
              Т.е lucene это такая core сущность, на основе которой уже сделаны полноценные решения для поиска — Solr/ES. И как мне кажется ES появился, потому что Solr многим не нравился. Согласны?)
              PS: Я бы с радостью почитал где нибудь о истории развитии ES и Solr, так как сам не имею четкого представления об этом :(


  1. psman
    25.06.2015 16:26

    С диапазонами — мое решение было таким habrahabr.ru/post/114113


    1. evnuh Автор
      25.06.2015 16:35

      Я кстати вашу статью читал когда думал, с диапазонами у меня проблемы не было, кому-то может и пригодится


  1. dgstudio
    25.06.2015 21:16

    Респект! Но почему бы не искать прямо по EAV-модели в SQL? Это же очень легко делается, даже по произвольному количеству атрибутов. Взяли первый атрибут, по таблице связей entity_id — value_id получили множество товаров (entity_ids), далее взяли второй атрибут и сократили это множество, далее взяли следующий атрибут… и так далее.

    По каким причинам этот подход был отвергнут?


    1. ITweb
      25.06.2015 22:48
      -1

      EAV не жизнеспособна на сколько-нибуть значимых объемах товарной базы


      1. Rupper
        26.06.2015 13:05
        +1

        Ну как бы 400 000 товаров легко крутились. Так что я бы не делал таких громких заявлений.
        Конкретно — ulmart.ru его предыдущий дизайн сайта.


        1. ITweb
          29.06.2015 14:17

          А причем тут дизайн и структура БД? Вы точно уверены что ulmart применяет EAV? Да и по скорости работы ulmart не показатель (по крайней мере в старой версии), я бы даже сказал наоборот.


          1. Rupper
            29.06.2015 14:30

            Я точно уверен :)
            А у вас опять много громких заявлений.


            1. ITweb
              29.06.2015 14:37

              Ну ок, но я как пользователь могу однозначно сказать, что ulmart очень медленно работает


              1. Rupper
                29.06.2015 14:45

                текущую версию делали не мы. И даже при этом, я могу сказать что тормозит скорее клиентская часть.
                На нашем движке страницы отдавались не более чем за 0.5с


                1. ITweb
                  29.06.2015 14:49

                  0.5 c это адски медленно, ну видимо у каждого свой порог медленно-быстро. У нас критическая планка для бекенда 100 мс.


        1. ITweb
          29.06.2015 14:36

          Тут еще важно сколько у товаров свойств по которым возможны фильтры. Я не поверю, что выборка по 3-4 условиям из EAV таблицы в десятки миллионов записей с нагрузкой в 30-40 запросов в секунду будет отрабатывать за приемлемое время без танцев с бубнами.


          1. Rupper
            29.06.2015 14:43

            Вера важна в определении технологии, безусловно. Однако Смекалка творит чудеса.


            1. ITweb
              29.06.2015 14:53

              Зачастую смекалки уже недостаточно и нужно принципиально новое решение, с EAV вы как раз примерно очертили границы его возможностей, это 0.5 с. Запаса прочности у такого решения практически нет, да и масштабировать его трудно и затратно.


              1. Rupper
                29.06.2015 14:59

                А что за проект? Где посмотреть?


                1. ITweb
                  29.06.2015 15:09

                  1. neolink
                    29.06.2015 15:18

                    ну справедливости ради у вас там больше 100мс (по крайней мере сейчас)
                    кстати то что выбранные значения исчезают немного расстраивает, посмотришь цвет, а твои фильтры уже сбросились наполовину


                    1. ITweb
                      29.06.2015 15:28

                      Вы смотрите ответа на http запрос, а я упоминал про бекэнд. Фильтр сбрасывается только если вы перешли в другой раздел.


                  1. Rupper
                    29.06.2015 15:21

                    По моим измерениям те же 0.5с на отдачу страницы.
                    Предлагаю закончить фехтование линейками и писюнами. Модель EAV достаточна для 99% интернет-магазинов рунета, удобна в поддержка и обладает многими преимуществами структурированности данных. Никто не спорит, что есть случаи, когда EAV не позволит выполнить требования. Ни EAV, ни другие решения не являются догмами, и ВСЕ имеют свои плюсы и минусы и области применения.


                    1. ITweb
                      29.06.2015 15:35

                      Согласен, но я в первом же комменте сказал про серьезные нагрузки.

                      Модель EAV достаточна для 99% интернет-магазинов рунета, удобна в поддержка и обладает многими преимуществами структурированности данных


                      Это можно отнести тогда ко всей статье в целом.


                  1. unVooDoo
                    29.06.2015 17:29

                    а какую схему использовали Вы, если не секрет?


      1. erlyvideo
        26.06.2015 17:33

        с EAV как раз можно делать очень быстрые индексные ranged запросы.


      1. alekciy
        28.06.2015 12:32

        Конкретику в студию. Сколько точно в граммах? И каков негативный, видимо, личный опыт использования данной модели?


  1. FB3
    26.06.2015 12:16

    Чтобы не делать SELECT * FROM IN () по БД, достаточно хранить минимальный набор инфы о товаре в том же редисе. Будет ещё быстрее выбирать, а минимум данных не сожрёт память.
    Особенно оно понадобится, когда будет миллион товаров и они будут шардиться по разным БД. Тогда с разных серверов MySQL страница будет собираться значительно дольше, чем из редиса.